Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/Character.cs
3 using Barotrauma.IO;
6 using FarseerPhysics;
7 using FarseerPhysics.Dynamics;
8 using Microsoft.Xna.Framework;
9 using System;
10 using System.Collections.Generic;
11 using System.Collections.Immutable;
12 using System.Diagnostics;
13 using System.Linq;
14 using System.Xml.Linq;
15 #if SERVER
16 using System.Text;
17 #endif
18 
19 namespace Barotrauma
20 {
21  public enum CharacterTeamType
22  {
23  None = 0,
24  Team1 = 1,
25  Team2 = 2,
26  FriendlyNPC = 3
27  }
28 
29  public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
30 
32  {
33  public readonly static List<Character> CharacterList = new List<Character>();
34 
35  public static int CharacterUpdateInterval = 1;
36  private static int characterUpdateTick = 1;
37 
38  public const float MaxHighlightDistance = 150.0f;
39  public const float MaxDragDistance = 200.0f;
40 
41  partial void UpdateLimbLightSource(Limb limb);
42 
43  private bool enabled = true;
44  public bool Enabled
45  {
46  get
47  {
48  return enabled && !Removed;
49  }
50  set
51  {
52  if (value == enabled) { return; }
53 
54  if (Removed)
55  {
56  enabled = false;
57  return;
58  }
59 
60  enabled = value;
61 
62  foreach (Limb limb in AnimController.Limbs)
63  {
64  if (limb.IsSevered) { continue; }
65  if (limb.body != null)
66  {
67  limb.body.Enabled = enabled;
68  }
69  UpdateLimbLightSource(limb);
70  }
71  foreach (var item in HeldItems)
72  {
73  if (item.body == null) { continue; }
74  if (!enabled)
75  {
76  item.body.Enabled = false;
77  }
78  else if (item.GetComponent<Holdable>() is { IsActive: true })
79  {
80  //held items includes all items in hand slots
81  //we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs
82  item.body.Enabled = true;
83  }
84 
85  }
87  }
88  }
89 
90 
91  private bool disabledByEvent;
95  public bool DisabledByEvent
96  {
97  get { return disabledByEvent; }
98  set
99  {
100  if (value == disabledByEvent) { return; }
101  disabledByEvent = value;
102  if (disabledByEvent)
103  {
104  Enabled = false;
105  CharacterList.Remove(this);
106  if (AiTarget != null) { AITarget.List.Remove(AiTarget); }
107  }
108  else
109  {
110  if (!CharacterList.Contains(this)) { CharacterList.Add(this); }
111  if (AiTarget != null && !AITarget.List.Contains(AiTarget)) { AITarget.List.Add(AiTarget); }
112  }
113  }
114  }
115 
116  public Hull PreviousHull = null;
117  public Hull CurrentHull = null;
118 
123  {
124  get
125  {
126  if (GameMain.NetworkMember == null)
127  {
128  return false;
129  }
130  else if (GameMain.NetworkMember.IsClient)
131  {
132  //all characters except the client's own character are controlled by the server
133  return this != Controlled;
134  }
135  else
136  {
137  return IsRemotePlayer;
138  }
139  }
140  }
141 
145  public bool IsRemotePlayer { get; set; }
146 
147  public bool IsLocalPlayer => Controlled == this;
148  public bool IsPlayer => Controlled == this || IsRemotePlayer;
149 
154  public bool IsBot => !IsPlayer && AIController is HumanAIController { Enabled: true };
155  public bool IsEscorted { get; set; }
156  public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
157 
158  public bool DoesBleed
159  {
160  get => Params.Health.DoesBleed;
161  set => Params.Health.DoesBleed = value;
162  }
163 
164  public readonly Dictionary<Identifier, SerializableProperty> Properties;
165  public Dictionary<Identifier, SerializableProperty> SerializableProperties
166  {
167  get { return Properties; }
168  }
169 
170  public Key[] Keys
171  {
172  get { return keys; }
173  }
174 
175  protected Key[] keys;
176 
177  private HumanPrefab humanPrefab;
179  {
180  get { return humanPrefab; }
181  set
182  {
183  if (humanPrefab == value) { return; }
184  humanPrefab = value;
185 
186  if (humanPrefab != null)
187  {
189  if (GameMain.NetworkMember != null)
190  {
192  }
193  }
194  else
195  {
197  }
198  }
199  }
200 
201  private Identifier? faction;
202  public Identifier Faction
203  {
204  get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; }
205  set { faction = value; }
206  }
207 
208  private CharacterTeamType teamID;
210  {
211  get { return teamID; }
212  set
213  {
214  teamID = value;
215  if (info != null) { info.TeamID = value; }
216  }
217  }
218 
219 
220  private CharacterTeamType? originalTeamID;
222  {
223  get { return originalTeamID ?? teamID; }
224  }
225 
226  private Wallet wallet;
227 
228  public Wallet Wallet
229  {
230  get
231  {
232  ThrowIfAccessingWalletsInSingleplayer();
233  return wallet;
234  }
235  set
236  {
237  ThrowIfAccessingWalletsInSingleplayer();
238  wallet = value;
239  }
240  }
241 
242  public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
243  public readonly HashSet<Projectile> AttachedProjectiles = new HashSet<Projectile>();
244 
245  protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
247  private const string OriginalChangeTeamIdentifier = "original";
248 
249  private void ThrowIfAccessingWalletsInSingleplayer()
250  {
251 #if CLIENT && DEBUG
252  if (Screen.Selected is TestScreen) { return; }
253 #endif
255  {
256  throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead.");
257  }
258  }
259 
260  public void SetOriginalTeam(CharacterTeamType newTeam)
261  {
262  TryRemoveTeamChange(OriginalChangeTeamIdentifier);
264  TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange);
265  }
266 
267  private void ChangeTeam(CharacterTeamType newTeam)
268  {
269  if (newTeam == teamID) { return; }
270  if (originalTeamID == null) { originalTeamID = teamID; }
271  TeamID = newTeam;
272  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
273  {
274  return;
275  }
276  // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
277  var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
278  SetOrder(order, isNewOrder: true, speak: false);
279 
280 #if SERVER
281  GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData());
282 #endif
283  }
284 
285  public bool HasTeamChange(string identifier)
286  {
287  return activeTeamChanges.ContainsKey(identifier);
288  }
289 
290  public bool TryAddNewTeamChange(string identifier, ActiveTeamChange newTeamChange)
291  {
292  bool success = activeTeamChanges.TryAdd(identifier, newTeamChange);
293  if (success)
294  {
295  if (currentTeamChange == null)
296  {
297  // set team logic to use active team changes as soon as the first team change is added
299  }
300  }
301  else
302  {
303 #if DEBUG
304  DebugConsole.ThrowError("Tried to add an existing team change! Make sure to check if the team change exists first.");
305 #endif
306  }
307  return success;
308  }
309  public bool TryRemoveTeamChange(string identifier)
310  {
311  if (activeTeamChanges.TryGetValue(identifier, out ActiveTeamChange removedTeamChange))
312  {
313  if (currentTeamChange == removedTeamChange)
314  {
315  currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier];
316  }
317  }
318  return activeTeamChanges.Remove(identifier);
319  }
320 
321  public void UpdateTeam()
322  {
323  if (currentTeamChange == null)
324  {
325  return;
326  }
327 
328  ActiveTeamChange bestTeamChange = currentTeamChange;
329  foreach (var desiredTeamChange in activeTeamChanges) // order of iteration matters because newest is preferred when multiple same-priority team changes exist
330  {
331  if (bestTeamChange.TeamChangePriority < desiredTeamChange.Value.TeamChangePriority)
332  {
333  bestTeamChange = desiredTeamChange.Value;
334  }
335  }
336  if (TeamID != bestTeamChange.DesiredTeamId)
337  {
338  ChangeTeam(bestTeamChange.DesiredTeamId);
339  currentTeamChange = bestTeamChange;
340 
341  if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior
342  {
343  var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
344  SetOrder(order, isNewOrder: true, speak: false);
345  }
346  }
347  }
348 
349  public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2;
350 
351  public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2;
352 
353  public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2;
354 
355  public bool IsInstigator => CombatAction is { IsInstigator: true };
356 
364  public bool IsCriminal;
365 
369  public bool IsHostileEscortee;
370 
372 
374 
375  private Vector2 cursorPosition;
376 
377  protected float oxygenAvailable;
378 
379  //seed used to generate this character
380  public readonly string Seed;
381  protected Item focusedItem;
382  private Character selectedCharacter, selectedBy;
383 
384  private const int maxLastAttackerCount = 4;
385 
386  public class Attacker
387  {
389  public float Damage;
390  }
391 
392  private readonly List<Attacker> lastAttackers = new List<Attacker>();
393  public IEnumerable<Attacker> LastAttackers => lastAttackers;
394  public Character LastAttacker => lastAttackers.LastOrDefault()?.Character;
395  public Character LastOrderedCharacter { get; private set; }
396  public Character SecondLastOrderedCharacter { get; private set; }
397 
399 
401 
402  public Dictionary<ItemPrefab, double> ItemSelectedDurations
403  {
404  get { return itemSelectedDurations; }
405  }
406  private readonly Dictionary<ItemPrefab, double> itemSelectedDurations = new Dictionary<ItemPrefab, double>();
407  private double itemSelectedTime;
408 
409  public float InvisibleTimer { get; set; }
410 
411  public readonly CharacterPrefab Prefab;
412 
413  public readonly CharacterParams Params;
414 
415  public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
416 
417  public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName);
418 
419  public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
420 
421  public bool IsHumanoid => Params.Humanoid;
422 
423  public bool IsMachine => Params.IsMachine;
424 
425  public bool IsHusk => Params.Husk;
426  public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType("disguiseashusk".ToIdentifier()) > 0;
427  public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains("huskinfected".ToIdentifier());
428 
429  public bool IsMale => info?.IsMale ?? false;
430 
431  public bool IsFemale => info?.IsFemale ?? false;
432 
433  public string BloodDecalName => Params.BloodDecal;
434 
435  public bool CanSpeak
436  {
437  get => Params.CanSpeak;
438  set => Params.CanSpeak = value;
439  }
440 
441  public bool NeedsAir
442  {
443  get => Params.NeedsAir;
444  set => Params.NeedsAir = value;
445  }
446 
447  public bool NeedsWater
448  {
449  get => Params.NeedsWater;
450  set => Params.NeedsWater = value;
451  }
452 
454 
455  public float Noise
456  {
457  get => Params.Noise;
458  set => Params.Noise = value;
459  }
460 
461  public float Visibility
462  {
463  get => Params.Visibility;
464  set => Params.Visibility = value;
465  }
466 
468  {
469  get => Params.AI?.MaxPerceptionDistance ?? 0;
470  set
471  {
472  if (Params.AI != null)
473  {
475  }
476  }
477  }
478  public bool IsTraitor
479  {
480  get;
481  set;
482  }
483 
486 
487  private float attackCoolDown;
488 
489  public List<Order> CurrentOrders => Info?.CurrentOrders;
490  public bool IsDismissed => GetCurrentOrderWithTopPriority() == null;
491 
492  private readonly Dictionary<ActionType, List<StatusEffect>> statusEffects = new Dictionary<ActionType, List<StatusEffect>>();
493 
495  {
496  get;
497  set;
498  }
499 
500  public Vector2 AimRefPosition
501  {
502  get
503  {
504  if (ViewTarget == null) { return AnimController.AimSourcePos; }
505 
506  Vector2 viewTargetWorldPos = ViewTarget.WorldPosition;
507  if (ViewTarget is Item targetItem)
508  {
509  Turret turret = targetItem.GetComponent<Turret>();
510  if (turret != null)
511  {
512  viewTargetWorldPos = new Vector2(
513  targetItem.WorldRect.X + turret.TransformedBarrelPos.X,
514  targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
515  }
516  }
517  return Position + (viewTargetWorldPos - WorldPosition);
518  }
519  }
520 
521  private CharacterInfo info;
523  {
524  get
525  {
526  return info;
527  }
528  set
529  {
530  if (info != null && info != value)
531  {
532  info.Remove();
533  }
534  info = value;
535  if (info != null)
536  {
537  info.Character = this;
538  }
539  }
540  }
541 
542  public Identifier VariantOf => Prefab.VariantOf;
543 
544  public string Name
545  {
546  get
547  {
548  return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName.Value;
549  }
550  }
551 
552  public string DisplayName
553  {
554  get
555  {
556  if (IsPet)
557  {
558  string petName = (AIController as EnemyAIController).PetBehavior.GetTagName();
559  if (!string.IsNullOrEmpty(petName)) { return petName; }
560  }
561 
562  if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; }
563  LocalizedString displayName = Params.DisplayName;
564  if (displayName.IsNullOrWhiteSpace())
565  {
567  {
568  displayName = TextManager.Get($"Character.{SpeciesName}");
569  }
570  else
571  {
572  displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}");
573  }
574  }
575  return displayName.IsNullOrWhiteSpace() ? Name : displayName.Value;
576  }
577  }
578 
579  //Only used by server logs to determine "true identity" of the player for cases when they're disguised
580  public string LogName
581  {
582  get
583  {
584  if (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises) return Name;
585  return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName.Value;
586  }
587  }
588 
589  private float hideFaceTimer;
590  public bool HideFace
591  {
592  get
593  {
594  return hideFaceTimer > 0.0f;
595  }
596  set
597  {
598  bool wasHidden = HideFace;
599  hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
600  bool isHidden = HideFace;
601  if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden)
602  {
603  info.CheckDisguiseStatus(true);
604  }
605  }
606  }
607 
608  public string ConfigPath => Params.File.Path.Value;
609 
610  public float Mass
611  {
612  get { return AnimController.Mass; }
613  }
614 
615  public CharacterInventory Inventory { get; private set; }
616 
620  public bool DisableInteract { get; set; }
621 
626  public bool DisableFocusingOnEntities { get; set; }
627 
628  //text displayed when the character is highlighted if custom interact is set
629  public LocalizedString CustomInteractHUDText { get; private set; }
630  private Action<Character, Character> onCustomInteract;
632 
635  {
636  get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; }
637  }
638 
639  private float lockHandsTimer;
640  public bool LockHands
641  {
642  get
643  {
644  return lockHandsTimer > 0.0f;
645  }
646  set
647  {
648  lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
649  if (value)
650  {
651  SelectedCharacter = null;
652  }
653 #if CLIENT
654  HintManager.OnHandcuffed(this);
655 #endif
656  }
657  }
658 
659  public bool AllowInput => !Removed && !IsIncapacitated && Stun <= 0.0f;
660 
661  public bool CanMove
662  {
663  get
664  {
665  if (!AnimController.InWater && !AnimController.CanWalk) { return false; }
666  if (!AllowInput) { return false; }
667  return true;
668  }
669  }
671 
672  // Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction.
673  public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null;
674 
675  public Vector2 CursorPosition
676  {
677  get { return cursorPosition; }
678  set
679  {
680  if (!MathUtils.IsValid(value)) { return; }
681  cursorPosition = value;
682  }
683  }
684 
685  public Vector2 SmoothedCursorPosition
686  {
687  get;
688  private set;
689  }
690 
691  public Vector2 CursorWorldPosition
692  {
693  get { return Submarine == null ? cursorPosition : cursorPosition + Submarine.Position; }
694  }
695 
696  public Character FocusedCharacter { get; set; }
697 
699  {
700  get { return selectedCharacter; }
701  set
702  {
703  if (value == selectedCharacter) { return; }
704  if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
705  selectedCharacter = value;
706  if (selectedCharacter != null) {selectedCharacter.selectedBy = this; }
707 #if CLIENT
709 #endif
710  bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
711  CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
712 
713  if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet })
714  {
715 #if SERVER
716  var mpCampaign = GameMain.GameSession.Campaign as MultiPlayerCampaign;
717  if (mpCampaign != null && GameMain.Server is { ServerSettings: { } settings })
718  {
719  switch (settings.LootedMoneyDestination)
720  {
721  case LootedMoneyDestination.Wallet when IsPlayer:
722  Wallet.Give(balance);
723  break;
724  default:
725  mpCampaign.Bank.Give(balance);
726  break;
727 
728  }
729  }
730 
731  GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money);
732 
733  grabbedWallet.Deduct(balance);
734  //we need to save the grabbed character's wallet this at this point to ensure
735  //the client doesn't get to keep the money if they respawn
736  if (mpCampaign != null && selectedCharacter.Info != null)
737  {
738  var characterCampaignData = mpCampaign?.GetCharacterData(selectedCharacter.Info);
739  if (characterCampaignData!= null)
740  {
741  characterCampaignData.WalletData = grabbedWallet.Save();
742  characterCampaignData?.ApplyWalletData(selectedCharacter);
743  }
744  }
745 #elif CLIENT
747  {
748  spCampaign.Bank.Give(balance);
749  }
750  grabbedWallet.Deduct(balance);
751 #endif
752  }
753  }
754  }
755 
756  public Character SelectedBy
757  {
758  get { return selectedBy; }
759  set
760  {
761  if (selectedBy != null)
762  selectedBy.selectedCharacter = null;
763  selectedBy = value;
764  if (selectedBy != null)
765  selectedBy.selectedCharacter = this;
766  }
767  }
768 
772  public IEnumerable<Item> HeldItems
773  {
774  get
775  {
776  var item1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
777  var item2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
778  if (item1 != null) { yield return item1; }
779  if (item2 != null && item2 != item1) { yield return item2; }
780  }
781  }
782 
784  {
785  int rangedItemCount = 0;
786  foreach (var item in HeldItems)
787  {
788  if (item.GetComponent<RangedWeapon>() != null)
789  {
790  rangedItemCount++;
791  }
792 
793  if (rangedItemCount > 1)
794  {
795  return true;
796  }
797  }
798 
799  return false;
800  }
801 
802  private float lowPassMultiplier;
803  public float LowPassMultiplier
804  {
805  get { return lowPassMultiplier; }
806  set { lowPassMultiplier = MathHelper.Clamp(value, 0.0f, 1.0f); }
807  }
808 
809  private float obstructVisionAmount;
810  public float ObstructVisionAmount
811  {
812  get { return obstructVisionAmount; }
813  set
814  {
815  obstructVisionAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
816  }
817  }
818 
822  public bool ObstructVision
823  {
824  get
825  {
826  return obstructVisionAmount > 0.01f;
827  }
828  set
829  {
830  obstructVisionAmount = value ? 0.5f : 0.0f;
831  }
832  }
833 
834  private double pressureProtectionLastSet;
835  private float pressureProtection;
836  public float PressureProtection
837  {
838  get { return pressureProtection; }
839  set
840  {
841  pressureProtection = Math.Max(value, pressureProtection);
842  pressureProtectionLastSet = Timing.TotalTime;
843  }
844  }
845 
849  public bool InPressure
850  {
851  get { return CurrentHull == null || CurrentHull.LethalPressure > 0.0f; }
852  }
853 
858  {
859  get { return AnimController?.Anim ?? AnimController.Animation.None; }
860  }
861 
862  public const float KnockbackCooldown = 5.0f;
864 
865  private float ragdollingLockTimer;
866  public bool IsRagdolled;
867  public bool IsForceRagdolled;
868  public bool dontFollowCursor;
869 
870  public bool IsIncapacitated
871  {
872  get
873  {
874  if (IsUnconscious) { return true; }
876  }
877  }
878 
879  public bool IsUnconscious
880  {
881  get { return CharacterHealth.IsUnconscious; }
882  }
883 
884  public bool IsHandcuffed
885  {
886  get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
887  }
888 
889  public bool IsPet
890  {
891  get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; }
892  }
893 
894  public float Oxygen
895  {
896  get { return CharacterHealth.OxygenAmount; }
897  set
898  {
899  if (!MathUtils.IsValid(value)) return;
900  CharacterHealth.OxygenAmount = MathHelper.Clamp(value, -100.0f, 100.0f);
901  }
902  }
903 
904  public float OxygenAvailable
905  {
906  get { return oxygenAvailable; }
907  set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); }
908  }
909 
910  public float HullOxygenPercentage
911  {
912  get { return CurrentHull?.OxygenPercentage ?? 0.0f; }
913  }
914 
915  public bool UseHullOxygen { get; set; } = true;
916 
917  public float Stun
918  {
919  get { return IsRagdolled && !AnimController.IsHangingWithRope ? 1.0f : CharacterHealth.Stun; }
920  set
921  {
922  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
923  SetStun(value, true);
924  }
925  }
926 
927  public CharacterHealth CharacterHealth { get; private set; }
928 
929  public bool DisableHealthWindow;
930 
931  // These properties needs to be exposed for status effects
932  public float Vitality => CharacterHealth.Vitality;
933  public float Health => Vitality;
934  public float HealthPercentage => CharacterHealth.HealthPercentage;
935  public float MaxVitality => CharacterHealth.MaxVitality;
936  public float MaxHealth => MaxVitality;
937 
941  public bool WasFullHealth => CharacterHealth.WasInFullHealth;
942  public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
943  public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
944  public float EmpVulnerability => Params.Health.EmpVulnerability;
945  public float PoisonVulnerability => Params.Health.PoisonVulnerability;
946  public bool IsFlipped => AnimController.IsFlipped;
947 
948  public float Bloodloss
949  {
950  get { return CharacterHealth.BloodlossAmount; }
951  set
952  {
953  if (!MathUtils.IsValid(value)) { return; }
955  }
956  }
957 
958  public float Bleeding
959  {
961  }
962 
963  private bool speechImpedimentSet;
964 
965  //value between 0-100 (50 = speech range is reduced by 50%)
966  private float speechImpediment;
967  public float SpeechImpediment
968  {
969  get
970  {
971  if (!CanSpeak || IsUnconscious || IsKnockedDown) { return 100.0f; }
972  return speechImpediment;
973  }
974  set
975  {
976  if (value < speechImpediment) { return; }
977  speechImpedimentSet = true;
978  speechImpediment = MathHelper.Clamp(value, 0.0f, 100.0f);
979  }
980  }
981 
982  private float textChatVolume;
983 
988  public float TextChatVolume
989  {
990  get => textChatVolume;
991  set => textChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
992  }
993 
994  public float PressureTimer
995  {
996  get;
997  private set;
998  }
999 
1000  public float DisableImpactDamageTimer
1001  {
1002  get;
1003  set;
1004  }
1005 
1006  public bool IgnoreMeleeWeapons
1007  {
1008  get;
1009  set;
1010  }
1011 
1015  public float CurrentSpeed
1016  {
1017  get { return AnimController?.Collider?.LinearVelocity.Length() ?? 0.0f; }
1018  }
1019 
1020  private Item _selectedItem;
1024  public Item SelectedItem
1025  {
1026  get => _selectedItem;
1027  set
1028  {
1029  var prevSelectedItem = _selectedItem;
1030  _selectedItem = value;
1031  if (value is not null)
1032  {
1033  CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value));
1034  }
1035 #if CLIENT
1036  HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
1037  if (Controlled == this)
1038  {
1039  _selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
1040 
1041  if (_selectedItem == null)
1042  {
1044  }
1045  else if (!_selectedItem.IsLadder)
1046  {
1048  }
1049 
1050  _selectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
1051  }
1052 #endif
1053  if (prevSelectedItem != null && (_selectedItem == null || _selectedItem != prevSelectedItem) && itemSelectedTime > 0)
1054  {
1055  double selectedDuration = Timing.TotalTime - itemSelectedTime;
1056  if (itemSelectedDurations.ContainsKey(prevSelectedItem.Prefab))
1057  {
1058  itemSelectedDurations[prevSelectedItem.Prefab] += selectedDuration;
1059  }
1060  else
1061  {
1062  itemSelectedDurations.Add(prevSelectedItem.Prefab, selectedDuration);
1063  }
1064  itemSelectedTime = 0;
1065  }
1066  if (_selectedItem != null && (prevSelectedItem == null || prevSelectedItem != _selectedItem))
1067  {
1068  itemSelectedTime = Timing.TotalTime;
1069  }
1070  if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null)
1071  {
1072  prevSelectedItem.OnDeselect(this);
1073  }
1074  }
1075  }
1079  public Item SelectedSecondaryItem { get; set; }
1080  public void ReleaseSecondaryItem() => SelectedSecondaryItem = null;
1084  public bool HasSelectedAnyItem => SelectedItem != null || SelectedSecondaryItem != null;
1090  public bool IsAnySelectedItem(Item item) => item == SelectedItem || item == SelectedSecondaryItem;
1091  public bool HasSelectedAnotherSecondaryItem(Item item) => SelectedSecondaryItem != null && SelectedSecondaryItem != item;
1092 
1093  public Item FocusedItem
1094  {
1095  get { return focusedItem; }
1096  set { focusedItem = value; }
1097  }
1098 
1099  public Item PickingItem
1100  {
1101  get;
1102  set;
1103  }
1104 
1106  {
1107  get { return null; }
1108  }
1109 
1110  private bool isDead;
1111  public bool IsDead
1112  {
1113  get { return isDead; }
1114  set
1115  {
1116  if (isDead == value) { return; }
1117  if (value)
1118  {
1119  Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
1120  }
1121  else
1122  {
1123  Revive();
1124  }
1125  }
1126  }
1127 
1128  public bool EnableDespawn { get; set; } = true;
1129 
1131  {
1132  get;
1133  private set;
1134  }
1135 
1136  //can other characters select (= grab) this character
1137  public bool CanBeSelected
1138  {
1139  get
1140  {
1141  return !Removed;
1142  }
1143  }
1144 
1145  public bool IsDraggable => !Removed || AnimController.Draggable;
1146 
1147  public bool CanAim
1148  {
1149  get
1150  {
1151  return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope);
1152  }
1153  }
1154 
1155  public bool InWater => AnimController is AnimController { InWater: true };
1156 
1157  public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100;
1158 
1162  public bool GodMode = false;
1163 
1164  public bool Unkillable
1165  {
1166  get { return CharacterHealth.Unkillable; }
1167  set { CharacterHealth.Unkillable = value; }
1168  }
1169 
1173  public bool UseHealthWindow
1174  {
1175  get { return CharacterHealth.UseHealthWindow; }
1176  set { CharacterHealth.UseHealthWindow = value; }
1177  }
1178 
1180  public Identifier MerchantIdentifier;
1181 
1182  private bool accessRemovedCharacterErrorShown;
1183  public override Vector2 SimPosition
1184  {
1185  get
1186  {
1187  if (AnimController?.Collider == null)
1188  {
1189  if (!accessRemovedCharacterErrorShown)
1190  {
1191  string errorMsg = "Attempted to access a potentially removed character. Character: [name], id: " + ID + ", removed: " + Removed + ".";
1192  if (AnimController == null)
1193  {
1194  errorMsg += " AnimController == null";
1195  }
1196  else if (AnimController.Collider == null)
1197  {
1198  errorMsg += " AnimController.Collider == null";
1199  }
1200  errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
1201  DebugConsole.NewMessage(errorMsg.Replace("[name]", Name), Color.Red);
1202  GameAnalyticsManager.AddErrorEventOnce(
1203  "Character.SimPosition:AccessRemoved",
1204  GameAnalyticsManager.ErrorSeverity.Error,
1205  errorMsg.Replace("[name]", SpeciesName.Value) + "\n" + Environment.StackTrace.CleanupStackTrace());
1206  accessRemovedCharacterErrorShown = true;
1207  }
1208  return Vector2.Zero;
1209  }
1210 
1212  }
1213  }
1214 
1215  public override Vector2 Position
1216  {
1217  get { return ConvertUnits.ToDisplayUnits(SimPosition); }
1218  }
1219 
1220  public override Vector2 DrawPosition
1221  {
1222  get
1223  {
1224  if (AnimController.MainLimb == null) { return Vector2.Zero; }
1226  }
1227  }
1228 
1229  public HashSet<Identifier> MarkedAsLooted = new();
1230 
1231  public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
1232  public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
1233 
1234  public float AITurretPriority
1235  {
1236  get => Params.AITurretPriority;
1237  private set => Params.AITurretPriority = value;
1238  }
1239 
1240  public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
1241  public OnDeathHandler OnDeath;
1242 
1243  public delegate void OnAttackedHandler(Character attacker, AttackResult attackResult);
1244  public OnAttackedHandler OnAttacked;
1245 
1255  public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null, bool spawnInitialItems = true)
1256  {
1257  return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent: true, ragdoll, spawnInitialItems);
1258  }
1259 
1272  public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true)
1273  {
1274  if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
1275  {
1276  speciesName = Path.GetFileNameWithoutExtension(speciesName);
1277  }
1278  return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound, spawnInitialItems);
1279  }
1280 
1281  public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true)
1282  {
1283  var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
1284  if (prefab == null)
1285  {
1286  string errorMsg = $"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace;
1287  if (throwErrorIfNotFound)
1288  {
1289  DebugConsole.ThrowError(errorMsg);
1290  }
1291  else
1292  {
1293  DebugConsole.AddWarning(errorMsg);
1294  }
1295 
1296  return null;
1297  }
1298  return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, spawnInitialItems);
1299  }
1300 
1301  public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true)
1302  {
1303  Character newCharacter = null;
1304  if (prefab.Identifier != CharacterPrefab.HumanSpeciesName || hasAi)
1305  {
1306  var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
1307 
1308  var ai = (prefab.Identifier == CharacterPrefab.HumanSpeciesName || aiCharacter.Params.UseHumanAI) ?
1309  new HumanAIController(aiCharacter) as AIController :
1310  new EnemyAIController(aiCharacter, seed);
1311  aiCharacter.SetAI(ai);
1312  newCharacter = aiCharacter;
1313  }
1314  else
1315  {
1316  newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
1317  }
1318 
1319 #if SERVER
1320  if (GameMain.Server != null && Spawner != null && createNetworkEvent)
1321  {
1322  Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newCharacter));
1323  }
1324 #endif
1325 
1326  GameMain.LuaCs.Hook.Call("character.created", new object[] { newCharacter });
1327 
1328  return newCharacter;
1329  }
1330 
1331  protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true)
1332  : base(null, id)
1333  {
1334  wallet = new Wallet(Option<Character>.Some(this));
1335  if (GameMain.GameSession?.Campaign?.Bank is { } bank)
1336  {
1337  wallet.SetRewardDistribution(bank.RewardDistribution);
1338  }
1339 
1340  this.Seed = seed;
1341  this.Prefab = prefab;
1342  MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
1343 
1344  IsRemotePlayer = isRemotePlayer;
1345 
1346  oxygenAvailable = 100.0f;
1347  aiTarget = new AITarget(this);
1348 
1349  lowPassMultiplier = 1.0f;
1350 
1351  Properties = SerializableProperty.GetProperties(this);
1352 
1353  Params = new CharacterParams(prefab.ContentFile as CharacterFile);
1354 
1355  Info = characterInfo;
1356 
1357  Identifier speciesName = prefab.Identifier;
1358 
1359  if (VariantOf == CharacterPrefab.HumanSpeciesName || speciesName == CharacterPrefab.HumanSpeciesName)
1360  {
1361  if (!VariantOf.IsEmpty)
1362  {
1363  DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!",
1364  contentPackage: Prefab.ContentPackage);
1365  }
1366  if (characterInfo == null)
1367  {
1369  }
1370  }
1371  if (Info != null)
1372  {
1373  teamID = Info.TeamID;
1374  //no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
1375  Info.IsNewHire = false;
1376  }
1377  keys = new Key[Enum.GetNames(typeof(InputType)).Length];
1378  for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
1379  {
1380  keys[i] = new Key((InputType)i);
1381  }
1382 
1383  var mainElement = prefab.ConfigElement;
1384  InitProjSpecific(mainElement);
1385 
1386  List<ContentXElement> inventoryElements = new List<ContentXElement>();
1387  List<float> inventoryCommonness = new List<float>();
1388  List<ContentXElement> healthElements = new List<ContentXElement>();
1389  List<float> healthCommonness = new List<float>();
1390  foreach (var subElement in mainElement.Elements())
1391  {
1392  switch (subElement.Name.ToString().ToLowerInvariant())
1393  {
1394  case "inventory":
1395  inventoryElements.Add(subElement);
1396  inventoryCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
1397  break;
1398  case "health":
1399  healthElements.Add(subElement);
1400  healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
1401  break;
1402  case "statuseffect":
1403  var statusEffect = StatusEffect.Load(subElement, Name);
1404  if (statusEffect != null)
1405  {
1406  if (!statusEffects.ContainsKey(statusEffect.type))
1407  {
1408  statusEffects.Add(statusEffect.type, new List<StatusEffect>());
1409  }
1410  statusEffects[statusEffect.type].Add(statusEffect);
1411  }
1412  break;
1413  }
1414  }
1415  if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement)
1416  {
1417  var overrideElement = Params.VariantFile.GetRootExcludingOverride().FromPackage(paramsMainElement.ContentPackage);
1418  // Only override if the override file contains matching elements
1419  if (overrideElement.GetChildElement("inventory") != null)
1420  {
1421  inventoryElements.Clear();
1422  inventoryCommonness.Clear();
1423  foreach (var subElement in overrideElement.GetChildElements("inventory"))
1424  {
1425  switch (subElement.Name.ToString().ToLowerInvariant())
1426  {
1427  case "inventory":
1428  inventoryElements.Add(subElement);
1429  inventoryCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
1430  break;
1431  }
1432  }
1433  }
1434  if (overrideElement.GetChildElement("health") != null)
1435  {
1436  healthElements.Clear();
1437  healthCommonness.Clear();
1438  foreach (var subElement in overrideElement.GetChildElements("health"))
1439  {
1440  healthElements.Add(subElement);
1441  healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
1442  }
1443  }
1444  }
1445 
1446  if (inventoryElements.Count > 0)
1447  {
1449  inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random),
1450  this,
1451  spawnInitialItems);
1452  }
1453  if (healthElements.Count == 0)
1454  {
1455  CharacterHealth = new CharacterHealth(this);
1456  }
1457  else
1458  {
1459  var selectedHealthElement = healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random);
1460  // If there's no limb elements defined in the override variant, let's use the limb health definitions of the original file.
1461  var limbHealthElement = selectedHealthElement;
1462  if (Params.VariantFile != null && limbHealthElement.GetChildElement("limb") == null)
1463  {
1464  limbHealthElement = Params.OriginalElement.GetChildElement("health");
1465  }
1466  CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
1467  }
1468 
1469  if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk")
1470  {
1471  Identifier nonHuskedSpeciesName = Identifier.Empty;
1472  AfflictionPrefabHusk matchingAffliction = null;
1473  foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType<AfflictionPrefabHusk>())
1474  {
1475  var nonHuskedName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, huskPrefab);
1476  if (huskPrefab.TargetSpecies.Contains(nonHuskedName))
1477  {
1478  var huskedSpeciesName = AfflictionHusk.GetHuskedSpeciesName(nonHuskedName, huskPrefab);
1479  if (huskedSpeciesName.Equals(speciesName))
1480  {
1481  nonHuskedSpeciesName = nonHuskedName;
1482  matchingAffliction = huskPrefab;
1483  break;
1484  }
1485  }
1486  }
1487  if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty)
1488  {
1489  DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n"
1490  + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"",
1491  contentPackage: Prefab.ContentPackage);
1492  // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
1493  nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
1494  speciesName = nonHuskedSpeciesName;
1495  }
1496  if (ragdollParams == null && prefab.VariantOf == null)
1497  {
1498  Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
1499  ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name, Params, Prefab.ContentPackage) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name, Params, Prefab.ContentPackage);
1500  }
1501  if (Params.HasInfo && info == null)
1502  {
1503  info = new CharacterInfo(nonHuskedSpeciesName);
1504  }
1505  }
1506  else if (Params.HasInfo && info == null)
1507  {
1508  info = new CharacterInfo(speciesName);
1509  }
1510 
1511  if (IsHumanoid)
1512  {
1513  AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams)
1514  {
1515  TargetDir = Direction.Right
1516  };
1517  }
1518  else
1519  {
1520  AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams);
1521  PressureProtection = int.MaxValue;
1522  }
1523 
1524  AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
1525 
1526  AnimController.FindHull(null);
1528 
1529  CharacterList.Add(this);
1530 
1531  //characters start disabled in the multiplayer mode, and are enabled if/when
1532  // - controlled by the player
1533  // - client receives a position update from the server
1534  // - server receives an input message from the client controlling the character
1535  // - if an AICharacter, the server enables it when close enough to any of the players
1536  Enabled = GameMain.NetworkMember == null;
1537 
1538  if (info != null)
1539  {
1540  LoadHeadAttachments();
1541  }
1542  ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
1543  }
1544  partial void InitProjSpecific(ContentXElement mainElement);
1545 
1546  public void ReloadHead(int? headId = null, int hairIndex = -1, int beardIndex = -1, int moustacheIndex = -1, int faceAttachmentIndex = -1)
1547  {
1548  if (Info == null) { return; }
1549  var head = AnimController.GetLimb(LimbType.Head);
1550  if (head == null) { return; }
1551  HashSet<Identifier> tags = Info.Head.Preset.TagSet.ToHashSet();
1552  if (headId.HasValue)
1553  {
1554  tags.RemoveWhere(t => t.StartsWith("variant"));
1555  tags.Add($"variant{headId.Value}".ToIdentifier());
1556  }
1557  var oldHeadInfo = Info.Head;
1558  Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
1559  if (hairIndex == -1)
1560  {
1561  Info.Head.HairIndex = oldHeadInfo.HairIndex;
1562  }
1563  if (beardIndex == -1)
1564  {
1565  Info.Head.BeardIndex = oldHeadInfo.BeardIndex;
1566  }
1567  if (moustacheIndex == -1)
1568  {
1569  Info.Head.MoustacheIndex = oldHeadInfo.MoustacheIndex;
1570  }
1571  if (faceAttachmentIndex == -1)
1572  {
1573  Info.Head.FaceAttachmentIndex = oldHeadInfo.FaceAttachmentIndex;
1574  }
1575  Info.Head.SkinColor = oldHeadInfo.SkinColor;
1576  Info.Head.HairColor = oldHeadInfo.HairColor;
1577  Info.Head.FacialHairColor = oldHeadInfo.FacialHairColor;
1578  Info.CheckColors();
1579 #if CLIENT
1580  head.RecreateSprites();
1581 #endif
1582  LoadHeadAttachments();
1583  }
1584 
1585  public void LoadHeadAttachments()
1586  {
1587  if (Info == null) { return; }
1588  if (AnimController == null) { return; }
1589  var head = AnimController.GetLimb(LimbType.Head);
1590  if (head == null) { return; }
1591  // Note that if there are any other wearables on the head, they are removed here.
1592  head.OtherWearables.ForEach(w => w.Sprite?.Remove());
1593  head.OtherWearables.Clear();
1594 
1595  //if the element has not been set at this point, the character has no hair and the index should be zero (= no hair)
1596  if (info.Head.FaceAttachment == null) { info.Head.FaceAttachmentIndex = 0; }
1597  Info.Head.FaceAttachment?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment)));
1598  if (info.Head.BeardElement == null) { info.Head.BeardIndex = 0; }
1599  Info.Head.BeardElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard)));
1600  if (info.Head.MoustacheElement == null) { info.Head.MoustacheIndex = 0; }
1601  Info.Head.MoustacheElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache)));
1602  if (info.Head.HairElement == null) { info.Head.HairIndex = 0; }
1603  Info.Head.HairElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair)));
1604 
1605 #if CLIENT
1606  if (info.Head?.HairWithHatElement?.GetChildElement("sprite") != null)
1607  {
1608  head.HairWithHatSprite = new WearableSprite(info.Head.HairWithHatElement.GetChildElement("sprite"), WearableType.Hair);
1609  }
1610  head.EnableHuskSprite = Params.Husk;
1611  head.LoadHerpesSprite();
1612  head.UpdateWearableTypesToHide();
1613 #endif
1614  }
1615 
1616  public bool IsKeyHit(InputType inputType)
1617  {
1618 #if SERVER
1619  if (GameMain.Server != null && IsRemotePlayer)
1620  {
1621  switch (inputType)
1622  {
1623  case InputType.Left:
1624  return dequeuedInput.HasFlag(InputNetFlags.Left) && !prevDequeuedInput.HasFlag(InputNetFlags.Left);
1625  case InputType.Right:
1626  return dequeuedInput.HasFlag(InputNetFlags.Right) && !prevDequeuedInput.HasFlag(InputNetFlags.Right);
1627  case InputType.Up:
1628  return dequeuedInput.HasFlag(InputNetFlags.Up) && !prevDequeuedInput.HasFlag(InputNetFlags.Up);
1629  case InputType.Down:
1630  return dequeuedInput.HasFlag(InputNetFlags.Down) && !prevDequeuedInput.HasFlag(InputNetFlags.Down);
1631  case InputType.Run:
1632  return dequeuedInput.HasFlag(InputNetFlags.Run) && prevDequeuedInput.HasFlag(InputNetFlags.Run);
1633  case InputType.Crouch:
1634  return dequeuedInput.HasFlag(InputNetFlags.Crouch) && !prevDequeuedInput.HasFlag(InputNetFlags.Crouch);
1635  case InputType.Select:
1636  return dequeuedInput.HasFlag(InputNetFlags.Select); //TODO: clean up the way this input is registered
1637  case InputType.Deselect:
1638  return dequeuedInput.HasFlag(InputNetFlags.Deselect);
1639  case InputType.Health:
1640  return dequeuedInput.HasFlag(InputNetFlags.Health);
1641  case InputType.Grab:
1642  return dequeuedInput.HasFlag(InputNetFlags.Grab);
1643  case InputType.Use:
1644  return dequeuedInput.HasFlag(InputNetFlags.Use) && !prevDequeuedInput.HasFlag(InputNetFlags.Use);
1645  case InputType.Shoot:
1646  return dequeuedInput.HasFlag(InputNetFlags.Shoot) && !prevDequeuedInput.HasFlag(InputNetFlags.Shoot);
1647  case InputType.Ragdoll:
1648  return dequeuedInput.HasFlag(InputNetFlags.Ragdoll) && !prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll);
1649  default:
1650  return false;
1651  }
1652  }
1653 #endif
1654 
1655  return keys[(int)inputType].Hit;
1656  }
1657 
1658  public bool IsKeyDown(InputType inputType)
1659  {
1660 #if SERVER
1661  if (GameMain.Server != null && IsRemotePlayer)
1662  {
1663  switch (inputType)
1664  {
1665  case InputType.Left:
1666  return dequeuedInput.HasFlag(InputNetFlags.Left);
1667  case InputType.Right:
1668  return dequeuedInput.HasFlag(InputNetFlags.Right);
1669  case InputType.Up:
1670  return dequeuedInput.HasFlag(InputNetFlags.Up);
1671  case InputType.Down:
1672  return dequeuedInput.HasFlag(InputNetFlags.Down);
1673  case InputType.Run:
1674  return dequeuedInput.HasFlag(InputNetFlags.Run);
1675  case InputType.Crouch:
1676  return dequeuedInput.HasFlag(InputNetFlags.Crouch);
1677  case InputType.Select:
1678  return false; //TODO: clean up the way this input is registered
1679  case InputType.Deselect:
1680  return false;
1681  case InputType.Aim:
1682  return dequeuedInput.HasFlag(InputNetFlags.Aim);
1683  case InputType.Use:
1684  return dequeuedInput.HasFlag(InputNetFlags.Use);
1685  case InputType.Shoot:
1686  return dequeuedInput.HasFlag(InputNetFlags.Shoot);
1687  case InputType.Attack:
1688  return dequeuedInput.HasFlag(InputNetFlags.Attack);
1689  case InputType.Ragdoll:
1690  return dequeuedInput.HasFlag(InputNetFlags.Ragdoll);
1691  }
1692  return false;
1693  }
1694 #endif
1695  if (inputType == InputType.Up || inputType == InputType.Down ||
1696  inputType == InputType.Left || inputType == InputType.Right)
1697  {
1698  var invertControls = CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier());
1699  if (invertControls != null)
1700  {
1701  switch (inputType)
1702  {
1703  case InputType.Left:
1704  inputType = InputType.Right;
1705  break;
1706  case InputType.Right:
1707  inputType = InputType.Left;
1708  break;
1709  case InputType.Up:
1710  inputType = InputType.Down;
1711  break;
1712  case InputType.Down:
1713  inputType = InputType.Up;
1714  break;
1715  }
1716  }
1717  }
1718 
1719  return keys[(int)inputType].Held;
1720  }
1721 
1722  public void SetInput(InputType inputType, bool hit, bool held)
1723  {
1724  keys[(int)inputType].Hit = hit;
1725  keys[(int)inputType].Held = held;
1726  keys[(int)inputType].SetState(hit, held);
1727  }
1728 
1729  public void ClearInput(InputType inputType)
1730  {
1731  keys[(int)inputType].Hit = false;
1732  keys[(int)inputType].Held = false;
1733  }
1734 
1735  public void ClearInputs()
1736  {
1737  if (keys == null) return;
1738  foreach (Key key in keys)
1739  {
1740  key.Hit = false;
1741  key.Held = false;
1742  }
1743  }
1744 
1745  public override string ToString()
1746  {
1747 #if DEBUG
1748  return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName.Value;
1749 #else
1750  return SpeciesName.Value;
1751 #endif
1752  }
1753 
1754  public void GiveJobItems(WayPoint spawnPoint = null)
1755  {
1756  if (info == null) { return; }
1757  if (info.HumanPrefabIds != default)
1758  {
1759  var humanPrefab = NPCSet.Get(info.HumanPrefabIds.NpcSetIdentifier, info.HumanPrefabIds.NpcIdentifier);
1760  if (humanPrefab == null)
1761  {
1762  DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
1763  }
1764  else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
1765  {
1766  return;
1767  }
1768  }
1769  info.Job?.GiveJobItems(this, spawnPoint);
1770  GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint);
1771  }
1772 
1773  public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
1774  {
1775  if (info?.Job == null || spawnPoint == null) { return; }
1776 
1777  foreach (Item item in Inventory.AllItems)
1778  {
1779  var idCard = item?.GetComponent<IdCard>();
1780  if (idCard == null) { continue; }
1781  //if the card belongs to someone else, don't add any tags.
1782  //otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round
1783  if (idCard.OwnerName != info.Name) { continue; }
1784  foreach (string s in spawnPoint.IdCardTags)
1785  {
1786  item.AddTag(s);
1787  }
1788  if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
1789  {
1790  GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
1791  }
1792  }
1793  }
1794 
1795  public float GetSkillLevel(string skillIdentifier) =>
1796  GetSkillLevel(skillIdentifier.ToIdentifier());
1797 
1798  private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
1799  {
1800  { new("helm"), StatTypes.HelmSkillOverride },
1801  { new("medical"), StatTypes.MedicalSkillOverride },
1802  { new("weapons"), StatTypes.WeaponsSkillOverride },
1803  { new("electrical"), StatTypes.ElectricalSkillOverride },
1804  { new("mechanical"), StatTypes.MechanicalSkillOverride }
1805  }.ToImmutableDictionary();
1806 
1807  public float GetSkillLevel(Identifier skillIdentifier)
1808  {
1809  if (Info?.Job == null) { return 0.0f; }
1810  float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
1811 
1812  if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
1813  {
1814  float skillOverride = GetStatValue(statType);
1815  if (skillOverride > skillLevel)
1816  {
1817  skillLevel = skillOverride;
1818  }
1819  }
1820 
1821  // apply multipliers first so that multipliers only affect base skill value
1822  foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
1823  {
1824  skillLevel *= affliction.GetSkillMultiplier();
1825  }
1826 
1827  if (skillIdentifier != null)
1828  {
1829  foreach (Item item in Inventory.AllItems)
1830  {
1831  if (item?.GetComponent<Wearable>() is Wearable wearable &&
1832  !Inventory.IsInLimbSlot(item, InvSlotType.Any))
1833  {
1834  foreach (var allowedSlot in wearable.AllowedSlots)
1835  {
1836  if (allowedSlot == InvSlotType.Any) { continue; }
1837  if (!Inventory.IsInLimbSlot(item, allowedSlot)) { continue; }
1838  if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
1839  {
1840  skillLevel += skillValue;
1841  break;
1842  }
1843  }
1844 
1845  }
1846  }
1847  }
1848 
1849  skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
1850  return Math.Max(skillLevel, 0);
1851  }
1852 
1853  // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
1854  public Vector2? OverrideMovement { get; set; }
1855  public bool ForceRun { get; set; }
1856 
1857  public bool IsClimbing => AnimController.IsClimbing;
1858 
1859  public Vector2 GetTargetMovement()
1860  {
1861  Vector2 targetMovement = Vector2.Zero;
1862  if (OverrideMovement.HasValue)
1863  {
1864  targetMovement = OverrideMovement.Value;
1865  }
1866  else
1867  {
1868  if (IsKeyDown(InputType.Left)) { targetMovement.X -= 1.0f; }
1869  if (IsKeyDown(InputType.Right)) { targetMovement.X += 1.0f; }
1870  if (IsKeyDown(InputType.Up)) { targetMovement.Y += 1.0f; }
1871  if (IsKeyDown(InputType.Down)) { targetMovement.Y -= 1.0f; }
1872  }
1873  bool run = false;
1874  if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun)
1875  {
1876 
1877  run = CanRun;
1878  }
1879  return ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run));
1880  }
1881 
1882  //can't run if
1883  // - dragging someone
1884  // - crouching
1885  // - moving backwards
1886  public bool CanRun => CanRunWhileDragging() &&
1887  AnimController is not HumanoidAnimController { Crouching: true } &&
1888  !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) &&
1890 
1891  public bool CanRunWhileDragging()
1892  {
1893  if (selectedCharacter is not { IsDraggable: true }) { return true; }
1894  //if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted)
1895  if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; }
1896  return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging);
1897  }
1898 
1899  public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
1900  {
1901  //the vertical component is only used for falling through platforms and climbing ladders when not in water,
1902  //so the movement can't be normalized or the Character would walk slower when pressing down/up
1903  if (AnimController.InWater)
1904  {
1905  float length = targetMovement.Length();
1906  if (length > 0.0f)
1907  {
1908  targetMovement /= length;
1909  }
1910  }
1911  targetMovement *= currentSpeed;
1912  float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
1913  targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
1914  targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
1915  SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier));
1916  targetMovement *= SpeedMultiplier;
1917  // Reset, status effects will set the value before the next update
1918  ResetSpeedMultiplier();
1919  return targetMovement;
1920  }
1921 
1922  private float greatestNegativeSpeedMultiplier = 1f;
1923  private float greatestPositiveSpeedMultiplier = 1f;
1924 
1928  public float SpeedMultiplier { get; private set; } = 1;
1929 
1930 
1931  private double propulsionSpeedMultiplierLastSet;
1932  private float propulsionSpeedMultiplier;
1936  public float PropulsionSpeedMultiplier
1937  {
1938  get { return propulsionSpeedMultiplier; }
1939  set
1940  {
1941  propulsionSpeedMultiplier = value;
1942  propulsionSpeedMultiplierLastSet = Timing.TotalTime;
1943  }
1944  }
1945 
1946  public void StackSpeedMultiplier(float val)
1947  {
1948  greatestNegativeSpeedMultiplier = Math.Min(val, greatestNegativeSpeedMultiplier);
1949  greatestPositiveSpeedMultiplier = Math.Max(val, greatestPositiveSpeedMultiplier);
1950  }
1951 
1952  public void ResetSpeedMultiplier()
1953  {
1954  greatestPositiveSpeedMultiplier = 1f;
1955  greatestNegativeSpeedMultiplier = 1f;
1956  if (Timing.TotalTime > propulsionSpeedMultiplierLastSet + 0.1)
1957  {
1958  propulsionSpeedMultiplier = 1.0f;
1959  }
1960  }
1961 
1962  private float greatestNegativeHealthMultiplier = 1f;
1963  private float greatestPositiveHealthMultiplier = 1f;
1964 
1968  public float HealthMultiplier { get; private set; } = 1;
1969 
1970  public void StackHealthMultiplier(float val)
1971  {
1972  greatestNegativeHealthMultiplier = Math.Min(val, greatestNegativeHealthMultiplier);
1973  greatestPositiveHealthMultiplier = Math.Max(val, greatestPositiveHealthMultiplier);
1974  }
1975 
1976  private void CalculateHealthMultiplier()
1977  {
1978  HealthMultiplier = greatestPositiveHealthMultiplier - (1f - greatestNegativeHealthMultiplier);
1979  // Reset, status effects should set the values again, if the conditions match
1980  greatestPositiveHealthMultiplier = 1f;
1981  greatestNegativeHealthMultiplier = 1f;
1982  }
1983 
1987  public float HumanPrefabHealthMultiplier { get; private set; } = 1;
1988 
1993  {
1994  float reduction = 0;
1995  reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
1996  reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
1998  {
1999  if (AnimController.InWater)
2000  {
2001  // Currently only humans use hands for swimming.
2002  reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
2003  reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
2004  }
2005  }
2006  else
2007  {
2008  int totalTailLimbs = 0;
2009  int destroyedTailLimbs = 0;
2010  foreach (var limb in AnimController.Limbs)
2011  {
2012  if (limb.type == LimbType.Tail)
2013  {
2014  totalTailLimbs++;
2015  if (limb.IsSevered)
2016  {
2017  destroyedTailLimbs++;
2018  }
2019  }
2020  }
2021  if (destroyedTailLimbs > 0)
2022  {
2023  reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
2024  }
2025  }
2026  return Math.Clamp(reduction, 0, 1f);
2027  }
2028 
2029  private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f)
2030  {
2031  if (limb != null)
2032  {
2033  sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType));
2034  }
2035  return Math.Clamp(sum, 0, 1f);
2036  }
2037 
2038  public float GetRightHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), 0, max: 1);
2039  public float GetLeftHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), 0, max: 1);
2040 
2041  public float GetLegPenalty(float startSum = 0)
2042  {
2043  float sum = startSum;
2044  foreach (var limb in AnimController.Limbs)
2045  {
2046  switch (limb.type)
2047  {
2048  case LimbType.RightFoot:
2049  case LimbType.LeftFoot:
2050  sum += CalculateMovementPenalty(limb, sum, max: 0.5f);
2051  break;
2052  }
2053  }
2054  return Math.Clamp(sum, 0, 1f);
2055  }
2056 
2057  public float ApplyTemporarySpeedLimits(float speed)
2058  {
2059  float max;
2061  {
2062  max = AnimController.InWater ? 0.5f : 0.8f;
2063  }
2064  else
2065  {
2066  max = AnimController.InWater ? 0.9f : 0.5f;
2067  }
2068  speed *= 1f - MathHelper.Lerp(0, max, GetTemporarySpeedReduction());
2069  return speed;
2070  }
2071 
2075  private const float cursorFollowMargin = 40;
2076 
2077  public void Control(float deltaTime, Camera cam)
2078  {
2079  ViewTarget = null;
2080  if (!AllowInput) { return; }
2081 
2082  if (Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer))
2083  {
2084  SmoothedCursorPosition = cursorPosition;
2085  }
2086  else
2087  {
2088  //apply some smoothing to the cursor positions of remote players when playing as a client
2089  //to make aiming look a little less choppy
2090  Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition;
2091  smoothedCursorDiff = NetConfig.InterpolateCursorPositionError(smoothedCursorDiff);
2092  SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
2093  }
2094 
2095  bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled;
2096  if (!aiControlled)
2097  {
2098  Vector2 targetMovement = GetTargetMovement();
2099  AnimController.TargetMovement = targetMovement;
2101  }
2102 
2103  if (AnimController is HumanoidAnimController humanAnimController)
2104  {
2105  humanAnimController.Crouching =
2106  humanAnimController.ForceSelectAnimationType == AnimationType.Crouch ||
2107  IsKeyDown(InputType.Crouch);
2108  if (Screen.Selected is not { IsEditor: true })
2109  {
2110  humanAnimController.ForceSelectAnimationType = AnimationType.NotDefined;
2111  }
2112  }
2113 
2114  if (!aiControlled &&
2117  (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this) &&
2118  ((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) &&
2120  {
2121  if (dontFollowCursor)
2122  {
2124  }
2125  else
2126  {
2127  if (CursorPosition.X < AnimController.Collider.Position.X - cursorFollowMargin)
2128  {
2130  }
2131  else if (CursorPosition.X > AnimController.Collider.Position.X + cursorFollowMargin)
2132  {
2134  }
2135  }
2136  }
2137 
2138  if (GameMain.NetworkMember != null)
2139  {
2140  if (GameMain.NetworkMember.IsServer)
2141  {
2142  if (!aiControlled)
2143  {
2144  if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
2145  {
2147  }
2148  else
2149  {
2151  }
2152  }
2153  }
2154  else if (GameMain.NetworkMember.IsClient && Controlled != this)
2155  {
2156  if (memState.Count > 0)
2157  {
2158  AnimController.TargetDir = memState[0].Direction;
2159  }
2160  }
2161  }
2162 
2163 #if DEBUG && CLIENT
2164  if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.F))
2165  {
2167  if (AIController != null && AIController is EnemyAIController enemyAI)
2168  {
2169  enemyAI.LatchOntoAI?.DeattachFromBody(reset: true);
2170  }
2171  }
2172 #endif
2173 
2174  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim))
2175  {
2176  if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi)
2177  {
2178  enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity);
2179  }
2180  }
2181 
2182  if (attackCoolDown > 0.0f)
2183  {
2184  attackCoolDown -= deltaTime;
2185  }
2186  else if (IsKeyDown(InputType.Attack))
2187  {
2188  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this)
2189  {
2190  if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
2191  {
2192  currentAttackTarget = default;
2193  }
2194  currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
2195  }
2196  else if (IsPlayer)
2197  {
2198  float dist = -1;
2199  Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
2200  List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
2201  ignoredBodies.Add(AnimController.Collider.FarseerBody);
2202 
2203  var body = Submarine.PickBody(
2204  SimPosition,
2205  attackPos,
2206  ignoredBodies,
2207  Physics.CollisionCharacter | Physics.CollisionWall);
2208 
2209  IDamageable attackTarget = null;
2210  if (body != null)
2211  {
2212  attackPos = Submarine.LastPickedPosition;
2213 
2214  if (body.UserData is Submarine sub)
2215  {
2216  body = Submarine.PickBody(
2217  SimPosition - ((Submarine)body.UserData).SimPosition,
2218  attackPos - ((Submarine)body.UserData).SimPosition,
2219  ignoredBodies,
2220  Physics.CollisionWall);
2221 
2222  if (body != null)
2223  {
2224  attackPos = Submarine.LastPickedPosition + sub.SimPosition;
2225  attackTarget = body.UserData as IDamageable;
2226  }
2227  }
2228  else
2229  {
2230  if (body.UserData is IDamageable damageable)
2231  {
2232  attackTarget = damageable;
2233  }
2234  else if (body.UserData is Limb limb)
2235  {
2236  attackTarget = limb.character;
2237  }
2238  }
2239  }
2240  var currentContexts = GetAttackContexts();
2241  var validLimbs = AnimController.Limbs.Where(l =>
2242  {
2243  if (l.IsSevered || l.IsStuck) { return false; }
2244  if (l.Disabled) { return false; }
2245  var attack = l.attack;
2246  if (attack == null) { return false; }
2247  if (attack.CoolDownTimer > 0) { return false; }
2248  if (!attack.IsValidContext(currentContexts)) { return false; }
2249  if (attackTarget != null)
2250  {
2251  if (!attack.IsValidTarget(attackTarget as Entity)) { return false; }
2252  if (attackTarget is ISerializableEntity se && attackTarget is Character)
2253  {
2254  if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
2255  }
2256  }
2257  if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
2258  return true;
2259  });
2260  var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
2261  // Select closest
2262  var attackLimb = sortedLimbs.FirstOrDefault();
2263  if (attackLimb != null)
2264  {
2265  if (attackTarget is Character targetCharacter)
2266  {
2267  dist = ConvertUnits.ToDisplayUnits(Vector2.Distance(Submarine.LastPickedPosition, attackLimb.SimPosition));
2268  foreach (Limb limb in targetCharacter.AnimController.Limbs)
2269  {
2270  if (limb.IsSevered || limb.Removed) { continue; }
2271  float tempDist = ConvertUnits.ToDisplayUnits(Vector2.Distance(limb.SimPosition, attackLimb.SimPosition));
2272  if (tempDist < dist)
2273  {
2274  dist = tempDist;
2275  }
2276  }
2277  }
2278  attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult, dist);
2279  if (!attackLimb.attack.IsRunning)
2280  {
2281  attackCoolDown = 1.0f;
2282  }
2283  }
2284  }
2285  }
2286 
2287  if (Inventory != null)
2288  {
2289  if (IsKeyHit(InputType.DropItem))
2290  {
2291  foreach (Item item in HeldItems)
2292  {
2293  if (!CanInteractWith(item)) { continue; }
2294 
2295  if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item))
2296  {
2297  SelectedItem.OwnInventory.TryPutItem(item, this);
2298  }
2299  else
2300  {
2301  item.Drop(this);
2302  }
2303  //only drop one held item per key hit
2304  break;
2305  }
2306  }
2307 
2308  bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
2309  if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
2310  {
2311  foreach (Item item in HeldItems)
2312  {
2313  tryUseItem(item, deltaTime);
2314  }
2315  foreach (Item item in Inventory.AllItems)
2316  {
2317  if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
2318  {
2319  tryUseItem(item, deltaTime);
2320  }
2321  }
2322  }
2323  }
2324 
2325  void tryUseItem(Item item, float deltaTime)
2326  {
2327  if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
2328  {
2329  item.SecondaryUse(deltaTime, this);
2330  }
2331  if (IsKeyDown(InputType.Use) && !item.IsShootable)
2332  {
2333  if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
2334  {
2335  item.Use(deltaTime, user: this);
2336  }
2337  }
2338  if (IsKeyDown(InputType.Shoot) && item.IsShootable)
2339  {
2340  if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
2341  {
2342  item.Use(deltaTime, user: this);
2343  }
2344 #if CLIENT
2345  else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
2346  {
2347  HintManager.OnShootWithoutAiming(this, item);
2348  }
2349 #endif
2350  }
2351  }
2352 
2353  if (SelectedItem != null)
2354  {
2355  tryUseItem(SelectedItem, deltaTime);
2356  }
2357 
2358  if (SelectedCharacter != null)
2359  {
2360  if (!SelectedCharacter.CanBeSelected ||
2361  (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance &&
2362  SelectedCharacter.GetDistanceToClosestLimb(GetRelativeSimPosition(selectedCharacter, WorldPosition)) > ConvertUnits.ToSimUnits(MaxDragDistance)))
2363  {
2364  DeselectCharacter();
2365  }
2366  }
2367 
2368  if (IsRemotelyControlled && keys != null)
2369  {
2370  foreach (Key key in keys)
2371  {
2372  key.ResetHit();
2373  }
2374  }
2375  }
2376 
2377  private struct AttackTargetData
2378  {
2379  public Limb AttackLimb { get; set; }
2380  public IDamageable DamageTarget { get; set; }
2381  public Vector2 AttackPos { get; set; }
2382  }
2383 
2384  private AttackTargetData currentAttackTarget;
2385  public void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos)
2386  {
2387  currentAttackTarget = new AttackTargetData()
2388  {
2389  AttackLimb = attackLimb,
2390  DamageTarget = damageTarget,
2391  AttackPos = attackPos
2392  };
2393  }
2394 
2395  private Limb GetSeeingLimb()
2396  {
2398  }
2399 
2400  public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false)
2401  {
2402  seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
2403  if (target is Character targetCharacter)
2404  {
2405  return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
2406  }
2407  else
2408  {
2409  return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
2410  }
2411  }
2412 
2413  public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
2414  {
2415  if (seeingEntity is Character seeingCharacter)
2416  {
2417  return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
2418  }
2419  if (target is Character targetCharacter)
2420  {
2421  return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
2422  }
2423  else
2424  {
2425  return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
2426  }
2427  }
2428 
2429  private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
2430  {
2431  System.Diagnostics.Debug.Assert(target != null);
2432  if (target == null || target.Removed) { return false; }
2433  if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
2435  {
2436  //find the limbs that are furthest from the target's position (from the viewer's point of view)
2437  Limb leftExtremity = null, rightExtremity = null;
2438  float leftMostDot = 0.0f, rightMostDot = 0.0f;
2439  Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
2440  Vector2 leftDir = new Vector2(dir.Y, -dir.X);
2441  Vector2 rightDir = new Vector2(-dir.Y, dir.X);
2442  foreach (Limb limb in target.AnimController.Limbs)
2443  {
2444  if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
2445  if (limb.Hidden) { continue; }
2446  Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
2447  float leftDot = Vector2.Dot(limbDir, leftDir);
2448  if (leftDot > leftMostDot)
2449  {
2450  leftMostDot = leftDot;
2451  leftExtremity = limb;
2452  continue;
2453  }
2454  float rightDot = Vector2.Dot(limbDir, rightDir);
2455  if (rightDot > rightMostDot)
2456  {
2457  rightMostDot = rightDot;
2458  rightExtremity = limb;
2459  continue;
2460  }
2461  }
2462  if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
2463  if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
2464  }
2465  return false;
2466  }
2467 
2468  private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
2469  {
2470  System.Diagnostics.Debug.Assert(target != null);
2471  if (target == null) { return false; }
2472  if (seeingEntity == null) { return false; }
2473  // TODO: Could we just use the method below? If not, let's refactor it so that we can.
2474  Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
2475  if (checkFacing && seeingEntity is Character seeingCharacter)
2476  {
2477  if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
2478  }
2479  //both inside the same sub (or both outside)
2480  //OR the we're inside, the other character outside
2481  if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
2482  {
2483  return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
2484  }
2485  //we're outside, the other character inside
2486  else if (seeingEntity.Submarine == null)
2487  {
2488  return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
2489  }
2490  //both inside different subs
2491  else
2492  {
2493  return
2494  Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
2495  Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
2496  }
2497 
2498  bool IsBlocking(Fixture f)
2499  {
2500  var body = f.Body;
2501  if (body == null) { return false; }
2502  if (body.UserData is Structure wall)
2503  {
2504  if (!wall.CastShadow && seeThroughWindows) { return false; }
2505  return wall != target;
2506  }
2507  else if (body.UserData is Item item)
2508  {
2509  if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
2510  {
2511  if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
2512  }
2513 
2514  return item != target;
2515  }
2516  return true;
2517  }
2518  }
2519 
2523  public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X;
2524 
2525  public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item, slotType) : item.IsOwnedBy(this);
2526 
2527  public bool HasEquippedItem(Item item, InvSlotType? slotType = null, Func<InvSlotType, bool> predicate = null)
2528  {
2529  if (Inventory == null) { return false; }
2530  for (int i = 0; i < Inventory.Capacity; i++)
2531  {
2532  InvSlotType slot = Inventory.SlotTypes[i];
2533  if (predicate != null)
2534  {
2535  if (!predicate(slot)) { continue; }
2536  }
2537  if (slotType.HasValue)
2538  {
2539  if (!slotType.Value.HasFlag(slot)) { continue; }
2540  }
2541  else if (slot == InvSlotType.Any)
2542  {
2543  continue;
2544  }
2545  if (Inventory.GetItemAt(i) == item) { return true; }
2546  }
2547  return false;
2548  }
2549 
2550  public bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null)
2551  {
2552  if (Inventory == null) { return false; }
2553  for (int i = 0; i < Inventory.Capacity; i++)
2554  {
2555  if (slotType.HasValue)
2556  {
2557  if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
2558  }
2559  else if (Inventory.SlotTypes[i] == InvSlotType.Any)
2560  {
2561  continue;
2562  }
2563  var item = Inventory.GetItemAt(i);
2564  if (item == null) { continue; }
2565  if (!allowBroken && item.Condition <= 0.0f) { continue; }
2566  if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return true; }
2567  }
2568  return false;
2569  }
2570 
2571  public Item GetEquippedItem(Identifier tagOrIdentifier = default, InvSlotType? slotType = null)
2572  {
2573  if (Inventory == null) { return null; }
2574  for (int i = 0; i < Inventory.Capacity; i++)
2575  {
2576  if (slotType.HasValue)
2577  {
2578  if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
2579  }
2580  else if (Inventory.SlotTypes[i] == InvSlotType.Any)
2581  {
2582  continue;
2583  }
2584  var item = Inventory.GetItemAt(i);
2585  if (item == null) { continue; }
2586  if (tagOrIdentifier.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier))
2587  {
2588  return item;
2589  }
2590  }
2591  return null;
2592  }
2593 
2595  {
2596  if (!CanInteract || inventory.Locked) { return false; }
2597 
2598  if (inventory.Owner is Character inventoryOwner)
2599  {
2600  return inventoryOwner.IsInventoryAccessibleTo(this, accessLevel) && (inventoryOwner == this || CanInteractWith(inventoryOwner));
2601  }
2602 
2603  if (inventory.Owner is Item item)
2604  {
2605  if (!CanInteractWith(item))
2606  {
2607  //could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this
2608  foreach (var linkedEntity in item.linkedTo)
2609  {
2610  if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; }
2611  }
2612  return false;
2613  }
2614  ItemContainer container = (inventory as ItemInventory)?.Container;
2615  if (container != null)
2616  {
2617  if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
2618  if (!container.AllowAccess) { return false; }
2619  }
2620  }
2621  return true;
2622  }
2623 
2624  public bool CanBeHealedBy(Character character, bool checkFriendlyTeam = true) =>
2625  !character.IsClimbing && !DisableHealthWindow &&
2626  UseHealthWindow && character.CanInteract &&
2627  (!checkFriendlyTeam || IsFriendly(character) || CanBeDraggedBy(character)) &&
2628  character.CanInteractWith(this, 160f, false);
2629 
2630  public bool CanBeDraggedBy(Character character)
2631  {
2632  if (!IsDraggable) { return false; }
2633  return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID);
2634  }
2635 
2640  {
2641  if (Removed || Inventory == null) { return false; }
2642  if (!Inventory.AccessibleWhenAlive && !IsDead)
2643  {
2644  if (character == this)
2645  {
2646  return Inventory.AccessibleByOwner;
2647  }
2648  return false;
2649  }
2650  if (character == this) { return true; }
2651  if (IsKnockedDown || LockHands) { return true; }
2652  return accessLevel switch
2653  {
2654  CharacterInventory.AccessLevel.Restricted => false,
2655  CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
2656  CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(),
2657  _ => throw new NotImplementedException()
2658  };
2659 
2660  bool IsOnSameTeam() => character.TeamID == teamID;
2661  bool IsFriendlyPet() => IsPet && character.IsFriendly(this);
2662  }
2663 
2664  private Stopwatch sw;
2665  private Stopwatch StopWatch => sw ??= new Stopwatch();
2666  private float _selectedItemPriority;
2667  private Item _foundItem;
2675  public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable<Identifier> identifiers = null, bool ignoreBroken = true,
2676  IEnumerable<Item> ignoredItems = null, IEnumerable<Identifier> ignoredContainerIdentifiers = null,
2677  Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null)
2678  {
2680  {
2681  StopWatch.Restart();
2682  }
2683  if (itemIndex == 0)
2684  {
2685  _foundItem = null;
2686  _selectedItemPriority = 0;
2687  }
2688  int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
2689  int checkedItemCount = 0;
2690  for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
2691  {
2692  checkedItemCount++;
2693  var item = Item.ItemList[itemIndex];
2694  if (!item.IsInteractable(this)) { continue; }
2695  if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
2696  if (item.Submarine == null) { continue; }
2697  if (item.Submarine.TeamID != TeamID) { continue; }
2698  if (item.CurrentHull == null) { continue; }
2699  if (ignoreBroken && item.Condition <= 0) { continue; }
2700  if (Submarine != null)
2701  {
2702  if (!Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
2703  }
2704  if (customPredicate != null && !customPredicate(item)) { continue; }
2705  if (identifiers != null && identifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
2706  if (ignoredContainerIdentifiers != null && item.Container != null)
2707  {
2708  if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
2709  }
2710  if (IsItemTakenBySomeoneElse(item)) { continue; }
2711  Entity rootInventoryOwner = item.GetRootInventoryOwner();
2712  if (rootInventoryOwner is Item ownerItem)
2713  {
2714  if (!ownerItem.IsInteractable(this)) { continue; }
2715  }
2716  float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1;
2717  if (itemPriority <= 0) { continue; }
2718  Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
2719  Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition;
2720  float distanceFactor = AIObjective.GetDistanceFactor(refPos, itemPos, verticalDistanceMultiplier: 5, maxDistance: maxItemDistance, factorAtMaxDistance: 0);
2721  itemPriority *= distanceFactor;
2722  if (itemPriority > _selectedItemPriority)
2723  {
2724  _selectedItemPriority = itemPriority;
2725  _foundItem = item;
2726  }
2727  }
2728  targetItem = _foundItem;
2729  bool completed = itemIndex >= Item.ItemList.Count - 1;
2730  if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
2731  {
2732  var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
2733  if (StopWatch.ElapsedMilliseconds > 5)
2734  {
2735  DebugConsole.ThrowError(msg);
2736  }
2737  else
2738  {
2739  // An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
2740  DebugConsole.AddWarning(msg);
2741  }
2742  }
2743  return completed;
2744  }
2745 
2746  public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
2747 
2748  public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
2749  {
2750  if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; }
2751  if (!c.CharacterHealth.UseHealthWindow && !c.IsDraggable && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
2752 
2753  if (!skipDistanceCheck)
2754  {
2755  maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent());
2756  if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist &&
2757  Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist)
2758  {
2759  return false;
2760  }
2761  }
2762 
2763  return !checkVisibility || CanSeeTarget(c);
2764  }
2765 
2766  public bool CanInteractWith(Item item, bool checkLinked = true)
2767  {
2768  return CanInteractWith(item, out _, checkLinked);
2769  }
2770 
2771  public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinked)
2772  {
2773  distanceToItem = -1.0f;
2774 
2775  bool hidden = item.IsHidden;
2776 #if CLIENT
2777  if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
2778 #endif
2779  if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
2780 
2781  if (item.ParentInventory != null)
2782  {
2783  return CanAccessInventory(item.ParentInventory);
2784  }
2785 
2786  Wire wire = item.GetComponent<Wire>();
2787  if (wire != null && item.GetComponent<ConnectionPanel>() == null)
2788  {
2789  //locked wires are never interactable
2790  if (wire.Locked) { return false; }
2791  if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { return false; }
2792 
2793  //wires are interactable if the character has selected an item the wire is connected to,
2794  //and it's disconnected from the other end
2795  if (wire.Connections[0]?.Item != null && SelectedItem == wire.Connections[0].Item)
2796  {
2797  return wire.Connections[1] == null;
2798  }
2799  if (wire.Connections[1]?.Item != null && SelectedItem == wire.Connections[1].Item)
2800  {
2801  return wire.Connections[0] == null;
2802  }
2803  if (SelectedItem?.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false)
2804  {
2805  return wire.Connections[0] == null && wire.Connections[1] == null;
2806  }
2807  }
2808 
2809  if (checkLinked && item.DisplaySideBySideWhenLinked)
2810  {
2811  foreach (MapEntity linked in item.linkedTo)
2812  {
2813  if (linked is Item linkedItem &&
2814  //if the linked item is inside this container (a modder or sub builder doing smth really weird?)
2815  //don't check it here because it'd lead to an infinite loop
2816  linkedItem.ParentInventory?.Owner != item)
2817  {
2818  if (CanInteractWith(linkedItem, out float distToLinked, checkLinked: false))
2819  {
2820  distanceToItem = distToLinked;
2821  return true;
2822  }
2823  }
2824  }
2825  }
2826 
2827  if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; }
2828 
2829  Pickable pickableComponent = item.GetComponent<Pickable>();
2830  if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; }
2831 
2832  if (SelectedItem?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
2833  //optimization: don't use HeldItems because it allocates memory and this method is executed very frequently
2834  var heldItem1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
2835  if (heldItem1?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
2836  var heldItem2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
2837  if (heldItem2?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
2838 
2839  Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation));
2840 
2841  Vector2 upperBodyPosition = Position + (characterDirection * 20.0f);
2842  Vector2 lowerBodyPosition = Position - (characterDirection * 60.0f);
2843 
2844  if (Submarine != null)
2845  {
2846  upperBodyPosition += Submarine.Position;
2847  lowerBodyPosition += Submarine.Position;
2848  }
2849 
2850  bool insideTrigger = item.IsInsideTrigger(upperBodyPosition) || item.IsInsideTrigger(lowerBodyPosition);
2851  if (item.Prefab.Triggers.Length > 0 && !insideTrigger && item.Prefab.RequireBodyInsideTrigger) { return false; }
2852 
2853  Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
2854 
2855  // Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect
2856  Vector2 playerDistanceCheckPosition =
2857  lowerBodyPosition.Y < upperBodyPosition.Y ?
2858  Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition) :
2859  Vector2.Clamp(itemDisplayRect.Center.ToVector2(), upperBodyPosition, lowerBodyPosition);
2860 
2861  // If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item
2862  if (itemDisplayRect.Contains(playerDistanceCheckPosition))
2863  {
2864  distanceToItem = 0.0f;
2865  }
2866  else
2867  {
2868  // Here we get the point on the itemDisplayRect which is closest to playerDistanceCheckPosition
2869  Vector2 rectIntersectionPoint = new Vector2(
2870  MathHelper.Clamp(playerDistanceCheckPosition.X, itemDisplayRect.X, itemDisplayRect.Right),
2871  MathHelper.Clamp(playerDistanceCheckPosition.Y, itemDisplayRect.Y, itemDisplayRect.Bottom));
2872  distanceToItem = Vector2.Distance(rectIntersectionPoint, playerDistanceCheckPosition);
2873  }
2874 
2875  float interactDistance = item.InteractDistance;
2876  if ((SelectedSecondaryItem != null || item.IsSecondaryItem) && AnimController is HumanoidAnimController c)
2877  {
2878  // Use a distance slightly shorter than the arms length to keep the character in a comfortable pose
2879  float armLength = 0.75f * ConvertUnits.ToDisplayUnits(c.ArmLength);
2880  interactDistance = Math.Min(interactDistance, armLength);
2881  }
2882  if (distanceToItem > interactDistance && item.InteractDistance > 0.0f) { return false; }
2883 
2884  Vector2 itemPosition = GetPosition(Submarine, item, item.SimPosition);
2885 
2886  if (SelectedSecondaryItem != null && !item.IsSecondaryItem)
2887  {
2888  //don't allow selecting another Controller if it'd try to turn the character in the opposite direction
2889  //(e.g. periscope that's facing the wrong way while sitting in a chair)
2890  if (item.GetComponent<Controller>() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; }
2891 
2892  //if a Controller that controls the character's pose is selected,
2893  //don't allow selecting items that are behind the character's back
2894  if (SelectedSecondaryItem.GetComponent<Controller>() is { ControlCharacterPose: true } selectedController)
2895  {
2896  float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin);
2897  if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; }
2898  if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; }
2899  }
2900  }
2901 
2902  if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
2903  {
2904  var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
2905  bool itemCenterVisible = CheckBody(body, item);
2906 
2907  if (!itemCenterVisible && item.Prefab.RequireCursorInsideTrigger)
2908  {
2909  foreach (Rectangle trigger in item.Prefab.Triggers)
2910  {
2911  Rectangle transformTrigger = item.TransformTrigger(trigger, world: false);
2912 
2913  RectangleF simRect = new RectangleF(
2914  x: ConvertUnits.ToSimUnits(transformTrigger.X),
2915  y: ConvertUnits.ToSimUnits(transformTrigger.Y - transformTrigger.Height),
2916  width: ConvertUnits.ToSimUnits(transformTrigger.Width),
2917  height: ConvertUnits.ToSimUnits(transformTrigger.Height));
2918 
2919  simRect.Location = GetPosition(Submarine, item, simRect.Location);
2920 
2921  Vector2 closest = ToolBox.GetClosestPointOnRectangle(simRect, SimPosition);
2922  var triggerBody = Submarine.CheckVisibility(SimPosition, closest, ignoreLevel: true);
2923 
2924  if (CheckBody(triggerBody, item)) { return true; }
2925  }
2926  }
2927  else
2928  {
2929  return itemCenterVisible;
2930  }
2931 
2932  }
2933 
2934  return true;
2935 
2936  static bool CheckBody(Body body, Item item)
2937  {
2938  if (body is null) { return true; }
2939  var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
2940  if (otherItem != item &&
2941  (body.UserData as ItemComponent)?.Item != item &&
2942  /*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
2943  otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
2944  Submarine.LastPickedFixture?.UserData as Item != item)
2945  {
2946  return false;
2947  }
2948 
2949  return true;
2950  }
2951 
2952  static Vector2 GetPosition(Submarine submarine, Item item, Vector2 simPosition)
2953  {
2954  Vector2 position = simPosition;
2955 
2956  Vector2 itemSubPos = item.Submarine?.SimPosition ?? Vector2.Zero;
2957  Vector2 subPos = submarine?.SimPosition ?? Vector2.Zero;
2958 
2959  if (submarine == null && item.Submarine != null)
2960  {
2961  //character is outside, item inside
2962  position += itemSubPos;
2963  }
2964  else if (submarine != null && item.Submarine == null)
2965  {
2966  //character is inside, item outside
2967  position -= subPos;
2968  }
2969  else if (submarine != item.Submarine && submarine != null)
2970  {
2971  //character and the item are inside different subs
2972  position += itemSubPos;
2973  position -= subPos;
2974  }
2975 
2976  return position;
2977  }
2978  }
2979 
2985  public void SetCustomInteract(Action<Character, Character> onCustomInteract, LocalizedString hudText)
2986  {
2987  this.onCustomInteract = onCustomInteract;
2988  CustomInteractHUDText = hudText;
2989  }
2990 
2991  public void SelectCharacter(Character character)
2992  {
2993  if (character == null || character == this) { return; }
2994  SelectedCharacter = character;
2995  }
2996 
2997  public void DeselectCharacter()
2998  {
2999  if (SelectedCharacter == null) { return; }
3000  if (!SelectedCharacter.AllowInput)
3001  {
3002  //we cannot reset the pull joints if the target is conscious (moving on its own),
3003  //that'd interfere with its animations
3004  SelectedCharacter.AnimController?.ResetPullJoints();
3005  }
3006  SelectedCharacter = null;
3007  }
3008 
3009  public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
3010  {
3011  bool isLocalPlayer = Controlled == this;
3012 
3013  if (!isLocalPlayer && (this is AICharacter && !IsRemotePlayer))
3014  {
3015  return;
3016  }
3017 
3018  if (DisableInteract)
3019  {
3020  DisableInteract = false;
3021  return;
3022  }
3023 
3024  if (!CanInteract)
3025  {
3026  SelectedItem = SelectedSecondaryItem = null;
3027  focusedItem = null;
3028  if (!AllowInput)
3029  {
3030  FocusedCharacter = null;
3031  if (SelectedCharacter != null) { DeselectCharacter(); }
3032  return;
3033  }
3034  }
3035 
3036 #if CLIENT
3037  if (isLocalPlayer)
3038  {
3039  if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities)
3040  {
3041  if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
3042  {
3044  {
3045  FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
3046  if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; }
3047  float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
3048  if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
3049  {
3050  //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
3051  aimAssist = 0.0f;
3052  }
3053 
3054  UpdateInteractablesInRange();
3055 
3056  if (!ShowInteractionLabels) // show labels handles setting the focused item in CharacterHUD, so we can click on them boxes
3057  {
3058  focusedItem = CanInteract ? FindClosestItem(interactablesInRange, mouseSimPos, aimAssist) : null;
3059  }
3060 
3061  if (focusedItem != null)
3062  {
3063  if (focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None ||
3064  /*pets' "play" interaction can interfere with interacting with items, so let's remove focus from the pet if the cursor is closer to a highlighted item*/
3065  FocusedCharacter is { IsPet: true } && Vector2.DistanceSquared(focusedItem.SimPosition, mouseSimPos) < Vector2.DistanceSquared(FocusedCharacter.SimPosition, mouseSimPos))
3066  {
3067  FocusedCharacter = null;
3068  }
3069  }
3070  findFocusedTimer = 0.05f;
3071  }
3072  else
3073  {
3074  if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; }
3075  if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; }
3076  }
3077  }
3078  }
3079  else
3080  {
3081  FocusedCharacter = null;
3082  focusedItem = null;
3083  }
3084  findFocusedTimer -= deltaTime;
3085  DisableFocusingOnEntities = false;
3086  }
3087 #endif
3088  var head = AnimController.GetLimb(LimbType.Head);
3089  bool headInWater = head == null ?
3091  head.InWater;
3092  //climb ladders automatically when pressing up/down inside their trigger area
3093  Ladder currentLadder = SelectedSecondaryItem?.GetComponent<Ladder>();
3094  if ((SelectedSecondaryItem == null || currentLadder != null) &&
3095  !headInWater && Screen.Selected != GameMain.SubEditorScreen)
3096  {
3097  bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down);
3098  bool isControlled = Controlled == this;
3099 
3100  Ladder nearbyLadder = null;
3101  if (isControlled || climbInput)
3102  {
3103  float minDist = float.PositiveInfinity;
3104  foreach (Ladder ladder in Ladder.List)
3105  {
3106  if (ladder == currentLadder)
3107  {
3108  continue;
3109  }
3110  else if (currentLadder != null)
3111  {
3112  //only switch from ladder to another if the ladders are above the current ladders and pressing up, or vice versa
3113  if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y != IsKeyDown(InputType.Up))
3114  {
3115  continue;
3116  }
3117  }
3118 
3119  if (CanInteractWith(ladder.Item, out float dist, checkLinked: false) && dist < minDist)
3120  {
3121  minDist = dist;
3122  nearbyLadder = ladder;
3123  if (isControlled)
3124  {
3125  ladder.Item.IsHighlighted = true;
3126  }
3127  break;
3128  }
3129  }
3130  }
3131 
3132  if (nearbyLadder != null && climbInput)
3133  {
3134  if (nearbyLadder.Select(this))
3135  {
3136  SelectedSecondaryItem = nearbyLadder.Item;
3137  }
3138  }
3139  }
3140 
3141  bool selectInputSameAsDeselect = false;
3142 #if CLIENT
3143  selectInputSameAsDeselect = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect];
3144 #endif
3145 
3146  if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars
3147  {
3148  DeselectCharacter();
3149  }
3150  else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDraggedBy(this) && (CanInteract || FocusedCharacter.IsDead && CanEat))
3151  {
3152  SelectCharacter(FocusedCharacter);
3153  }
3154  else if (FocusedCharacter is { IsIncapacitated: false } && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
3155  {
3156  (FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
3157  }
3158  else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CanBeHealedBy(this))
3159  {
3160  if (FocusedCharacter == SelectedCharacter)
3161  {
3162  DeselectCharacter();
3163 #if CLIENT
3164  if (Controlled == this)
3165  {
3167  }
3168 #endif
3169  }
3170  else
3171  {
3172  SelectCharacter(FocusedCharacter);
3173 #if CLIENT
3174  if (Controlled == this)
3175  {
3176  HealingCooldown.PutOnCooldown();
3178  }
3179 #elif SERVER
3180  if (GameMain.Server?.ConnectedClients is { } clients)
3181  {
3182  foreach (Client c in clients)
3183  {
3184  if (c.Character != this) { continue; }
3185 
3186  HealingCooldown.SetCooldown(c);
3187  break;
3188  }
3189  }
3190 #endif
3191  }
3192  }
3193  else if (FocusedCharacter != null && IsKeyHit(InputType.Use) && FocusedCharacter.onCustomInteract != null && FocusedCharacter.AllowCustomInteract)
3194  {
3195  FocusedCharacter.onCustomInteract(FocusedCharacter, this);
3196  }
3197  else if (IsKeyHit(InputType.Deselect) && SelectedItem != null &&
3198  (focusedItem == null || focusedItem == SelectedItem || !selectInputSameAsDeselect))
3199  {
3200  SelectedItem = null;
3201 #if CLIENT
3203 #endif
3204  }
3205  else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
3206  (focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
3207  {
3208  ReleaseSecondaryItem();
3209 #if CLIENT
3211 #endif
3212  }
3213  else if (IsKeyHit(InputType.Health) && SelectedItem != null)
3214  {
3215  SelectedItem = null;
3216  }
3217  else if (focusedItem != null)
3218  {
3219 #if CLIENT
3220  if (CharacterInventory.DraggingItemToWorld) { return; }
3221  if (selectInputSameAsDeselect)
3222  {
3223  keys[(int)InputType.Deselect].Reset();
3224  }
3225 #endif
3226  bool canInteract = focusedItem.TryInteract(this);
3227 #if CLIENT
3228  if (Controlled == this)
3229  {
3230  focusedItem.IsHighlighted = true;
3231  if (canInteract)
3232  {
3234  }
3235  }
3236 #endif
3237  }
3238  }
3239 
3240  public static void UpdateAnimAll(float deltaTime)
3241  {
3242  foreach (Character c in CharacterList)
3243  {
3244  if (!c.Enabled || c.AnimController.Frozen) continue;
3245 
3246  c.AnimController.UpdateAnimations(deltaTime);
3247  }
3248  }
3249 
3250  public static void UpdateAll(float deltaTime, Camera cam)
3251  {
3252  if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
3253  {
3254  foreach (Character c in CharacterList)
3255  {
3256  if (c is not AICharacter && !c.IsRemotePlayer) { continue; }
3257 
3258  if (c.IsPlayer || (c.IsBot && !c.IsDead))
3259  {
3260  c.Enabled = true;
3261  }
3262  else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
3263  {
3264  //disable AI characters that are far away from all clients and the host's character and not controlled by anyone
3265  float closestPlayerDist = c.GetDistanceToClosestPlayer();
3266  if (closestPlayerDist > c.Params.DisableDistance)
3267  {
3268  c.Enabled = false;
3269  if (c.IsDead && c.AIController is EnemyAIController)
3270  {
3271  Spawner?.AddEntityToRemoveQueue(c);
3272  }
3273  }
3274  else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
3275  {
3276  c.Enabled = true;
3277  }
3278  }
3279  else if (Submarine.MainSub != null)
3280  {
3281  //disable AI characters that are far away from the sub and the controlled character
3282  float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition);
3283  if (Controlled != null)
3284  {
3285  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, c.WorldPosition));
3286  }
3287  else
3288  {
3289  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.GetPosition(), c.WorldPosition));
3290  }
3291 
3292  if (distSqr > MathUtils.Pow2(c.Params.DisableDistance))
3293  {
3294  c.Enabled = false;
3295  if (c.IsDead && c.AIController is EnemyAIController)
3296  {
3298  }
3299  }
3300  else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f))
3301  {
3302  c.Enabled = true;
3303  }
3304  }
3305  }
3306  }
3307 
3308  characterUpdateTick++;
3309 
3310  if (characterUpdateTick % CharacterUpdateInterval == 0)
3311  {
3312  for (int i = 0; i < CharacterList.Count; i++)
3313  {
3314  if (GameMain.LuaCs.Game.UpdatePriorityCharacters.Contains(CharacterList[i])) continue;
3315 
3316  CharacterList[i].Update(deltaTime * CharacterUpdateInterval, cam);
3317  }
3318  }
3319 
3320  foreach (Character character in GameMain.LuaCs.Game.UpdatePriorityCharacters)
3321  {
3322  if (character.Removed) { continue; }
3323 
3324  character.Update(deltaTime, cam);
3325  }
3326 
3327 #if CLIENT
3328  UpdateSpeechBubbles(deltaTime);
3329 #endif
3330  }
3331 
3332  static partial void UpdateSpeechBubbles(float deltaTime);
3333 
3334  public virtual void Update(float deltaTime, Camera cam)
3335  {
3336  UpdateProjSpecific(deltaTime, cam);
3337 
3338  if (TextChatVolume > 0)
3339  {
3340  TextChatVolume -= 0.2f * deltaTime;
3341  }
3342 
3343  if (InvisibleTimer > 0.0f)
3344  {
3345  if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
3346  {
3347  InvisibleTimer = Math.Min(InvisibleTimer, 1.0f);
3348  }
3349  InvisibleTimer -= deltaTime;
3350  }
3351 
3352  KnockbackCooldownTimer -= deltaTime;
3353 
3354  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
3355 
3356  UpdateDespawn(deltaTime);
3357 
3358  if (!Enabled) { return; }
3359 
3360  if (Level.Loaded != null)
3361  {
3362  if (WorldPosition.Y < Level.MaxEntityDepth ||
3364  {
3365  Enabled = false;
3366  Kill(CauseOfDeathType.Pressure, null);
3367  return;
3368  }
3369  }
3370 
3371  ApplyStatusEffects(ActionType.Always, deltaTime);
3372 
3373  PreviousHull = CurrentHull;
3374  CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true);
3375 
3376  obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f);
3377 
3378  if (Inventory != null)
3379  {
3380  foreach (Item item in Inventory.AllItems)
3381  {
3382  if (item.body == null || item.body.Enabled) { continue; }
3383  item.SetTransform(SimPosition, 0.0f);
3384  item.Submarine = Submarine;
3385  }
3386  }
3387 
3388  HideFace = false;
3389  IgnoreMeleeWeapons = false;
3390 
3391  UpdateSightRange(deltaTime);
3392  UpdateSoundRange(deltaTime);
3393 
3394  UpdateAttackers(deltaTime);
3395 
3396  foreach (var characterTalent in characterTalents)
3397  {
3398  characterTalent.UpdateTalent(deltaTime);
3399  }
3400 
3401  if (IsDead) { return; }
3402 
3403  if (GameMain.NetworkMember != null)
3404  {
3405  UpdateNetInput();
3406  }
3407  else
3408  {
3409  AnimController.Frozen = false;
3410  }
3411 
3412  DisableImpactDamageTimer -= deltaTime;
3413 
3414  if (!speechImpedimentSet)
3415  {
3416  //if no statuseffect or anything else has set a speech impediment, allow speaking normally
3417  speechImpediment = 0.0f;
3418  }
3419  speechImpedimentSet = false;
3420 
3421  if (NeedsAir)
3422  {
3423  //implode if not protected from pressure, and either outside or in a high-pressure hull
3424  if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
3425  {
3426  if (PressureTimer > CharacterHealth.PressureKillDelay * 0.1f)
3427  {
3428  //after a brief delay, start doing increasing amounts of organ damage
3430  targetLimb: AnimController.MainLimb,
3431  new Affliction(AfflictionPrefab.OrganDamage, PressureTimer / 10.0f * deltaTime));
3432  }
3433 
3434  if (CharacterHealth.PressureKillDelay <= 0.0f)
3435  {
3436  PressureTimer = 100.0f;
3437  }
3438  else
3439  {
3440  PressureTimer += ((AnimController.CurrentHull == null) ?
3442  }
3443 
3444  if (PressureTimer >= 100.0f)
3445  {
3446  if (Controlled == this) { cam.Zoom = 5.0f; }
3447  if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
3448  {
3449  Implode();
3450  if (IsDead) { return; }
3451  }
3452  }
3453  }
3454  else
3455  {
3456  PressureTimer = 0.0f;
3457  }
3458  }
3459  else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure)
3460  {
3461  float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f;
3462  if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth)
3463  {
3464  //implode if below crush depth, and either outside or in a high-pressure hull
3466  {
3467  Implode();
3468  if (IsDead) { return; }
3469  }
3470  }
3471  }
3472 
3473  ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
3474  ApplyStatusEffects(ActionType.OnActive, deltaTime);
3475 
3476  //wait 0.1 seconds so status effects that continuously set InDetectable to true can keep the character InDetectable
3477  if (aiTarget != null && Timing.TotalTime > aiTarget.InDetectableSetTime + 0.1f)
3478  {
3479  aiTarget.InDetectable = false;
3480  }
3481 
3482  UpdateControlled(deltaTime, cam);
3483 
3484  //Health effects
3485  if (NeedsOxygen)
3486  {
3487  UpdateOxygen(deltaTime);
3488  }
3489 
3490  CalculateHealthMultiplier();
3491  CharacterHealth.Update(deltaTime);
3492 
3493  if (IsIncapacitated)
3494  {
3495  Stun = Math.Max(5.0f, Stun);
3497  SelectedItem = SelectedSecondaryItem = null;
3498  return;
3499  }
3500 
3501  UpdateAIChatMessages(deltaTime);
3502 
3503  bool wasRagdolled = IsRagdolled;
3504  if (IsForceRagdolled)
3505  {
3506  IsRagdolled = IsForceRagdolled;
3507  }
3508  else if (this != Controlled)
3509  {
3510  wasRagdolled = IsRagdolled;
3511  IsRagdolled = IsKeyDown(InputType.Ragdoll);
3512  if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true })
3513  {
3514  ClearInput(InputType.Ragdoll);
3515  }
3516  }
3517  else
3518  {
3519  bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body);
3520  bool bodyMovingTooFast(PhysicsBody body)
3521  {
3522  return
3523  body.LinearVelocity.LengthSquared() > 8.0f * 8.0f ||
3524  //falling down counts as going too fast
3525  (!InWater && body.LinearVelocity.Y < -5.0f);
3526  }
3527  if (ragdollingLockTimer > 0.0f)
3528  {
3529  ragdollingLockTimer -= deltaTime;
3530  }
3531  else if (!tooFastToUnragdoll)
3532  {
3533  IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
3534  if (wasRagdolled != IsRagdolled && !AnimController.IsHangingWithRope)
3535  {
3536  ragdollingLockTimer = 0.2f;
3537  }
3538  }
3539  SetInput(InputType.Ragdoll, false, IsRagdolled);
3540  }
3541  if (!wasRagdolled && IsRagdolled && !AnimController.IsHangingWithRope)
3542  {
3543  CheckTalents(AbilityEffectType.OnRagdoll);
3544  }
3545 
3546  lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
3547 
3548  if (IsRagdolled || !CanMove)
3549  {
3550  if (AnimController is HumanoidAnimController humanAnimController)
3551  {
3552  humanAnimController.Crouching = false;
3553  }
3554  if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
3556  SelectedItem = SelectedSecondaryItem = null;
3557  return;
3558  }
3559 
3560  //AI and control stuff
3561 
3562  Control(deltaTime, cam);
3563 
3564  bool isNotControlled = Controlled != this;
3565 
3566  if (isNotControlled && (!(this is AICharacter) || IsRemotePlayer))
3567  {
3568  Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
3569  DoInteractionUpdate(deltaTime, mouseSimPos);
3570  }
3571 
3572  if (MustDeselect(SelectedItem))
3573  {
3574  SelectedItem = null;
3575  }
3576  if (MustDeselect(SelectedSecondaryItem))
3577  {
3578  ReleaseSecondaryItem();
3579  }
3580 
3581  if (!IsDead) { LockHands = false; }
3582 
3583  bool MustDeselect(Item item)
3584  {
3585  if (item == null) { return false; }
3586  if (!CanInteractWith(item)) { return true; }
3587  bool hasSelectableComponent = false;
3588  foreach (var component in item.Components)
3589  {
3590  //the "selectability" of an item can change e.g. if the player unequips another item that's required to access it
3591  if (component.CanBeSelected && component.HasRequiredItems(this, addMessage: false))
3592  {
3593  hasSelectableComponent = true;
3594  break;
3595  }
3596  }
3597  return !hasSelectableComponent;
3598  }
3599  }
3600 
3601  partial void UpdateControlled(float deltaTime, Camera cam);
3602 
3603  partial void UpdateProjSpecific(float deltaTime, Camera cam);
3604 
3605  partial void SetOrderProjSpecific(Order order);
3606 
3607 
3608  public void AddAttacker(Character character, float damage)
3609  {
3610  Attacker attacker = lastAttackers.FirstOrDefault(a => a.Character == character);
3611  if (attacker != null)
3612  {
3613  lastAttackers.Remove(attacker);
3614  }
3615  else
3616  {
3617  attacker = new Attacker { Character = character };
3618  }
3619 
3620  if (lastAttackers.Count > maxLastAttackerCount)
3621  {
3622  lastAttackers.RemoveRange(0, lastAttackers.Count - maxLastAttackerCount);
3623  }
3624 
3625  attacker.Damage += damage;
3626  lastAttackers.Add(attacker);
3627  }
3628 
3629  public void ForgiveAttacker(Character character)
3630  {
3631  int index;
3632  if ((index = lastAttackers.FindIndex(a => a.Character == character)) >= 0)
3633  {
3634  lastAttackers.RemoveAt(index);
3635  }
3636  }
3637 
3638  public float GetDamageDoneByAttacker(Character otherCharacter)
3639  {
3640  if (otherCharacter == null) { return 0; }
3641  float dmg = 0;
3642  Attacker attacker = LastAttackers.LastOrDefault(a => a.Character == otherCharacter);
3643  if (attacker != null)
3644  {
3645  dmg = attacker.Damage;
3646  }
3647  return dmg;
3648  }
3649 
3650  private void UpdateAttackers(float deltaTime)
3651  {
3652  //slowly forget about damage done by attackers
3653  foreach (Attacker enemy in LastAttackers)
3654  {
3655  float cumulativeDamage = enemy.Damage;
3656  if (cumulativeDamage > 0)
3657  {
3658  float reduction = deltaTime;
3659  if (cumulativeDamage < 2)
3660  {
3661  // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
3662  reduction *= 0.5f;
3663  }
3664  enemy.Damage = Math.Max(0.0f, enemy.Damage - reduction);
3665  }
3666  }
3667  }
3668 
3669  private void UpdateOxygen(float deltaTime)
3670  {
3671  if (NeedsAir)
3672  {
3673  if (Timing.TotalTime > pressureProtectionLastSet + 0.1)
3674  {
3675  pressureProtection = 0.0f;
3676  }
3677  }
3678  if (NeedsWater)
3679  {
3680  float waterAvailable = 100;
3681  if (!AnimController.InWater && CurrentHull != null)
3682  {
3683  waterAvailable = CurrentHull.WaterPercentage;
3684  }
3685  OxygenAvailable += MathHelper.Clamp(waterAvailable - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
3686  }
3687  else
3688  {
3689  float hullAvailableOxygen = 0.0f;
3690  if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
3691  {
3692  //don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
3693  //(i.e. if the character has some external source of oxygen)
3694  if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage && UseHullOxygen)
3695  {
3696  AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
3697  }
3698  hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
3699 
3700  }
3701  OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
3702  }
3703  UseHullOxygen = true;
3704  }
3705 
3709  protected float GetDistanceToClosestPlayer()
3710  {
3711  return (float)Math.Sqrt(GetDistanceSqrToClosestPlayer());
3712  }
3713 
3718  {
3719  float distSqr = float.MaxValue;
3720  foreach (Character otherCharacter in CharacterList)
3721  {
3722  if (otherCharacter == this || !otherCharacter.IsRemotePlayer) { continue; }
3723  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, WorldPosition));
3724  if (otherCharacter.ViewTarget != null)
3725  {
3726  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.ViewTarget.WorldPosition, WorldPosition));
3727  }
3728  }
3729 #if SERVER
3730  for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++)
3731  {
3732  var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos;
3733  if (spectatePos != null)
3734  {
3735  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(spectatePos.Value, WorldPosition));
3736  }
3737  }
3738 #else
3739  if (this == Controlled) { return 0.0f; }
3740  if (controlled != null)
3741  {
3742  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, WorldPosition));
3743  }
3744  distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.Position, WorldPosition));
3745 #endif
3746  return distSqr;
3747  }
3748 
3749  public float GetDistanceToClosestLimb(Vector2 simPos)
3750  {
3751  float closestDist = float.MaxValue;
3752  foreach (Limb limb in AnimController.Limbs)
3753  {
3754  if (limb.IsSevered) { continue; }
3755  float dist = Vector2.Distance(simPos, limb.SimPosition);
3756  dist -= limb.body.GetMaxExtent();
3757  closestDist = Math.Min(closestDist, dist);
3758  if (closestDist <= 0.0f) { return 0.0f; }
3759  }
3760  return closestDist;
3761  }
3762 
3763  private float despawnTimer;
3764  private void UpdateDespawn(float deltaTime, bool createNetworkEvents = true)
3765  {
3766  if (!EnableDespawn) { return; }
3767 
3768  //clients don't despawn characters unless the server says so
3769  if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
3770 
3771  if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; }
3772 
3773  int subCorpseCount = 0;
3774 
3775  if (Submarine != null)
3776  {
3777  subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
3778  if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; }
3779  }
3780 
3781  if (SelectedBy != null)
3782  {
3783  despawnTimer = 0.0f;
3784  return;
3785  }
3786 
3787  float distToClosestPlayer = GetDistanceToClosestPlayer();
3788  if (distToClosestPlayer > Params.DisableDistance)
3789  {
3790  //despawn in 1 minute if very far from all human players
3791  despawnTimer = Math.Max(despawnTimer, GameSettings.CurrentConfig.CorpseDespawnDelay - 60.0f);
3792  }
3793 
3794  float despawnPriority = 1.0f;
3795  if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold)
3796  {
3797  //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
3798  despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold;
3799  }
3800  if (AIController is EnemyAIController)
3801  {
3802  //enemies despawn faster
3803  despawnPriority *= 2.0f;
3804  }
3805 
3806  despawnTimer += deltaTime * despawnPriority;
3807  if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
3808 
3809  Despawn();
3810  }
3811 
3812  private void Despawn(bool createNetworkEvents = true)
3813  {
3814  if (!EnableDespawn) { return; }
3815 
3816  Identifier despawnContainerId =
3817  IsHuman ?
3818  Tags.DespawnContainer :
3819  Params.DespawnContainer;
3820  if (!despawnContainerId.IsEmpty)
3821  {
3822  var containerPrefab =
3823  MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
3824  ItemPrefab.Prefabs.Find(me => me?.Tags != null && me.Tags.Contains(despawnContainerId)) ??
3825  (MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab);
3826  if (containerPrefab == null)
3827  {
3828  DebugConsole.NewMessage($"Could not spawn a container for a despawned character's items. No item with the tag \"{despawnContainerId}\" or the identifier \"metalcrate\" found.", Color.Red);
3829  }
3830  else
3831  {
3832  Spawner?.AddItemToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
3833  }
3834 
3835  void onItemContainerSpawned(Item item)
3836  {
3837  if (Inventory == null) { return; }
3838 
3839  item.UpdateTransform();
3840  item.AddTag("name:" + Name);
3841  if (info?.Job != null) { item.AddTag($"job:{info.Job.Name}"); }
3842 
3843  var itemContainer = item?.GetComponent<ItemContainer>();
3844  if (itemContainer == null) { return; }
3845  List<Item> inventoryItems = new List<Item>(Inventory.AllItemsMod);
3846 
3847  //unequipping genetic materials normally destroys it in GeneticMaterial.Update, let's do that manually here
3848  var geneticMaterials = Inventory.FindAllItems(it => it.GetComponent<GeneticMaterial>() != null, recursive: true);
3849  foreach (var geneticMaterial in geneticMaterials)
3850  {
3851  geneticMaterial.ApplyStatusEffects(ActionType.OnSevered, 1.0f, this);
3852  }
3853 
3854  foreach (Item inventoryItem in inventoryItems)
3855  {
3856  if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents))
3857  {
3858  //if the item couldn't be put inside the despawn container, just drop it
3859  inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents);
3860  }
3861  }
3862  //this needs to happen after the items have been dropped (we can no longer sync dropping the items if the character has been removed)
3863  Spawner.AddEntityToRemoveQueue(this);
3864  }
3865  }
3866  else
3867  {
3868  Spawner.AddEntityToRemoveQueue(this);
3869  }
3870  }
3871 
3872  public void DespawnNow(bool createNetworkEvents = true)
3873  {
3874  Despawn(createNetworkEvents);
3875  //update twice: first to spawn the duffel bag and move the items into it, then to remove the character
3876  for (int i = 0; i < 2; i++)
3877  {
3878  Spawner.Update(createNetworkEvents);
3879  }
3880  }
3881 
3882  public static void RemoveByPrefab(CharacterPrefab prefab)
3883  {
3884  if (CharacterList == null) { return; }
3885  List<Character> list = new List<Character>(CharacterList);
3886  foreach (Character character in list)
3887  {
3888  if (character.Prefab == prefab)
3889  {
3890  character.Remove();
3891  }
3892  }
3893  }
3894 
3895  private readonly float maxAIRange = 20000;
3896  private readonly float aiTargetChangeSpeed = 5;
3897 
3898  private void UpdateSightRange(float deltaTime)
3899  {
3900  if (aiTarget == null) { return; }
3901  float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000);
3902  float massFactor = (float)Math.Sqrt(Mass / 20);
3903  float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
3904  targetRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
3905  float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
3906  if (!float.IsNaN(newRange))
3907  {
3908  aiTarget.SightRange = newRange;
3909  }
3910  }
3911 
3912  private void UpdateSoundRange(float deltaTime)
3913  {
3914  const float textChatVolumeMultiplier = 0.5f;
3915  const float voiceChatVolumeMultiplier = 1.5f;
3916 
3917  if (aiTarget == null) { return; }
3918  if (IsDead)
3919  {
3920  aiTarget.SoundRange = 0;
3921  }
3922  else
3923  {
3924  float massFactor = (float)Math.Sqrt(Mass / 10);
3925  float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
3926  float speechImpedimentMultiplier = 1.0f - SpeechImpediment / 100.0f;
3927  if (TextChatVolume > 0)
3928  {
3929  targetRange = Math.Max(targetRange, TextChatVolume * textChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
3930  }
3931 
3932  if (IsPlayer)
3933  {
3934  float voipAmplitude = 0.0f;
3935 #if SERVER
3936  foreach (var c in GameMain.Server.ConnectedClients)
3937  {
3938  if (c.Character != this) { continue; }
3939  voipAmplitude = c.VoipServerDecoder.Amplitude;
3940  break;
3941  }
3942 #elif CLIENT && DEBUG
3943  if (Controlled == this && GameMain.Client != null)
3944  {
3945  voipAmplitude = GameMain.Client.DebugServerVoipAmplitude;
3946  }
3947 #endif
3948  targetRange = Math.Max(targetRange, voipAmplitude * voiceChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
3949  }
3950 
3951  targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
3952  targetRange = Math.Min(targetRange, maxAIRange);
3953 
3954  float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
3955 
3956  newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
3957  if (!float.IsNaN(newRange))
3958  {
3959  aiTarget.SoundRange = newRange;
3960  }
3961  }
3962  }
3963 
3964  public bool CanHearCharacter(Character speaker)
3965  {
3966  if (speaker == null || speaker.SpeechImpediment > 100.0f) { return false; }
3967  if (speaker == this) { return true; }
3968  ChatMessageType messageType = ChatMessage.CanUseRadio(speaker) && ChatMessage.CanUseRadio(this) ?
3969  ChatMessageType.Radio :
3970  ChatMessageType.Default;
3971  return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this));
3972  }
3973 
3975  public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force = false)
3976  {
3977  var orderGiver = order?.OrderGiver;
3978  //set the character order only if the character is close enough to hear the message
3979  if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
3980 
3981  if (order != null)
3982  {
3983  if (order.AutoDismiss)
3984  {
3985  switch (order.Category)
3986  {
3987  case OrderCategory.Operate when order.TargetEntity != null:
3988  // If there's another character operating the same device, make them dismiss themself
3989  foreach (var character in CharacterList)
3990  {
3991  if (character == this) { continue; }
3992  if (character.TeamID != TeamID) { continue; }
3993  if (character.AIController is not HumanAIController) { continue; }
3994  if (!HumanAIController.IsActive(character)) { continue; }
3995  foreach (var currentOrder in character.CurrentOrders)
3996  {
3997  if (currentOrder == null) { continue; }
3998  if (currentOrder.Category != OrderCategory.Operate) { continue; }
3999  if (currentOrder.Identifier != order.Identifier) { continue; }
4000  if (currentOrder.TargetEntity != order.TargetEntity) { continue; }
4001  if (!currentOrder.AutoDismiss) { continue; }
4002  character.SetOrder(currentOrder.GetDismissal(), isNewOrder, speak: speak, force: force);
4003  break;
4004  }
4005  }
4006  break;
4007  case OrderCategory.Movement:
4008  // If there character has another movement order, dismiss that order
4009  Order orderToReplace = null;
4010  foreach (var currentOrder in CurrentOrders)
4011  {
4012  if (currentOrder == null) { continue; }
4013  if (currentOrder.Category != OrderCategory.Movement) { continue; }
4014  orderToReplace = currentOrder;
4015  break;
4016  }
4017  if (orderToReplace is { AutoDismiss: true })
4018  {
4019  SetOrder(orderToReplace.GetDismissal(), isNewOrder, speak: speak, force: force);
4020  }
4021  break;
4022  }
4023  }
4024  }
4025 
4026  // Prevent adding duplicate orders
4027  RemoveDuplicateOrders(order);
4028  AddCurrentOrder(order);
4029 
4030  if (orderGiver != null && order.Identifier != "dismissed" && isNewOrder)
4031  {
4032  var abilityOrderedCharacter = new AbilityOrderedCharacter(this);
4033  orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter);
4034 
4035  if (order.OrderGiver.LastOrderedCharacter != this)
4036  {
4038  order.OrderGiver.LastOrderedCharacter = this;
4039  }
4040  }
4041 
4042  if (AIController is HumanAIController humanAI)
4043  {
4044  humanAI.SetOrder(order, speak);
4045  }
4046  SetOrderProjSpecific(order);
4047  }
4048 
4049  private void AddCurrentOrder(Order newOrder)
4050  {
4051  if (newOrder == null || newOrder.Identifier == "dismissed")
4052  {
4053  if (newOrder.Option != Identifier.Empty)
4054  {
4055  if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.Option)))
4056  {
4057  var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.Option));
4058  int dismissedOrderPriority = dismissedOrderInfo.ManualPriority;
4059  CurrentOrders.Remove(dismissedOrderInfo);
4060  for (int i = 0; i < CurrentOrders.Count; i++)
4061  {
4062  var orderInfo = CurrentOrders[i];
4063  if (orderInfo.ManualPriority < dismissedOrderPriority)
4064  {
4065  CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority + 1);
4066  }
4067  }
4068  }
4069  }
4070  else
4071  {
4072  CurrentOrders.Clear();
4073  }
4074  }
4075  else
4076  {
4077  for (int i = 0; i < CurrentOrders.Count; i++)
4078  {
4079  var orderInfo = CurrentOrders[i];
4080  if (orderInfo.ManualPriority <= newOrder.ManualPriority)
4081  {
4082  CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority - 1);
4083  }
4084  }
4085  CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
4086  CurrentOrders.Add(newOrder);
4087  // Sort the current orders so the one with the highest priority comes first
4088  CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
4089  }
4090  }
4091 
4092  private bool RemoveDuplicateOrders(Order order)
4093  {
4094  bool removed = false;
4095  int? priorityOfRemoved = null;
4096  for (int i = CurrentOrders.Count - 1; i >= 0; i--)
4097  {
4098  var orderInfo = CurrentOrders[i];
4099  if (order.Identifier == orderInfo.Identifier)
4100  {
4101  priorityOfRemoved = orderInfo.ManualPriority;
4102  CurrentOrders.RemoveAt(i);
4103  removed = true;
4104  break;
4105  }
4106  }
4107 
4108  if (!priorityOfRemoved.HasValue) { return removed; }
4109 
4110  for (int i = 0; i < CurrentOrders.Count; i++)
4111  {
4112  var orderInfo = CurrentOrders[i];
4113  if (orderInfo.ManualPriority < priorityOfRemoved.Value)
4114  {
4115  CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority + 1);
4116  }
4117  }
4118 
4119  CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
4120  // Sort the current orders so the one with the highest priority comes first
4121  CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
4122 
4123  return removed;
4124  }
4125 
4127  {
4128  return GetCurrentOrder(orderInfo =>
4129  {
4130  if (orderInfo == null) { return false; }
4131  if (orderInfo.Identifier == "dismissed") { return false; }
4132  if (orderInfo.ManualPriority < 1) { return false; }
4133  return true;
4134  });
4135  }
4136 
4138  {
4139  return GetCurrentOrder(orderInfo =>
4140  {
4141  return orderInfo.MatchesOrder(order);
4142  });
4143  }
4144 
4145  private Order GetCurrentOrder(Func<Order, bool> predicate)
4146  {
4147  if (CurrentOrders != null && CurrentOrders.Any(predicate))
4148  {
4149  return CurrentOrders.First(predicate);
4150  }
4151  else
4152  {
4153  return null;
4154  }
4155  }
4156 
4157  private readonly List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
4158 
4159  //key = identifier, value = time the message was sent
4160  private readonly Dictionary<Identifier, float> prevAiChatMessages = new Dictionary<Identifier, float>();
4161 
4162  public void DisableLine(Identifier identifier)
4163  {
4164  if (identifier != Identifier.Empty)
4165  {
4166  prevAiChatMessages[identifier] = (float)Timing.TotalTime;
4167  }
4168  }
4169 
4170  public void DisableLine(string identifier)
4171  {
4172  DisableLine(identifier.ToIdentifier());
4173  }
4174 
4175  public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, Identifier identifier = default, float minDurationBetweenSimilar = 0.0f)
4176  {
4177  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
4178  if (string.IsNullOrEmpty(message)) { return; }
4179 
4180  if (SpeechImpediment >= 100.0f) { return; }
4181 
4182  if (prevAiChatMessages.ContainsKey(identifier) &&
4183  prevAiChatMessages[identifier] < Timing.TotalTime - minDurationBetweenSimilar)
4184  {
4185  prevAiChatMessages.Remove(identifier);
4186  }
4187 
4188  //already sent a similar message a moment ago
4189  if (identifier != Identifier.Empty && minDurationBetweenSimilar > 0.0f &&
4190  (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier)))
4191  {
4192  return;
4193  }
4194  aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
4195  }
4196 
4197  private void UpdateAIChatMessages(float deltaTime)
4198  {
4199  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
4200 
4201  List<AIChatMessage> sentMessages = new List<AIChatMessage>();
4202  foreach (AIChatMessage message in aiChatMessageQueue)
4203  {
4204  message.SendDelay -= deltaTime;
4205  if (message.SendDelay > 0.0f) { continue; }
4206 
4207  bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
4208  if (message.MessageType == null)
4209  {
4210  message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
4211  }
4212 #if CLIENT
4213  if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
4214  {
4215  string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
4216  if (!string.IsNullOrEmpty(modifiedMessage))
4217  {
4218  GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
4219  }
4220  if (canUseRadio)
4221  {
4222  Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
4223  radio.TransmitSignal(s, sentFromChat: true);
4224  }
4225  }
4226 #endif
4227 #if SERVER
4228  if (GameMain.Server != null && message.MessageType != ChatMessageType.Order)
4229  {
4230  GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
4231  }
4232 #endif
4233 #if CLIENT
4234  ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message);
4235 #endif
4236  sentMessages.Add(message);
4237  }
4238 
4239  foreach (AIChatMessage sent in sentMessages)
4240  {
4241  sent.SendTime = Timing.TotalTime;
4242  aiChatMessageQueue.Remove(sent);
4243  if (sent.Identifier != Identifier.Empty)
4244  {
4245  prevAiChatMessages[sent.Identifier] = (float)sent.SendTime;
4246  }
4247  }
4248 
4249  if (prevAiChatMessages.Count > 100)
4250  {
4251  HashSet<Identifier> toRemove = new HashSet<Identifier>();
4252  foreach (KeyValuePair<Identifier,float> prevMessage in prevAiChatMessages)
4253  {
4254  if (prevMessage.Value < Timing.TotalTime - 60.0f)
4255  {
4256  toRemove.Add(prevMessage.Key);
4257  }
4258  }
4259  foreach (Identifier identifier in toRemove)
4260  {
4261  prevAiChatMessages.Remove(identifier);
4262  }
4263  }
4264  }
4265 
4266  public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
4267  {
4268  CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
4269  }
4270 
4271  public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
4272  {
4273  return ApplyAttack(attacker, worldPosition, attack, deltaTime, impulseDirection, playSound);
4274  }
4275 
4279  public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
4280  {
4281  if (Removed)
4282  {
4283  string errorMsg = "Tried to apply an attack to a removed character ([name]).\n" + Environment.StackTrace.CleanupStackTrace();
4284  DebugConsole.ThrowError(errorMsg.Replace("[name]", Name));
4285  GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value));
4286  return new AttackResult();
4287  }
4288 
4289  Limb limbHit = targetLimb;
4290 
4291  float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime;
4292 
4293  Vector2 attackImpulse = Vector2.Zero;
4294  if (Math.Abs(impulseMagnitude) > 0.0f)
4295  {
4296  impulseDirection = impulseDirection.LengthSquared() > 0.0001f ?
4297  Vector2.Normalize(impulseDirection) :
4298  Vector2.UnitX;
4299  attackImpulse = impulseDirection * impulseMagnitude;
4300  }
4301 
4302  AbilityAttackData attackData = new AbilityAttackData(attack, this, attacker);
4303  IEnumerable<Affliction> attackAfflictions;
4304  if (attackData.Afflictions != null)
4305  {
4306  attackAfflictions = attackData.Afflictions.Union(attack.Afflictions.Keys);
4307  }
4308  else
4309  {
4310  attackAfflictions = attack.Afflictions.Keys;
4311  }
4312 
4313  var attackResult = targetLimb == null ?
4314  AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) :
4315  DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode);
4316 
4317  if (attacker != null)
4318  {
4319  var abilityAttackResult = new AbilityAttackResult(attackResult);
4320  attacker.CheckTalents(AbilityEffectType.OnAttackResult, abilityAttackResult);
4321  CheckTalents(AbilityEffectType.OnAttackedResult, abilityAttackResult);
4322  }
4323 
4324  if (limbHit == null) { return new AttackResult(); }
4325  Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier;
4326  if (attacker != null)
4327  {
4328  forceWorld.X *= attacker.AnimController.Dir;
4329  }
4330  limbHit.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
4331  var mainLimb = limbHit.character.AnimController.MainLimb;
4332  if (limbHit != mainLimb)
4333  {
4334  // Always add force to mainlimb
4335  mainLimb.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
4336  }
4337 #if SERVER
4338  if (attacker is Character attackingCharacter && attackingCharacter.AIController == null)
4339  {
4340  StringBuilder sb = new StringBuilder();
4341  sb.Append(GameServer.CharacterLogName(this) + " attacked by " + GameServer.CharacterLogName(attackingCharacter) + ".");
4342  if (attackResult.Afflictions != null)
4343  {
4344  foreach (Affliction affliction in attackResult.Afflictions)
4345  {
4346  if (Math.Abs(affliction.Strength) <= 0.1f) { continue;}
4347  sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}");
4348  }
4349  }
4350  GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
4351  }
4352 #endif
4353  // Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
4354  TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer, attacker: attacker);
4355 
4356  return attackResult;
4357  }
4358 
4359  public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null)
4360  {
4361  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
4362 #if DEBUG
4363  if (targetLimb.character != this)
4364  {
4365  DebugConsole.ThrowError($"{Name} is attempting to sever joints of {targetLimb.character.Name}!");
4366  return;
4367  }
4368 #endif
4369  if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; }
4370  if (!IsDead)
4371  {
4372  if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
4373  if (!targetLimb.CanBeSeveredAlive) { return; }
4374  }
4375  bool wasSevered = false;
4376  float random = Rand.Value();
4377  foreach (LimbJoint joint in AnimController.LimbJoints)
4378  {
4379  if (!joint.CanBeSevered) { continue; }
4380  // Limb A is where we start creating the joint and LimbB is where the joint ends.
4381  // Normally the joints have been created starting from the body, in which case we'd want to use LimbB e.g. to severe a hand when it's hit.
4382  // But heads are a different case, because many characters have been created so that the head is first and then comes the rest of the body.
4383  // If this is the case, we'll have to use LimbA to decapitate the creature when it's hit on the head. Otherwise decapitation could happen only when we hit the body, not the head.
4384  var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
4385  if (referenceLimb != targetLimb) { continue; }
4386  float probability = severLimbsProbability;
4387  if (!IsDead && !ignoreSeveranceProbabilityModifier)
4388  {
4389  probability *= joint.Params.SeveranceProbabilityModifier;
4390  }
4391  if (probability <= 0) { continue; }
4392  if (random > probability) { continue; }
4393  bool severed = AnimController.SeverLimbJoint(joint);
4394  if (!wasSevered)
4395  {
4396  wasSevered = severed;
4397  }
4398  if (severed)
4399  {
4400  Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
4401  otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
4402  if (attacker != null)
4403  {
4404  if (statusEffects.TryGetValue(ActionType.OnSevered, out var statusEffectList))
4405  {
4406  foreach (var statusEffect in statusEffectList)
4407  {
4408  statusEffect.SetUser(attacker);
4409  }
4410  }
4411  if (targetLimb.StatusEffects.TryGetValue(ActionType.OnSevered, out var limbStatusEffectList))
4412  {
4413  foreach (var statusEffect in limbStatusEffectList)
4414  {
4415  statusEffect.SetUser(attacker);
4416  }
4417  }
4418  }
4419  ApplyStatusEffects(ActionType.OnSevered, 1.0f);
4420  targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
4421  }
4422  }
4423  if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
4424  {
4425  enemyAI.ReevaluateAttacks();
4426  }
4427  }
4428 
4429  public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2? attackImpulse = null, Character attacker = null, float damageMultiplier = 1f)
4430  {
4431  return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse ?? Vector2.Zero, out _, attacker, damageMultiplier: damageMultiplier);
4432  }
4433 
4434  public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
4435  {
4436  hitLimb = null;
4437 
4438  if (Removed) { return new AttackResult(); }
4439 
4440  float closestDistance = 0.0f;
4441  foreach (Limb limb in AnimController.Limbs)
4442  {
4443  float distance = Vector2.DistanceSquared(worldPosition, limb.WorldPosition);
4444  if (hitLimb == null || distance < closestDistance)
4445  {
4446  hitLimb = limb;
4447  closestDistance = distance;
4448  }
4449  }
4450 
4451  return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier);
4452  }
4453 
4454  public void RecordKill(Character target)
4455  {
4456  var abilityCharacterKill = new AbilityCharacterKill(target, this);
4457  foreach (Character attackerCrewmember in GetFriendlyCrew(this))
4458  {
4459  attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, abilityCharacterKill);
4460  }
4461  CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill);
4462 
4463  if (!IsOnPlayerTeam) { return; }
4464  CreatureMetrics.RecordKill(target.SpeciesName);
4465  }
4466 
4467  public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
4468  {
4469  if (Removed) { return new AttackResult(); }
4470 
4471  //character inside the sub received damage from a monster outside the sub
4472  //can happen during normal gameplay if someone for example fires a ranged weapon from outside,
4473  //the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside
4474 
4475  // Disabled, because this happens every now and then when the monsters can get in and out of the sub.
4476 
4477 // if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null)
4478 // {
4479 // string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})";
4480 // GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name,
4481 // GameAnalyticsManager.ErrorSeverity.Warning,
4482 // errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
4483 //#if DEBUG
4484 // DebugConsole.ThrowError(errorMsg);
4485 //#endif
4486 // }
4487 
4488  SetStun(stun);
4489 
4490  if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
4491  {
4492  if (attacker.TeamID == TeamID)
4493  {
4494  afflictions = afflictions.Where(a => a.Prefab.IsBuff);
4495  if (!afflictions.Any()) { return new AttackResult(); }
4496  }
4497  }
4498 
4499  Vector2 dir = hitLimb.WorldPosition - worldPosition;
4500  if (attackImpulse.LengthSquared() > 0.0f)
4501  {
4502  Vector2 diff = dir;
4503  if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); }
4504  Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff);
4505  hitLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
4506  var mainLimb = hitLimb.character.AnimController.MainLimb;
4507  if (hitLimb != mainLimb)
4508  {
4509  // Always add force to mainlimb
4510  mainLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
4511  }
4512  }
4513  bool wasDead = IsDead;
4514  Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
4515  float prevVitality = CharacterHealth.Vitality;
4516  AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker);
4517  CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
4518  if (shouldImplode)
4519  {
4520  // Only used by assistant's True Potential talent. Has to run here in order to properly give kill credit when it activates.
4521  Implode();
4522  }
4523 
4524  if (attacker != this)
4525  {
4526  OnAttacked?.Invoke(attacker, attackResult);
4527  OnAttackedProjSpecific(attacker, attackResult, stun);
4528  if (!wasDead)
4529  {
4530  TryAdjustAttackerSkill(attacker, attackResult);
4531  }
4532  }
4533  if (attackResult.Damage > 0)
4534  {
4535  LastDamage = attackResult;
4536  if (attacker != null && attacker != this && !attacker.Removed)
4537  {
4538  AddAttacker(attacker, attackResult.Damage);
4539  if (IsOnPlayerTeam)
4540  {
4541  CreatureMetrics.AddEncounter(attacker.SpeciesName);
4542  }
4543  if (attacker.IsOnPlayerTeam)
4544  {
4545  CreatureMetrics.AddEncounter(SpeciesName);
4546  }
4547  }
4548  ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
4549  hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
4550  }
4551 #if CLIENT
4552  if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID)
4553  {
4554  CharacterHUD.ShowBossHealthBar(this, attackResult.Damage);
4555  }
4556 #endif
4557  return attackResult;
4558  }
4559 
4560  partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
4561 
4562  public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult)
4563  {
4564  if (attacker == null) { return; }
4565  if (!attacker.IsOnPlayerTeam) { return; }
4566  bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
4567  if (!isEnemy) { return; }
4568  float weaponDamage = 0;
4569  float medicalDamage = 0;
4570  foreach (var affliction in attackResult.Afflictions)
4571  {
4572  if (affliction.Prefab.IsBuff) { continue; }
4573  if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
4574  if (Params.Health.ImmunityIdentifiers.Contains(affliction.Identifier)) { continue; }
4575  if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
4576  {
4577  if (!Params.Health.PoisonImmunity)
4578  {
4579  float relativeVitality = MaxVitality / 100f;
4580  // Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied.
4581  float dmg = affliction.Strength;
4582  if (relativeVitality > 0)
4583  {
4584  dmg /= relativeVitality;
4585  }
4586  if (PoisonVulnerability > 0)
4587  {
4588  dmg /= PoisonVulnerability;
4589  }
4590  float strength = MaxVitality;
4591  if (Params.AI != null)
4592  {
4593  strength = Params.AI.CombatStrength;
4594  }
4595  // Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x.
4596  float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength));
4597  dmg *= vitalityFactor;
4598  medicalDamage += dmg * affliction.Prefab.MedicalSkillGain;
4599  }
4600  }
4601  else
4602  {
4603  medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain;
4604  }
4605  weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain;
4606  }
4607  if (medicalDamage > 0)
4608  {
4609  IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage);
4610  }
4611  if (weaponDamage > 0)
4612  {
4613  IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage);
4614  }
4615 
4616  void IncreaseSkillLevel(Identifier skill, float damage)
4617  {
4618  attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f);
4619  }
4620  }
4621 
4622  public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null)
4623  {
4624  if (healer == null) { return; }
4625  bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID;
4626  if (isEnemy) { return; }
4627  float medicalGain = healthChange;
4628  if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines))
4629  {
4630  medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
4631  }
4632  if (medicalGain > 0)
4633  {
4634  healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
4635  }
4636  }
4637 
4642  public bool IsKnockedDown => (IsRagdolled && !AnimController.IsHangingWithRope) || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
4643 
4644  public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
4645  {
4646  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
4647  if (Screen.Selected != GameMain.GameScreen) { return; }
4648  if (newStun > 0 && Params.Health.StunImmunity)
4649  {
4650  if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
4651  {
4652  return;
4653  }
4654  }
4655  if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
4656  if (Math.Sign(newStun) != Math.Sign(Stun))
4657  {
4659  }
4660  CharacterHealth.Stun = newStun;
4661  if (newStun > 0.0f)
4662  {
4663  SelectedItem = SelectedSecondaryItem = null;
4664  if (SelectedCharacter != null) { DeselectCharacter(); }
4665  }
4666  HealthUpdateInterval = 0.0f;
4667  }
4668 
4669  private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
4670  public void ApplyStatusEffects(ActionType actionType, float deltaTime)
4671  {
4672  if (actionType == ActionType.OnEating)
4673  {
4674  float eatingRegen = Params.Health.HealthRegenerationWhenEating;
4675  if (eatingRegen > 0)
4676  {
4678  }
4679  }
4680  if (statusEffects.TryGetValue(actionType, out var statusEffectList))
4681  {
4682  foreach (StatusEffect statusEffect in statusEffectList)
4683  {
4684  if (statusEffect.type == ActionType.OnDamaged)
4685  {
4686  if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; }
4687  if (statusEffect.OnlyWhenDamagedByPlayer)
4688  {
4689  if (LastAttacker == null || !LastAttacker.IsPlayer)
4690  {
4691  continue;
4692  }
4693  }
4694  }
4695  if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
4696  statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
4697  {
4698  targets.Clear();
4699  statusEffect.AddNearbyTargets(WorldPosition, targets);
4700  statusEffect.Apply(actionType, deltaTime, this, targets);
4701  }
4702  else if (statusEffect.targetLimbs != null)
4703  {
4704  foreach (var limbType in statusEffect.targetLimbs)
4705  {
4706  if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
4707  {
4708  // Target all matching limbs
4709  foreach (var limb in AnimController.Limbs)
4710  {
4711  if (limb.IsSevered) { continue; }
4712  if (limb.type == limbType)
4713  {
4714  ApplyToLimb(actionType, deltaTime, statusEffect, this, limb);
4715  }
4716  }
4717  }
4718  else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
4719  {
4720  // Target just the first matching limb
4721  Limb limb = AnimController.GetLimb(limbType);
4722  if (limb != null)
4723  {
4724  ApplyToLimb(actionType, deltaTime, statusEffect, this, limb);
4725  }
4726  }
4727  else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
4728  {
4729  // Target just the last matching limb
4730  Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
4731  if (limb != null)
4732  {
4733  ApplyToLimb(actionType, deltaTime, statusEffect, this, limb);
4734  }
4735  }
4736  }
4737  }
4738  else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
4739  {
4740  // Target all limbs
4741  foreach (var limb in AnimController.Limbs)
4742  {
4743  if (limb.IsSevered) { continue; }
4744  ApplyToLimb(actionType, deltaTime, statusEffect, character: this, limb);
4745  }
4746  }
4747  if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character))
4748  {
4749  statusEffect.Apply(actionType, deltaTime, this, this);
4750  }
4751  if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
4752  {
4753  statusEffect.Apply(actionType, deltaTime, this, CurrentHull);
4754  }
4755  }
4756  if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
4757  {
4758  // OnDamaged is called only for the limb that is hit.
4759  foreach (Limb limb in AnimController.Limbs)
4760  {
4761  limb.ApplyStatusEffects(actionType, deltaTime);
4762  }
4763  }
4764  }
4765  //OnActive effects are handled by the afflictions themselves
4766  if (actionType != ActionType.OnActive)
4767  {
4769  }
4770 
4771  static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb)
4772  {
4773  statusEffect.sourceBody = limb.body;
4774  statusEffect.Apply(actionType, deltaTime, entity: character, target: limb);
4775  }
4776  }
4777 
4778  private void Implode(bool isNetworkMessage = false)
4779  {
4780  if (CharacterHealth.Unkillable || GodMode || IsDead) { return; }
4781 
4782  if (!isNetworkMessage)
4783  {
4784  if (GameMain.NetworkMember is { IsClient: true }) { return; }
4785  }
4786 
4787  CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength));
4788  if (GameMain.NetworkMember is not { IsClient: true } || isNetworkMessage)
4789  {
4790  Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true);
4791  }
4792  if (IsDead)
4793  {
4794  BreakJoints();
4795  }
4796  }
4797 
4798  public void BreakJoints()
4799  {
4800  Vector2 centerOfMass = AnimController.GetCenterOfMass();
4801  foreach (Limb limb in AnimController.Limbs)
4802  {
4803  if (limb.IsSevered) { continue; }
4804  limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false);
4805 
4806  Vector2 diff = centerOfMass - limb.SimPosition;
4807 
4808  if (!MathUtils.IsValid(diff))
4809  {
4810  string errorMsg = "Attempted to apply an invalid impulse to a limb in Character.BreakJoints (" + diff + "). Limb position: " + limb.SimPosition + ", center of mass: " + centerOfMass + ".";
4811  DebugConsole.ThrowError(errorMsg);
4812  GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
4813  return;
4814  }
4815 
4816  if (diff == Vector2.Zero) { continue; }
4817  limb.body.ApplyLinearImpulse(diff * 50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
4818  }
4819 
4820  ImplodeFX();
4821 
4822  foreach (var joint in AnimController.LimbJoints)
4823  {
4824  if (joint.LimbA.type == LimbType.Head || joint.LimbB.type == LimbType.Head) { continue; }
4825  if (joint.revoluteJoint != null)
4826  {
4827  joint.revoluteJoint.LimitEnabled = false;
4828  }
4829  }
4830  }
4831 
4832  partial void ImplodeFX();
4833 
4834  public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
4835  {
4836  if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; }
4837 
4838  HealthUpdateInterval = 0.0f;
4839 
4840  //clients aren't allowed to kill characters unless they receive a network message
4841  if (!isNetworkMessage && GameMain.NetworkMember is { IsClient: true })
4842  {
4843  return;
4844  }
4845 
4846 #if SERVER
4847  if (GameMain.NetworkMember is { IsServer: true })
4848  {
4849  GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData(forceAfflictionData: true));
4850  }
4851 #endif
4852 
4853  AnimController.Frozen = false;
4854 
4855  Character killer = causeOfDeathAffliction?.Source;
4856  if (IsBot)
4857  {
4858  foreach (var item in Inventory.AllItems)
4859  {
4860  if (item.Equipper is { IsPlayer: true } &&
4861  item.GetComponents<ItemContainer>().Any(ic => ic.BlameEquipperForDeath()))
4862  {
4863  killer = item.Equipper;
4864  if (AIController is HumanAIController humanAi)
4865  {
4866  humanAi.OnAttacked(killer, new AttackResult(damage: MaxVitality));
4867  }
4868  break;
4869  }
4870  }
4871  }
4872 
4873  CauseOfDeath = new CauseOfDeath(
4874  causeOfDeath, causeOfDeathAffliction?.Prefab,
4875  killer, LastDamageSource);
4876 
4877  // Save these resistances in the CharacterInfo object so that if they
4878  // are needed for respawning, they will be available (because there
4879  // will be no Character instance in the limbo/bardo state)
4880  if (info != null)
4881  {
4882  info.LastResistanceMultiplierSkillLossDeath = GetAbilityResistance(Tags.SkillLossDeathResistance);
4883  info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance);
4884  }
4885 
4886  //it's important that we set isDead before executing the status effects,
4887  //otherwise a statuseffect might kill the character "again" and trigger a loop that crashes the game
4888  isDead = true;
4889  ApplyStatusEffects(ActionType.OnDeath, 1.0f);
4890 
4891 #if CLIENT
4892  // Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
4893  // NOTE: The opposite is done in Revive
4894  if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
4895  GameMain.Client.Character == this &&
4896  GameMain.Client.CharacterInfo is CharacterInfo characterInfo)
4897  {
4898  characterInfo.PermanentlyDead = true;
4899  }
4900 #endif
4901 
4902 #if SERVER
4903  if (Info is not null)
4904  {
4905  Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution);
4906  }
4907 #endif
4908 
4909  if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
4910  {
4911  string causeOfDeathStr = causeOfDeathAffliction == null ?
4912  causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", "");
4913 
4914  string characterType = GetCharacterType(this);
4915  GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + causeOfDeathStr);
4916  if (CauseOfDeath.Killer != null)
4917  {
4918  GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":Killer:" + GetCharacterType(CauseOfDeath.Killer));
4919  }
4920  if (CauseOfDeath.DamageSource != null)
4921  {
4922  string damageSourceStr = CauseOfDeath.DamageSource.ToString();
4923  if (CauseOfDeath.DamageSource is Item damageSourceItem) { damageSourceStr = damageSourceItem.ToString(); }
4924  GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":DamageSource:" + damageSourceStr);
4925  }
4926 
4927  static string GetCharacterType(Character character)
4928  {
4929  if (character.IsPlayer)
4930  return "Player";
4931  else if (character.AIController is EnemyAIController)
4932  return "Enemy" + character.SpeciesName;
4933  else if (character.AIController is HumanAIController && character.TeamID == CharacterTeamType.Team2)
4934  return "EnemyHuman";
4935  else if (character.Info != null && character.TeamID == CharacterTeamType.Team1)
4936  return "AICrew";
4937  else if (character.Info != null && character.TeamID == CharacterTeamType.FriendlyNPC)
4938  return "FriendlyNPC";
4939  return "Unknown";
4940  }
4941  }
4942 
4943  OnDeath?.Invoke(this, CauseOfDeath);
4944 
4945  if (CauseOfDeath.Type != CauseOfDeathType.Disconnected)
4946  {
4947  var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
4948  CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
4950  }
4951 
4953  {
4954  AchievementManager.OnCharacterKilled(this, CauseOfDeath);
4955  }
4956 
4957  GameMain.LuaCs.Hook.Call("character.death", this, causeOfDeathAffliction);
4958  KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
4959 
4960  if (info != null)
4961  {
4962  info.CauseOfDeath = CauseOfDeath;
4963  info.MissionsCompletedSinceDeath = 0;
4964  }
4965  AnimController.movement = Vector2.Zero;
4966  AnimController.TargetMovement = Vector2.Zero;
4967 
4968  if (!LockHands)
4969  {
4970  foreach (Item heldItem in HeldItems.ToList())
4971  {
4972  //if the item is both wearable and holdable, and currently worn, don't drop the item
4973  var wearable = heldItem.GetComponent<Wearable>();
4974  if (wearable is { IsActive: true }) { continue; }
4975  heldItem.Drop(this);
4976  }
4977  }
4978 
4979  SelectedItem = SelectedSecondaryItem = null;
4980  SelectedCharacter = null;
4981 
4983  if (AnimController.LimbJoints != null)
4984  {
4985  foreach (var joint in AnimController.LimbJoints)
4986  {
4987  if (joint.revoluteJoint != null)
4988  {
4989  joint.revoluteJoint.MotorEnabled = false;
4990  }
4991  }
4992  }
4994  }
4995  partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
4996 
4997  public void Revive(bool removeAfflictions = true, bool createNetworkEvent = false)
4998  {
4999  if (Removed)
5000  {
5001  DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace.CleanupStackTrace());
5002  return;
5003  }
5004 
5005  aiTarget?.Remove();
5006 
5007  aiTarget = new AITarget(this);
5008  if (removeAfflictions)
5009  {
5011  SetAllDamage(0.0f, 0.0f, 0.0f);
5012  Bloodloss = 0.0f;
5013  SetStun(0.0f, true);
5014  }
5015  Oxygen = 100.0f;
5016  isDead = false;
5017 
5018  if (info != null)
5019  {
5020  info.CauseOfDeath = null;
5021 
5022  // Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
5023  // NOTE: The opposite is done in Kill
5024  // FYI: In case you're wondering, it's alright to revive a "permanently" dead character here, because if
5025  // this gets called, the character wasn't actually dead anyway (eg. returning to lobby without saving)
5026  if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath })
5027  {
5028  info.PermanentlyDead = false;
5029  }
5030  }
5031 
5032  foreach (LimbJoint joint in AnimController.LimbJoints)
5033  {
5034  var revoluteJoint = joint.revoluteJoint;
5035  if (revoluteJoint != null)
5036  {
5037  revoluteJoint.MotorEnabled = true;
5038  }
5039  joint.Enabled = true;
5040  joint.IsSevered = false;
5041  }
5042 
5043  foreach (Limb limb in AnimController.Limbs)
5044  {
5045 #if CLIENT
5046  if (limb.LightSource != null)
5047  {
5048  limb.LightSource.Color = limb.InitialLightSourceColor;
5049  }
5050 #endif
5051  limb.body.Enabled = true;
5052  limb.IsSevered = false;
5053  }
5054 
5056  if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
5057  {
5058  GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData());
5059  }
5060  }
5061 
5062  public override void Remove()
5063  {
5064  if (Removed)
5065  {
5066  DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace.CleanupStackTrace());
5067  return;
5068  }
5069  DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")");
5070 
5071 #if CLIENT
5072  //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns
5073  if (GameMain.Client?.ClientPeer is { IsActive: true })
5074  {
5076  }
5077 #endif
5078 
5079  base.Remove();
5080 
5081  foreach (Item heldItem in HeldItems.ToList())
5082  {
5083  heldItem.Drop(this);
5084  }
5085 
5086  info?.Remove();
5087 
5088 #if CLIENT
5089  GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false);
5090 
5091  if (Controlled == this) { Controlled = null; }
5092 #endif
5093 
5094  CharacterList.Remove(this);
5095 
5096  foreach (var attachedProjectile in AttachedProjectiles.ToList())
5097  {
5098  attachedProjectile.Unstick();
5099  }
5100  Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
5101  Latchers.Clear();
5102 
5103  if (Inventory != null)
5104  {
5105  foreach (Item item in Inventory.AllItems)
5106  {
5107  Spawner?.AddItemToRemoveQueue(item);
5108  }
5109  }
5110 
5111  itemSelectedDurations.Clear();
5112 
5113  DisposeProjSpecific();
5114 
5115  aiTarget?.Remove();
5118 
5119  foreach (Character c in CharacterList)
5120  {
5121  if (c.FocusedCharacter == this) { c.FocusedCharacter = null; }
5122  if (c.SelectedCharacter == this) { c.SelectedCharacter = null; }
5123  }
5124  }
5125  partial void DisposeProjSpecific();
5126 
5127  public void TeleportTo(Vector2 worldPos)
5128  {
5129  CurrentHull = null;
5130  AnimController.CurrentHull = null;
5131  Submarine = null;
5132  AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false);
5133  AnimController.FindHull(worldPos, setSubmarine: true);
5134  CurrentHull = AnimController.CurrentHull;
5135  if (AIController is HumanAIController humanAI)
5136  {
5137  humanAI.PathSteering?.ResetPath();
5138  }
5139  }
5140 
5141  public static void SaveInventory(Inventory inventory, XElement parentElement)
5142  {
5143  if (inventory == null || parentElement == null) { return; }
5144  var items = inventory.AllItems.Distinct();
5145  foreach (Item item in items)
5146  {
5147  item.Submarine = inventory.Owner.Submarine;
5148  var itemElement = item.Save(parentElement);
5149 
5150  List<int> slotIndices = inventory.FindIndices(item);
5151  itemElement.Add(new XAttribute("i", string.Join(",", slotIndices)));
5152 
5153  foreach (ItemContainer container in item.GetComponents<ItemContainer>())
5154  {
5155  XElement childInvElement = new XElement("inventory");
5156  itemElement.Add(childInvElement);
5157  SaveInventory(container.Inventory, childInvElement);
5158  }
5159  }
5160  }
5161 
5165  public void SaveInventory()
5166  {
5167  SaveInventory(Inventory, Info?.InventoryData);
5168  }
5169 
5170  public void SpawnInventoryItems(Inventory inventory, ContentXElement itemData)
5171  {
5172  SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
5173  }
5174 
5175  private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List<Item> extraDuffelBags)
5176  {
5177  foreach (var itemElement in element.Elements())
5178  {
5179  var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true, idRemap: IdRemap.DiscardId);
5180  if (newItem == null) { continue; }
5181 
5182  if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) &&
5183  GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
5184  {
5185  newItem.CreateStatusEvent(loadingRound: true);
5186  }
5187 #if SERVER
5188  newItem.GetComponent<Terminal>()?.SyncHistory();
5189  if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
5190  if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
5191 #endif
5192  int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
5193  if (!slotIndices.Any())
5194  {
5195  DebugConsole.ThrowError("Invalid inventory data in character \"" + Name + "\" - no slot indices found");
5196  continue;
5197  }
5198 
5199  bool canBePutInOriginalInventory = true;
5200  if (slotIndices[0] >= inventory.Capacity)
5201  {
5202  canBePutInOriginalInventory = false;
5203  //legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet
5204  //now there's just one, try to put the extra items where they fit (= stack them)
5205  for (int i = 0; i < inventory.Capacity; i++)
5206  {
5207  if (inventory.CanBePutInSlot(newItem, i))
5208  {
5209  slotIndices[0] = i;
5210  canBePutInOriginalInventory = true;
5211  break;
5212  }
5213  }
5214  }
5215  else
5216  {
5217  canBePutInOriginalInventory = inventory.CanBePutInSlot(newItem, slotIndices[0], ignoreCondition: true);
5218  }
5219 
5220  if (canBePutInOriginalInventory)
5221  {
5222  inventory.TryPutItem(newItem, slotIndices[0], false, false, null);
5223  newItem.ParentInventory = inventory;
5224 
5225  //force the item to the correct slots
5226  // e.g. putting the item in a hand slot will also put it in the first available Any-slot,
5227  // which may not be where it actually was
5228  for (int i = 0; i < inventory.Capacity; i++)
5229  {
5230  if (slotIndices.Contains(i))
5231  {
5232  if (!inventory.GetItemsAt(i).Contains(newItem)) { inventory.ForceToSlot(newItem, i); }
5233  }
5234  else if (inventory.FindIndices(newItem).Contains(i))
5235  {
5236  inventory.ForceRemoveFromSlot(newItem, i);
5237  }
5238  }
5239  }
5240  else
5241  {
5242  // In case the inventory capacity is smaller than it was when saving:
5243  // 1) Spawn a new duffel bag if none yet spawned or if the existing ones aren't enough
5244  if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.FindByIdentifier("duffelbag".ToIdentifier()) is ItemPrefab duffelBagPrefab)
5245  {
5246  var hull = Hull.FindHull(WorldPosition, guess: CurrentHull);
5247  var mainSub = Submarine.MainSubs.FirstOrDefault(s => s.TeamID == TeamID);
5248  if ((hull == null || hull.Submarine != mainSub) && mainSub != null)
5249  {
5250  var wp = WayPoint.GetRandom(spawnType: SpawnType.Cargo, sub: mainSub) ?? WayPoint.GetRandom(sub: mainSub);
5251  if (wp != null)
5252  {
5253  hull = Hull.FindHull(wp.WorldPosition);
5254  }
5255  }
5256  var newDuffelBag = new Item(duffelBagPrefab,
5257  hull != null ? CargoManager.GetCargoPos(hull, duffelBagPrefab) : Position,
5258  hull?.Submarine ?? Submarine);
5259  extraDuffelBags.Add(newDuffelBag);
5260 #if SERVER
5261  Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newDuffelBag));
5262 #endif
5263  }
5264 
5265  // 2) Find a slot for the new item
5266  for (int i = 0; i < extraDuffelBags.Count; i++)
5267  {
5268  var duffelBag = extraDuffelBags[i];
5269  for (int j = 0; j < duffelBag.OwnInventory.Capacity; j++)
5270  {
5271  if (duffelBag.OwnInventory.TryPutItem(newItem, j, false, false, null))
5272  {
5273  newItem.ParentInventory = duffelBag.OwnInventory;
5274  break;
5275  }
5276  }
5277  }
5278  }
5279 
5280 #if SERVER
5281  foreach (var circuitBox in newItem.GetComponents<CircuitBox>())
5282  {
5283  circuitBox.MarkServerRequiredInitialization();
5284  }
5285 #endif
5286 
5287  int itemContainerIndex = 0;
5288  var itemContainers = newItem.GetComponents<ItemContainer>().ToList();
5289  foreach (var childInvElement in itemElement.Elements())
5290  {
5291  if (itemContainerIndex >= itemContainers.Count) break;
5292  if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
5293  SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement, extraDuffelBags);
5294  itemContainerIndex++;
5295  }
5296  }
5297  }
5298 
5299  private readonly HashSet<AttackContext> currentContexts = new HashSet<AttackContext>();
5300 
5301  public IEnumerable<AttackContext> GetAttackContexts()
5302  {
5303  currentContexts.Clear();
5304  if (AnimController.InWater)
5305  {
5306  currentContexts.Add(AttackContext.Water);
5307  }
5308  else
5309  {
5310  currentContexts.Add(AttackContext.Ground);
5311  }
5312  if (CurrentHull == null)
5313  {
5314  currentContexts.Add(AttackContext.Outside);
5315  }
5316  else
5317  {
5318  currentContexts.Add(AttackContext.Inside);
5319  }
5320  return currentContexts;
5321  }
5322 
5323  private readonly List<Hull> visibleHulls = new List<Hull>();
5324  private readonly HashSet<Hull> tempList = new HashSet<Hull>();
5325 
5331  public List<Hull> GetVisibleHulls()
5332  {
5333  visibleHulls.Clear();
5334  tempList.Clear();
5335  if (CurrentHull != null)
5336  {
5337  visibleHulls.Add(CurrentHull);
5338  var adjacentHulls = CurrentHull.GetConnectedHulls(true, 1);
5339  float maxDistance = 1000f;
5340  foreach (var hull in adjacentHulls)
5341  {
5342  if (hull.ConnectedGaps.Any(g =>
5343  g.Open > 0.9f &&
5344  g.linkedTo.Contains(CurrentHull) &&
5345  Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
5346  {
5347  if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2))
5348  {
5349  visibleHulls.Add(hull);
5350  }
5351  }
5352  }
5353  visibleHulls.AddRange(CurrentHull.GetLinkedEntities(tempList, filter: h =>
5354  {
5355  // Ignore adjacent hulls because they were already handled above
5356  if (adjacentHulls.Contains(h))
5357  {
5358  return false;
5359  }
5360  else
5361  {
5362  if (h.ConnectedGaps.Any(g =>
5363  g.Open > 0.9f &&
5364  Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) &&
5365  CanSeeTarget(g)))
5366  {
5367  return Vector2.DistanceSquared(h.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2);
5368  }
5369  else
5370  {
5371  return false;
5372  }
5373  }
5374  }));
5375  }
5376  return visibleHulls;
5377  }
5378 
5379  public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => Submarine.GetRelativeSimPosition(this, target, worldPos);
5380 
5381  public bool IsCaptain => HasJob("captain");
5382  public bool IsEngineer => HasJob("engineer");
5383  public bool IsMechanic => HasJob("mechanic");
5384  public bool IsMedic => HasJob("medicaldoctor");
5385  public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer") || HasJob("outpostsecurityofficer");
5386  public bool IsAssistant => HasJob("assistant");
5387  public bool IsWatchman => HasJob("watchman");
5388  public bool IsVip => HasJob("prisoner");
5389  public bool IsPrisoner => HasJob("prisoner");
5390  public bool IsKiller => HasJob("killer");
5391 
5392  public Color? UniqueNameColor { get; set; } = null;
5393 
5394  public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
5395 
5396  public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier;
5397 
5401  public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
5402 
5403  public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure);
5404 
5405 #region Talents
5406  private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
5407 
5408  public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
5409 
5410  public void LoadTalents()
5411  {
5412  List<Identifier> toBeRemoved = null;
5413  foreach (Identifier talent in info.UnlockedTalents)
5414  {
5415  if (!GiveTalent(talent, addingFirstTime: false))
5416  {
5417  DebugConsole.AddWarning(Name + " had talent that did not exist! Removing talent from CharacterInfo.");
5418  toBeRemoved ??= new List<Identifier>();
5419  toBeRemoved.Add(talent);
5420  }
5421  }
5422 
5423  if (toBeRemoved != null)
5424  {
5425  foreach (Identifier removeTalent in toBeRemoved)
5426  {
5427  Info.UnlockedTalents.Remove(removeTalent);
5428  }
5429  }
5430  }
5431 
5432  public bool GiveTalent(Identifier talentIdentifier, bool addingFirstTime = true)
5433  {
5434  TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier == talentIdentifier);
5435  if (talentPrefab == null)
5436  {
5437  DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists.");
5438  return false;
5439  }
5440  return GiveTalent(talentPrefab, addingFirstTime);
5441  }
5442 
5443  public bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime = true)
5444  {
5445  TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UintIdentifier == talentIdentifier);
5446  if (talentPrefab == null)
5447  {
5448  DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists.");
5449  return false;
5450  }
5451  return GiveTalent(talentPrefab, addingFirstTime);
5452  }
5453 
5454  public bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime = true)
5455  {
5456  if (info == null) { return false; }
5457  info.UnlockedTalents.Add(talentPrefab.Identifier);
5458  if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; }
5459 #if SERVER
5460  GameMain.NetworkMember.CreateEntityEvent(this, new UpdateTalentsEventData());
5461 #endif
5462  CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this);
5463  characterTalents.Add(characterTalent);
5464  characterTalent.ActivateTalent(addingFirstTime);
5465  characterTalent.AddedThisRound = addingFirstTime;
5466 
5467  if (addingFirstTime)
5468  {
5469  OnTalentGiven(talentPrefab);
5470  GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None".ToIdentifier()) + ":" + talentPrefab.Identifier,
5472  }
5473  return true;
5474  }
5475 
5476  public bool HasTalent(Identifier identifier)
5477  {
5478  if (info == null) { return false; }
5479  return info.UnlockedTalents.Contains(identifier);
5480  }
5481 
5483  {
5484  if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
5485  {
5486  foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
5487  {
5488  foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
5489  {
5490  if (!talentOption.HasMaxTalents(info.UnlockedTalents))
5491  {
5492  return false;
5493  }
5494  }
5495  }
5496  }
5497  return true;
5498  }
5499 
5500  public bool HasTalents()
5501  {
5502  return characterTalents.Any();
5503  }
5504 
5505  public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
5506  {
5507  foreach (CharacterTalent characterTalent in CharacterTalents)
5508  {
5509  characterTalent.CheckTalent(abilityEffectType, abilityObject);
5510  }
5511  }
5512 
5513  public void CheckTalents(AbilityEffectType abilityEffectType)
5514  {
5515  foreach (var characterTalent in characterTalents)
5516  {
5517  characterTalent.CheckTalent(abilityEffectType, null);
5518  }
5519  }
5520 
5521  partial void OnTalentGiven(TalentPrefab talentPrefab);
5522 
5523 #endregion
5524 
5525  private readonly HashSet<Hull> sameRoomHulls = new();
5526 
5531  public bool IsInSameRoomAs(Character character)
5532  {
5533  if (character == this) { return true; }
5534 
5535  if (character.CurrentHull is null || CurrentHull is null)
5536  {
5537  // Outside doesn't count as a room
5538  return false;
5539  }
5540 
5541  if (character.Submarine != Submarine) { return false; }
5542  if (character.CurrentHull == CurrentHull) { return true; }
5543 
5544  sameRoomHulls.Clear();
5545  CurrentHull.GetLinkedEntities(sameRoomHulls);
5546  sameRoomHulls.Add(CurrentHull);
5547 
5548  return sameRoomHulls.Contains(character.CurrentHull);
5549  }
5550 
5551  public static IEnumerable<Character> GetFriendlyCrew(Character character)
5552  {
5553  if (character is null)
5554  {
5555  return Enumerable.Empty<Character>();
5556  }
5557  return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead);
5558  }
5559 
5560  public bool HasRecipeForItem(Identifier recipeIdentifier)
5561  {
5562  return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
5563  }
5564 
5565  public bool HasStoreAccessForItem(ItemPrefab prefab)
5566  {
5567  foreach (CharacterTalent talent in characterTalents)
5568  {
5569  foreach (Identifier unlockedItem in talent.UnlockedStoreItems)
5570  {
5571  if (prefab.Tags.Contains(unlockedItem)) { return true; }
5572  }
5573  }
5574 
5575  return false;
5576  }
5577 
5581  public void GiveMoney(int amount)
5582  {
5583  if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
5584  if (amount <= 0) { return; }
5585 
5586  Wallet wallet;
5587 #if SERVER
5588  if (!(campaign is MultiPlayerCampaign mpCampaign)) { throw new InvalidOperationException("Campaign on a server is not a multiplayer campaign"); }
5589  Client targetClient = null;
5590 
5591  foreach (Client client in GameMain.Server.ConnectedClients)
5592  {
5593  if (client.Character == this)
5594  {
5595  targetClient = client;
5596  break;
5597  }
5598  }
5599 
5600  wallet = targetClient is null ? mpCampaign.Bank : mpCampaign.GetWallet(targetClient);
5601 #else
5602  wallet = campaign.Wallet;
5603 #endif
5604 
5605  int prevAmount = wallet.Balance;
5606  wallet.Give(amount);
5607  OnMoneyChanged(prevAmount, wallet.Balance);
5608  }
5609 
5610 #if CLIENT
5611  public void SetMoney(int amount)
5612  {
5613  if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
5614  if (amount == campaign.Wallet.Balance) { return; }
5615 
5616  int prevAmount = campaign.Wallet.Balance;
5617  campaign.Wallet.Balance = amount;
5618  OnMoneyChanged(prevAmount, campaign.Wallet.Balance);
5619  }
5620 #endif
5621 
5622  partial void OnMoneyChanged(int prevAmount, int newAmount);
5623 
5628  private readonly Dictionary<StatTypes, float> statValues = new Dictionary<StatTypes, float>();
5629 
5633  private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
5634 
5635  public float GetStatValue(StatTypes statType, bool includeSaved = true)
5636  {
5637  if (!IsHuman) { return 0f; }
5638 
5639  float statValue = 0f;
5640  if (statValues.TryGetValue(statType, out float value))
5641  {
5642  statValue += value;
5643  }
5644  if (CharacterHealth != null)
5645  {
5646  statValue += CharacterHealth.GetStatValue(statType);
5647  }
5648  if (Info != null && includeSaved)
5649  {
5650  // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
5651  statValue += Info.GetSavedStatValue(statType);
5652  }
5653  if (wearableStatValues.TryGetValue(statType, out float wearableValue))
5654  {
5655  statValue += wearableValue;
5656  }
5657  foreach (var heldItem in HeldItems)
5658  {
5659  if (heldItem.GetComponent<Holdable>() is Holdable holdable &&
5660  holdable.HoldableStatValues.TryGetValue(statType, out float holdableValue))
5661  {
5662  statValue += holdableValue;
5663  }
5664  }
5665  return statValue;
5666  }
5667 
5668  public void OnWearablesChanged()
5669  {
5670  wearableStatValues.Clear();
5671  for (int i = 0; i < Inventory.Capacity; i++)
5672  {
5673  if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
5674  && Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
5675  {
5676  foreach (var statValuePair in wearable.WearableStatValues)
5677  {
5678  if (wearableStatValues.ContainsKey(statValuePair.Key))
5679  {
5680  wearableStatValues[statValuePair.Key] += statValuePair.Value;
5681  }
5682  else
5683  {
5684  wearableStatValues.Add(statValuePair.Key, statValuePair.Value);
5685  }
5686  }
5687  }
5688  }
5689  }
5690 
5691  public void ChangeStat(StatTypes statType, float value)
5692  {
5693  if (statValues.ContainsKey(statType))
5694  {
5695  statValues[statType] += value;
5696  }
5697  else
5698  {
5699  statValues.Add(statType, value);
5700  }
5701  }
5702 
5703  private static StatTypes GetSkillStatType(Identifier skillIdentifier)
5704  {
5705  // Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way
5706  switch (skillIdentifier.Value.ToLowerInvariant())
5707  {
5708  case "electrical":
5709  return StatTypes.ElectricalSkillBonus;
5710  case "helm":
5711  return StatTypes.HelmSkillBonus;
5712  case "mechanical":
5713  return StatTypes.MechanicalSkillBonus;
5714  case "medical":
5715  return StatTypes.MedicalSkillBonus;
5716  case "weapons":
5717  return StatTypes.WeaponsSkillBonus;
5718  default:
5719  return StatTypes.None;
5720  }
5721  }
5722 
5723  private AbilityFlags abilityFlags;
5724 
5725  public void AddAbilityFlag(AbilityFlags abilityFlag)
5726  {
5727  abilityFlags |= abilityFlag;
5728  }
5729 
5730  public void RemoveAbilityFlag(AbilityFlags abilityFlag)
5731  {
5732  abilityFlags &= ~abilityFlag;
5733  }
5734 
5735  public bool HasAbilityFlag(AbilityFlags abilityFlag)
5736  {
5737  return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
5738  }
5739 
5740  private readonly Dictionary<TalentResistanceIdentifier, float> abilityResistances = new();
5741 
5742  public float GetAbilityResistance(Identifier resistanceId)
5743  {
5744  float resistance = 0f;
5745  bool hadResistance = false;
5746 
5747  foreach (var (key, value) in abilityResistances)
5748  {
5749  if (key.ResistanceIdentifier == resistanceId)
5750  {
5751  resistance += value;
5752  hadResistance = true;
5753  }
5754  }
5755 
5756  // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
5757  return hadResistance ? resistance : 1f;
5758  }
5759 
5760  public float GetAbilityResistance(AfflictionPrefab affliction)
5761  {
5762  float resistance = 0f;
5763  bool hadResistance = false;
5764 
5765  foreach (var (key, value) in abilityResistances)
5766  {
5767  if (key.ResistanceIdentifier == affliction.AfflictionType ||
5768  key.ResistanceIdentifier == affliction.Identifier)
5769  {
5770  resistance += value;
5771  hadResistance = true;
5772  }
5773  }
5774 
5775  // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
5776  return hadResistance ? resistance : 1f;
5777  }
5778 
5779  public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
5780  {
5781  if (!MathUtils.IsValid(value))
5782  {
5783 #if DEBUG
5784  DebugConsole.ThrowError($"Attempted to set ability resistance to an invalid value ({value})\n" + Environment.StackTrace.CleanupStackTrace());
5785 #endif
5786  return;
5787  }
5788 
5789  if (abilityResistances.ContainsKey(identifier))
5790  {
5791  abilityResistances[identifier] *= value;
5792  }
5793  else
5794  {
5795  abilityResistances.Add(identifier, value);
5796  }
5797  }
5798 
5799  public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
5800 
5801  public bool IsFriendly(Character other) => IsFriendly(this, other);
5802 
5803  public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
5804 
5805  public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
5806  {
5807  if (myTeam == otherTeam) { return true; }
5808  return myTeam switch
5809  {
5810  // NPCs are friendly to the same team and the friendly NPCs
5811  CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
5812  // Friendly NPCs are friendly to both player teams
5813  CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
5814  _ => true
5815  };
5816  }
5817 
5818  public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
5819  public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID);
5820  public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam);
5821 
5822  public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
5823 
5824  public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
5825 
5826  public void StopClimbing()
5827  {
5829  ReleaseSecondaryItem();
5830  }
5831  }
5832 
5834  {
5835  public CharacterTeamType DesiredTeamId { get; }
5837  {
5838  Base, // given to characters when generated or when their base team is set
5839  Willful, // cognitive, willful team changes, such as prisoners escaping
5840  Absolute // possession, insanity, the like
5841  }
5842  public TeamChangePriorities TeamChangePriority { get; }
5843  public bool AggressiveBehavior { get; }
5844 
5845  public ActiveTeamChange(CharacterTeamType desiredTeamId, TeamChangePriorities teamChangePriority, bool aggressiveBehavior = false)
5846  {
5847  DesiredTeamId = desiredTeamId;
5848  TeamChangePriority = teamChangePriority;
5849  AggressiveBehavior = aggressiveBehavior;
5850  }
5851  }
5852 
5853  internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter
5854  {
5855  public Character Character { get; set; }
5856 
5857  public AbilityCharacterLoot(Character character)
5858  {
5859  Character = character;
5860  }
5861  }
5862 
5864  {
5865  public AbilityCharacterKill(Character character, Character killer)
5866  {
5867  Character = character;
5868  Killer = killer;
5869  }
5870  public Character Character { get; set; }
5871  public Character Killer { get; set; }
5872  }
5873 
5875  {
5876  public float DamageMultiplier { get; set; } = 1f;
5877  public float AddedPenetration { get; set; } = 0f;
5878  public List<Affliction> Afflictions { get; set; }
5879  public bool ShouldImplode { get; set; } = false;
5880  public Attack SourceAttack { get; }
5881  public Character Character { get; set; }
5882  public Character Attacker { get; set; }
5883 
5884  public AbilityAttackData(Attack sourceAttack, Character target, Character attacker)
5885  {
5886  SourceAttack = sourceAttack;
5887  Character = target;
5888  if (attacker != null)
5889  {
5890  Attacker = attacker;
5891  attacker.CheckTalents(AbilityEffectType.OnAttack, this);
5892  target.CheckTalents(AbilityEffectType.OnAttacked, this);
5893  DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.AttackMultiplier);
5894  if (attacker.TeamID == target.TeamID)
5895  {
5896  DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.TeamAttackMultiplier);
5897  }
5898  }
5899  }
5900  }
5901 
5903  {
5904  public AttackResult AttackResult { get; set; }
5905 
5906  public AbilityAttackResult(AttackResult attackResult)
5907  {
5908  AttackResult = attackResult;
5909  }
5910  }
5911 
5913  {
5915  {
5916  Character = character;
5917  }
5918  public Character Character { get; set; }
5919  }
5920 
5922  {
5924  {
5925  Character = character;
5926  }
5927  public Character Character { get; set; }
5928  }
5929 
5931  {
5933  {
5934  Item = item;
5935  }
5936  public Item Item { get; set; }
5937  }
5938 }
static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier=3, float maxDistance=10000.0f, float factorAtMinDistance=1.0f)
Get a normalized value representing how close the target position is. The value is a rough estimation...
AbilityAttackData(Attack sourceAttack, Character target, Character attacker)
ActiveTeamChange(CharacterTeamType desiredTeamId, TeamChangePriorities teamChangePriority, bool aggressiveBehavior=false)
override string ToString()
virtual float Strength
Definition: Affliction.cs:31
float GetSkillMultiplier()
Definition: Affliction.cs:315
Character Source
Which character gave this affliction
Definition: Affliction.cs:88
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
A special affliction type that gradually makes the character turn into another type of character....
static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab)
static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab)
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static readonly Identifier BleedingType
static readonly Identifier DamageType
static AfflictionPrefab OrganDamage
static readonly Identifier ParalysisType
readonly Identifier AfflictionType
Arbitrary string that is used to identify the type of the affliction.
static readonly PrefabCollection< AfflictionPrefab > Prefabs
static readonly Identifier EMPType
static readonly Identifier PoisonType
AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection.
AnimationType ForceSelectAnimationType
float GetCurrentSpeed(bool useMaxSpeed)
void UpdateAnimations(float deltaTime)
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float ImpactMultiplier
Used for multiplying the physics forces.
float DamageMultiplier
Used for multiplying all the damage.
readonly Dictionary< Affliction, XElement > Afflictions
Vector2 GetPosition()
Definition: Camera.cs:158
Vector2 Position
Definition: Camera.cs:398
readonly Character Killer
Definition: CauseOfDeath.cs:14
readonly CauseOfDeathType Type
Definition: CauseOfDeath.cs:12
readonly Entity DamageSource
Definition: CauseOfDeath.cs:15
static void ShowBossHealthBar(Character character, float damage)
void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking=true, bool ignoreUnkillability=false)
void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions=true)
void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking=true)
bool WasInFullHealth
Was the character in full health at the beginning of the frame?
void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions=true)
static IEnumerable< Character > GetFriendlyCrew(Character character)
bool IsItemTakenBySomeoneElse(Item item)
void SetCustomInteract(Action< Character, Character > onCustomInteract, LocalizedString hudText)
Set an action that's invoked when another character interacts with this one.
static bool IsFriendly(Character me, Character other)
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
void SetInput(InputType inputType, bool hit, bool held)
static bool IsSameSpeciesOrGroup(Character me, Character other)
bool CanInteractWith(Item item, out float distanceToItem, bool checkLinked)
float GetSkillLevel(string skillIdentifier)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
void ReloadHead(int? headId=null, int hairIndex=-1, int beardIndex=-1, int moustacheIndex=-1, int faceAttachmentIndex=-1)
readonly Dictionary< Identifier, SerializableProperty > Properties
void AddAttacker(Character character, float damage)
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
bool IsRemotelyControlled
Is the character controlled remotely (either by another player, or a server-side AIController)
bool HasJob(string identifier)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, bool createNetworkEvent=true, RagdollParams ragdoll=null, bool throwErrorIfNotFound=true, bool spawnInitialItems=true)
delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath)
bool DisableFocusingOnEntities
Prevents the character from highlighting items or characters with the cursor, meaning it can't intera...
bool CanInteractWith(Item item, bool checkLinked=true)
bool IsInSameRoomAs(Character character)
Check if the character is in the same room Room and hull differ in that a room can consist of multipl...
void Revive(bool removeAfflictions=true, bool createNetworkEvent=false)
AttackResult AddDamage(Vector2 worldPosition, IEnumerable< Affliction > afflictions, float stun, bool playSound, Vector2 attackImpulse, out Limb hitLimb, Character attacker=null, float damageMultiplier=1)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
static bool IsOnFriendlyTeam(Character me, Character other)
void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier=false, Character attacker=null)
static void RemoveByPrefab(CharacterPrefab prefab)
static void SaveInventory(Inventory inventory, XElement parentElement)
bool IsCriminal
Do the outpost security officers treat the character as a criminal? Triggers when the character has e...
bool IsFacing(Vector2 targetWorldPos)
A simple check if the character Dir is towards the target or not. Uses the world coordinates.
Dictionary< Identifier, SerializableProperty > SerializableProperties
float GetStatValue(StatTypes statType, bool includeSaved=true)
void ApplyStatusEffects(ActionType actionType, float deltaTime)
void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent=false)
void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos)
bool IsFriendly(Character other)
float GetLeftHandPenalty()
virtual void Update(float deltaTime, Camera cam)
bool CanBeHealedBy(Character character, bool checkFriendlyTeam=true)
void RemoveAbilityResistance(TalentResistanceIdentifier identifier)
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
delegate void OnAttackedHandler(Character attacker, AttackResult attackResult)
void GiveMoney(int amount)
Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gai...
static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows=false, bool checkFacing=false)
void ChangeStat(StatTypes statType, float value)
bool IsSameSpeciesOrGroup(Character other)
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
bool TryAddNewTeamChange(string identifier, ActiveTeamChange newTeamChange)
void TryAdjustHealerSkill(Character healer, float healthChange=0, Affliction affliction=null)
AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound=false, Limb targetLimb=null, float penetration=0f)
Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to...
bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel=CharacterInventory.AccessLevel.Limited)
bool IsOnFriendlyTeam(Character other)
bool IsOnFriendlyTeam(CharacterTeamType otherTeam)
bool HasItem(Item item, bool requireEquipped=false, InvSlotType? slotType=null)
bool HasSelectedAnotherSecondaryItem(Item item)
Item GetEquippedItem(Identifier tagOrIdentifier=default, InvSlotType? slotType=null)
bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken=true, InvSlotType? slotType=null)
void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
bool DisabledByEvent
MonsterEvents disable monsters (which includes removing them from the character list,...
void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult)
void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
float GetRightHandPenalty()
bool IsAnySelectedItem(Item item)
Is the item either the primary or the secondary selected item?
bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable< Identifier > identifiers=null, bool ignoreBroken=true, IEnumerable< Item > ignoredItems=null, IEnumerable< Identifier > ignoredContainerIdentifiers=null, Func< Item, bool > customPredicate=null, Func< Item, float > customPriorityFunction=null, float maxItemDistance=10000, ISpatialEntity positionalReference=null)
Finds the closest item seeking by identifiers or tags from the world. Ignores items that are outside ...
float HumanPrefabHealthMultiplier
Health multiplier of the human prefab this character is an instance of (if any)
static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, bool createNetworkEvent=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime=true)
AttackResult AddDamage(Vector2 worldPosition, IEnumerable< Affliction > afflictions, float stun, bool playSound, Vector2? attackImpulse=null, Character attacker=null, float damageMultiplier=1f)
void CheckTalents(AbilityEffectType abilityEffectType)
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
readonly Dictionary< string, ActiveTeamChange > activeTeamChanges
AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable< Affliction > afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker=null, float damageMultiplier=1, bool allowStacking=true, float penetration=0f, bool shouldImplode=false)
Identifier GetBaseCharacterSpeciesName()
bool HasJob(Identifier identifier)
bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel=CharacterInventory.AccessLevel.Limited)
Is the inventory accessible to the character? Doesn't check if the character can actually interact wi...
float GetTemporarySpeedReduction()
Speed reduction from the current limb specific damage. Min 0, max 1.
bool IsCommanding
Is the character player or does it have an active ship command manager (an AI controlled sub)?...
static void UpdateAll(float deltaTime, Camera cam)
bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime=true)
void SetOrder(Order order, bool isNewOrder, bool speak=true, bool force=false)
Force an order to be set for the character, bypassing hearing checks
Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
void SaveInventory()
Calls SaveInventory(Barotrauma.Inventory, XElement) using 'Inventory' and 'Info.InventoryData'
float GetDistanceSqrToClosestPlayer()
How far the character is from the closest human player (including spectators)
void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound=true)
List< Hull > GetVisibleHulls()
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
Dictionary< ItemPrefab, double > ItemSelectedDurations
bool DisableInteract
Prevents the character from interacting with items or characters
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, bool createNetworkEvent=true, RagdollParams ragdoll=null, bool throwErrorIfNotFound=true, bool spawnInitialItems=true)
Create a new character
float GetDistanceToClosestPlayer()
How far the character is from the closest human player (including spectators)
bool GiveTalent(Identifier talentIdentifier, bool addingFirstTime=true)
void SpawnInventoryItems(Inventory inventory, ContentXElement itemData)
float GetAbilityResistance(AfflictionPrefab affliction)
bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity=null, bool seeThroughWindows=false, bool checkFacing=false)
bool IsRemotePlayer
Is the character controlled by another human player (should always be false in single player)
Stores information about the Character that is needed between rounds in the menu etc....
void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility=false, float maxGain=2f)
Increases the characters skill at a rate proportional to their current skill. If you want to increase...
void CheckDisguiseStatus(bool handleBuff, IdCard idCard=null)
AccessLevel
How much access other characters have to the inventory? Restricted = Only accessible when character i...
Contains character data that should be editable in the character editor.
AIParams AI
Parameters for EnemyAIController. Not used by HumanAIController.
readonly CharacterFile File
static bool CompareGroup(Identifier group1, Identifier group2)
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
ContentXElement ConfigElement
static readonly Identifier HumanSpeciesName
void ActivateTalent(bool addingFirstTime)
void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
Makes an NPC switch to a combat state (with options for different kinds of behaviors,...
Definition: CombatAction.cs:11
readonly ContentPath Path
Definition: ContentFile.cs:137
string???????????? Value
Definition: ContentPath.cs:27
IEnumerable< ContentXElement > Elements()
ContentXElement? GetChildElement(string name)
Triggers a "conversation popup" with text and support for different branching options.
void KillCharacter(Character killedCharacter, bool resetCrewListIndex=true)
AITarget AiTarget
Definition: Entity.cs:55
static EntitySpawner Spawner
Definition: Entity.cs:31
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
const ushort NullEntityID
Definition: Entity.cs:14
static GameSession?? GameSession
Definition: GameMain.cs:88
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static bool IsSingleplayer
Definition: GameMain.cs:34
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
static bool IsActive(Character c)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
float HealthMultiplierInMultiplayer
Definition: HumanPrefab.cs:22
static readonly IdRemap DiscardId
Definition: IdRemap.cs:12
void ForceRemoveFromSlot(Item item, int index)
Removes an item from a specific slot. Doesn't do any sanity checks, use with caution!
virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition=false)
Can the item be put in the specified slot.
virtual bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
List< int > FindIndices(Item item)
Find the indices of all the slots the item is contained in (two-hand items for example can be in mult...
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
void ForceToSlot(Item item, int index)
Forces an item to a specific slot. Doesn't remove the item from existing slots/inventories or do any ...
IEnumerable< Item > GetItemsAt(int index)
Get all the item stored in the specified inventory slot. Can return more than one item if the slot co...
Item GetItemAt(int index)
Get the item stored in the specified inventory slot. If the slot contains a stack of items,...
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
bool IsShootable
Should the item's Use method be called with the "Use" or with the "Shoot" key?
bool RequireAimToSecondaryUse
If true, the user has to hold the "aim" key before secondary use is registered. True by default.
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
Rectangle TransformTrigger(Rectangle trigger, bool world=false)
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
void SecondaryUse(float deltaTime, Character character=null)
static Item Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
override XElement Save(XElement parentElement)
bool IsInsideTrigger(Vector2 worldPosition)
bool RequireAimToUse
If true, the user has to hold the "aim" key before use is registered. False by default.
static readonly List< Item > ItemList
Dictionary< Identifier, SerializableProperty > SerializableProperties
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
CampaignMode.InteractionType CampaignInteractionType
ImmutableArray< Rectangle > Triggers
Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true,...
The base class for components holding the different functionalities of the item
override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
void GiveJobItems(Character character, WayPoint spawnPoint=null)
Definition: Job.cs:131
JobPrefab Prefab
Definition: Job.cs:18
LatchOntoAI(XElement element, EnemyAIController enemyAI)
Definition: LatchOntoAI.cs:63
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
AttackResult AddDamage(Vector2 simPosition, float damage, float bleedingDamage, float burnDamage, bool playSound)
void ApplyStatusEffects(ActionType actionType, float deltaTime)
bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance=-1, Limb targetLimb=null)
Returns true if the attack successfully hit something. If the distance is not given,...
readonly List< WearableSprite > OtherWearables
object Call(string name, params object[] args)
HashSet< Character > UpdatePriorityCharacters
Definition: LuaGame.cs:176
Mersenne Twister based random
Definition: MTRandom.cs:9
bool IsHidden
Is the entity hidden due to HiddenInGame being enabled or the layer the entity is in being hidden?
static bool CanUseRadio(Character sender, bool ignoreJamming=false)
readonly Entity TargetEntity
Definition: Order.cs:495
bool AutoDismiss
Definition: Order.cs:564
readonly Character OrderGiver
Definition: Order.cs:499
readonly Identifier Option
Definition: Order.cs:482
readonly int ManualPriority
Definition: Order.cs:483
OrderCategory? Category
Definition: Order.cs:556
Identifier Identifier
Definition: Order.cs:540
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
void Play(Character player)
Definition: PetBehavior.cs:283
readonly ContentFile ContentFile
Definition: Prefab.cs:35
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
void FindHull(Vector2? worldPosition=null, bool setSubmarine=true)
void ResetPullJoints(Func< Limb, bool > condition=null)
bool IsHoldingToRope
Is attached to something with a rope.
bool IsHangingWithRope
Is hanging to something with a rope, so that can reel towards it. Currently only possible in water.
void SetPosition(Vector2 simPosition, bool lerp=false, bool ignorePlatforms=true, bool forceMainLimbToCollider=false, bool moveLatchers=true)
Limb GetLimb(LimbType limbType, bool excludeSevered=true)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
static Dictionary< Identifier, SerializableProperty > GetProperties(object obj)
static SkillSettings Current
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static StatusEffect Load(ContentXElement element, string parentDebugName)
void AddNearbyTargets(Vector2 worldPosition, List< ISerializableEntity > targets)
readonly bool OnlyWhenDamagedByPlayer
If enabled, the effect only executes when the entity receives damage from a player character (a chara...
readonly LimbType[] targetLimbs
Which types of limbs this effect can target? Only valid when targeting characters or limbs.
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? targetWorldPos=null)
static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel=false, bool ignoreSubs=false, bool ignoreSensors=true, bool ignoreDisabledWalls=true, bool ignoreBranches=true, Predicate< Fixture > blocksVisibilityPredicate=null)
Check visibility between two points (in sim units).
bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam=false, bool allowDifferentType=false)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
static readonly PrefabCollection< TalentPrefab > TalentPrefabs
Definition: TalentPrefab.cs:35
Interface for entities that the clients can send events to the server
Interface for entities that handle ServerNetObject.ENTITY_POSITION
OrderCategory
Definition: Order.cs:12
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
Definition: Enums.cs:615
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
@ InWater
Executes continuously when the entity is submerged. Valid for items and characters.
@ OnDeath
Executes when the character dies. Only valid for characters.
AbilityEffectType
Definition: Enums.cs:125
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180
readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier)