Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Characters/Character.cs
5 using Barotrauma.Sounds;
6 using FarseerPhysics;
7 using FarseerPhysics.Dynamics;
8 using Microsoft.Xna.Framework;
9 using Microsoft.Xna.Framework.Graphics;
10 using System;
11 using System.Collections.Generic;
12 using System.Linq;
13 
14 namespace Barotrauma
15 {
16  partial class Character
17  {
18  public static bool DisableControls;
19 
20  public static bool DebugDrawInteract;
21 
22  protected float soundTimer;
23  protected float hudInfoTimer = 1.0f;
24  protected bool hudInfoVisible = false;
25 
26  private float findFocusedTimer;
27 
28  protected float lastRecvPositionUpdateTime;
29 
30  private float hudInfoHeight = 100.0f;
31 
32  private List<CharacterSound> sounds;
33 
34  public bool ExternalHighlight;
35 
39  public bool IsVisible
40  {
41  get;
42  private set;
43  } = true;
44 
45  public bool ShowInteractionLabels { get; private set; }
46 
47  //the Character that the player is currently controlling
48  private static Character controlled;
49 
50  public static Character Controlled
51  {
52  get { return controlled; }
53  set
54  {
55  if (controlled == value) return;
56  if ((!(controlled is null)) && (!(Screen.Selected?.Cam is null)) && value is null)
57  {
58  Screen.Selected.Cam.TargetPos = Vector2.Zero;
59  Lights.LightManager.ViewTarget = null;
60  }
61  controlled = value;
62  if (controlled != null) controlled.Enabled = true;
64  }
65  }
66 
67  private Dictionary<object, HUDProgressBar> hudProgressBars;
68  private readonly List<KeyValuePair<object, HUDProgressBar>> progressBarRemovals = new List<KeyValuePair<object, HUDProgressBar>>();
69 
70  public Dictionary<object, HUDProgressBar> HUDProgressBars
71  {
72  get { return hudProgressBars; }
73  }
74 
75  private float blurStrength;
76  public float BlurStrength
77  {
78  get { return blurStrength; }
79  set { blurStrength = MathHelper.Clamp(value, 0.0f, 1.0f); }
80  }
81 
82  private float distortStrength;
83  public float DistortStrength
84  {
85  get { return distortStrength; }
86  set { distortStrength = MathHelper.Clamp(value, 0.0f, 1.0f); }
87  }
88 
89  private float radialDistortStrength;
90  public float RadialDistortStrength
91  {
92  get { return radialDistortStrength; }
93  set { radialDistortStrength = MathHelper.Clamp(value, 0.0f, 100.0f); }
94  }
95 
96  private float chromaticAberrationStrength;
98  {
99  get { return chromaticAberrationStrength; }
100  set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); }
101  }
102 
103  public Color GrainColor { get; set; }
104 
105  private float grainStrength;
106  public float GrainStrength
107  {
108  get => grainStrength;
109  set => grainStrength = Math.Max(0, value);
110  }
111 
116  {
117  get { return Level.Loaded?.Renderer?.CollapseEffectStrength ?? 0.0f; }
118  set
119  {
120  if (Level.Loaded?.Renderer == null) { return; }
121  if (Controlled == this)
122  {
123  float strength = MathHelper.Clamp(value, 0.0f, 1.0f);
126  Screen.Selected.Cam.Shake = Math.Max(MathF.Pow(strength, 3) * 100.0f, Screen.Selected.Cam.Shake);
127  Screen.Selected.Cam.Rotation = strength * (PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, (float)Timing.TotalTime * 0.05f) - 0.5f);
129  }
130  }
131  }
135  public float CameraShake
136  {
137  get { return Screen.Selected?.Cam?.Shake ?? 0.0f; }
138  set
139  {
140  if (!MathUtils.IsValid(value)) { return; }
141  if (this != Controlled) { return; }
142  if (Screen.Selected?.Cam != null)
143  {
144  Screen.Selected.Cam.Shake = value;
145  }
146  }
147  }
148 
149  private readonly List<ParticleEmitter> bloodEmitters = new List<ParticleEmitter>();
150  public IEnumerable<ParticleEmitter> BloodEmitters
151  {
152  get { return bloodEmitters; }
153  }
154 
155  private readonly List<ParticleEmitter> damageEmitters = new List<ParticleEmitter>();
156  public IEnumerable<ParticleEmitter> DamageEmitters
157  {
158  get { return damageEmitters; }
159  }
160 
161  private readonly List<ParticleEmitter> gibEmitters = new List<ParticleEmitter>();
162  public IEnumerable<ParticleEmitter> GibEmitters
163  {
164  get { return gibEmitters; }
165  }
166 
167  private class GUIMessage
168  {
169  public string RawText;
170  public Identifier Identifier;
171  public string Text;
172 
173  private int _value;
174  public int Value
175  {
176  get { return _value; }
177  set
178  {
179  _value = value;
180  Text = RawText.Replace("[value]", _value.ToString());
181  Size = GUIStyle.Font.MeasureString(Text);
182  }
183  }
184 
185  public Color Color;
186  public float Lifetime;
187  public float Timer;
188 
189  public Vector2 Size;
190 
191  public bool PlaySound;
192 
193  public GUIMessage(string rawText, Color color, float delay, Identifier identifier = default, int? value = null, float lifeTime = 3.0f)
194  {
195  RawText = Text = rawText;
196  if (value.HasValue)
197  {
198  Text = rawText.Replace("[value]", value.Value.ToString());
199  Value = value.Value;
200  }
201  Timer = -delay;
202  Size = GUIStyle.Font.MeasureString(Text);
203  Color = color;
204  Identifier = identifier;
205  Lifetime = lifeTime;
206  }
207  }
208 
209  private List<GUIMessage> guiMessages = new List<GUIMessage>();
210 
211  public static bool IsMouseOnUI => GUI.MouseOn != null ||
213 
214  public class ObjectiveEntity
215  {
216  public Entity Entity;
217  public Sprite Sprite;
218  public Color Color;
219 
220  public ObjectiveEntity(Entity entity, Sprite sprite, Color? color = null)
221  {
222  Entity = entity;
223  Sprite = sprite;
224  if (color.HasValue)
225  {
226  Color = color.Value;
227  }
228  else
229  {
230  Color = Color.White;
231  }
232  }
233  }
234 
235  private readonly List<ObjectiveEntity> activeObjectiveEntities = new List<ObjectiveEntity>();
236  public IEnumerable<ObjectiveEntity> ActiveObjectiveEntities
237  {
238  get { return activeObjectiveEntities; }
239  }
240 
241  private static readonly List<SpeechBubble> speechBubbles = new List<SpeechBubble>();
242 
243  private SpeechBubble textlessSpeechBubble;
244 
245  sealed class SpeechBubble
246  {
247  public float LifeTime;
248  public Vector2 PrevPosition;
249  public Vector2 Position;
250  public Vector2 DrawPosition;
251  public float MoveUpAmount;
252  public readonly string Text;
253  public readonly Character Character;
254  public readonly Submarine Submarine;
255  public readonly Vector2 TextSize;
256 
257  public Color Color;
258  public bool Moving;
259 
260  public SpeechBubble(Character character, float lifeTime, Color color, string text = "")
261  {
262  Text = ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text));
263  TextSize = GUIStyle.SmallFont.MeasureString(Text);
264 
265  Character = character;
266  Position = GetDesiredPosition();
267  Submarine = character.Submarine;
268  LifeTime = lifeTime;
269  Color = color;
270  }
271 
272  public Vector2 GetDesiredPosition()
273  {
274  return Character.Position + Vector2.UnitY * 100;
275  }
276  }
277 
278  private float pressureEffectTimer;
279 
280  partial void InitProjSpecific(ContentXElement mainElement)
281  {
282  soundTimer = Rand.Range(0.0f, Params.SoundInterval);
283 
284  sounds = new List<CharacterSound>();
285  Params.Sounds.ForEach(s => sounds.Add(new CharacterSound(s)));
286 
287  foreach (var subElement in mainElement.Elements())
288  {
289  switch (subElement.Name.ToString().ToLowerInvariant())
290  {
291  case "damageemitter":
292  damageEmitters.Add(new ParticleEmitter(subElement));
293  break;
294  case "bloodemitter":
295  bloodEmitters.Add(new ParticleEmitter(subElement));
296  break;
297  case "gibemitter":
298  gibEmitters.Add(new ParticleEmitter(subElement));
299  break;
300  }
301  }
302 
303  hudProgressBars = new Dictionary<object, HUDProgressBar>();
304  }
305 
306  partial void UpdateLimbLightSource(Limb limb)
307  {
308  if (limb.LightSource != null)
309  {
310  limb.LightSource.Enabled = enabled;
311  }
312  }
313 
314  private readonly List<Item> previousInteractablesInRange = new();
315  private readonly List<Item> interactablesInRange = new();
316 
317  private bool wasFiring;
318 
322  public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true)
323  {
324 
325  if (DisableControls || GUI.InputBlockingMenuOpen)
326  {
327  foreach (Key key in keys)
328  {
329  if (key == null) { continue; }
330  key.Reset();
331  }
332  if (GUI.InputBlockingMenuOpen)
333  {
334  cursorPosition =
335  Position + PlayerInput.MouseSpeed.ClampLength(10.0f); //apply a little bit of movement to the cursor pos to prevent AFK kicking
336  }
337  }
338  else
339  {
340  wasFiring |= keys[(int)InputType.Aim].Held && keys[(int)InputType.Shoot].Held;
341  for (int i = 0; i < keys.Length; i++)
342  {
343  keys[i].SetState();
344  }
345 
347  !keys[(int)InputType.Aim].Held &&
349  {
350  ResetInputIfPrimaryMouse(InputType.Use);
351  ResetInputIfPrimaryMouse(InputType.Shoot);
352  ResetInputIfPrimaryMouse(InputType.Select);
353  void ResetInputIfPrimaryMouse(InputType inputType)
354  {
355  if (GameSettings.CurrentConfig.KeyMap.Bindings[inputType].MouseButton == MouseButton.PrimaryMouse)
356  {
357  keys[(int)inputType].Reset();
358  }
359  }
360  }
361 
362  ShowInteractionLabels = keys[(int)InputType.ShowInteractionLabels].Held;
363 
365  {
366  focusedItem = InteractionLabelManager.HoveredItem;
367  }
368 
369  //if we were firing (= pressing the aim and shoot keys at the same time)
370  //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items
371  if (wasFiring && !keys[(int)InputType.Shoot].Held)
372  {
373  if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Shoot] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select])
374  {
375  keys[(int)InputType.Select].Reset();
376  }
377  if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Shoot] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use])
378  {
379  keys[(int)InputType.Use].Reset();
380  }
381  wasFiring = false;
382  }
383 
384  float targetOffsetAmount = 0.0f;
385  if (moveCam)
386  {
388  {
389  //wait until the character has been in pressure for one second so the zoom doesn't
390  //"flicker" in and out if the pressure fluctuates around the minimum threshold
391  pressureEffectTimer += deltaTime;
392  if (pressureEffectTimer > 1.0f)
393  {
394  float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure;
395  float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f, 0.0f, 1.0f);
396  cam.Zoom = MathHelper.Lerp(cam.Zoom,
397  cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f),
398  zoomInEffectStrength);
399  }
400  }
401  else
402  {
403  pressureEffectTimer = 0.0f;
404  }
405 
406  if (IsHumanoid)
407  {
408  cam.OffsetAmount = 250.0f;// MathHelper.Lerp(cam.OffsetAmount, 250.0f, deltaTime);
409  }
410  else
411  {
412  //increased visibility range when controlling large a non-humanoid
413  cam.OffsetAmount = MathHelper.Clamp(Mass, 250.0f, 1500.0f);
414  }
415  }
416 
417  UpdateLocalCursor(cam);
418 
419  Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
420  if (GUI.PauseMenuOpen)
421  {
422  cam.OffsetAmount = targetOffsetAmount = 0.0f;
423  }
424  else if (Lights.LightManager.ViewTarget is Item item && item.Prefab.FocusOnSelected)
425  {
426  cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected * item.OffsetOnSelectedMultiplier;
427  }
428  else if (SelectedItem != null && ViewTarget == null &&
429  SelectedItem.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this)))
430  {
431  cam.OffsetAmount = targetOffsetAmount = 0.0f;
432  cursorPosition =
433  Position +
434  PlayerInput.MouseSpeed.ClampLength(10.0f); //apply a little bit of movement to the cursor pos to prevent AFK kicking
435  }
436  else if (!GameSettings.CurrentConfig.EnableMouseLook)
437  {
438  cam.OffsetAmount = targetOffsetAmount = 0.0f;
439  }
440  else if (Lights.LightManager.ViewTarget == this)
441  {
442  if (GUI.PauseMenuOpen || IsUnconscious)
443  {
444  if (deltaTime > 0.0f)
445  {
446  cam.OffsetAmount = targetOffsetAmount = 0.0f;
447  }
448  }
449  else if (IsMouseOnUI)
450  {
451  targetOffsetAmount = cam.OffsetAmount;
452  }
453  else if (Vector2.DistanceSquared(AnimController.Limbs[0].SimPosition, mouseSimPos) > 1.0f)
454  {
455  Body body = Submarine.CheckVisibility(AnimController.Limbs[0].SimPosition, mouseSimPos);
456  Structure structure = body?.UserData as Structure;
457 
458  float sightDist = Submarine.LastPickedFraction;
459  if (body?.UserData is Structure && !((Structure)body.UserData).CastShadow)
460  {
461  sightDist = 1.0f;
462  }
463  targetOffsetAmount = Math.Max(250.0f, sightDist * 500.0f);
464  }
465  }
466 
467  cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, targetOffsetAmount, 0.05f);
468  DoInteractionUpdate(deltaTime, mouseSimPos);
469  }
470 
471  if (!GUI.InputBlockingMenuOpen)
472  {
473  if (SelectedItem != null &&
474  (SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)) ||
475  ((ViewTarget as Item)?.Prefab.FocusOnSelected ?? false) && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)))
476  {
477  if (GameMain.Client != null)
478  {
479  //emulate a Deselect input to get the character to deselect the item server-side
480  EmulateInput(InputType.Deselect);
481  }
482  //reset focus to prevent us from accidentally interacting with another entity
483  focusedItem = null;
484  FocusedCharacter = null;
485  findFocusedTimer = 0.2f;
486  SelectedItem = null;
487  }
488  }
489 
490  DisableControls = false;
491  }
492 
493  public void UpdateLocalCursor(Camera cam)
494  {
495  cursorPosition = cam.ScreenToWorld(PlayerInput.MousePosition);
496  if (AnimController.CurrentHull?.Submarine != null)
497  {
499  }
500  }
501 
502  partial void UpdateControlled(float deltaTime, Camera cam)
503  {
504  if (controlled != this) { return; }
505 
506  ControlLocalPlayer(deltaTime, cam);
507 
508  Lights.LightManager.ViewTarget = this;
509  CharacterHUD.Update(deltaTime, this, cam);
510 
511  if (hudProgressBars.Any())
512  {
513  foreach (var progressBar in hudProgressBars)
514  {
515  if (progressBar.Value.FadeTimer <= 0.0f)
516  {
517  progressBarRemovals.Add(progressBar);
518  continue;
519  }
520  progressBar.Value.Update(deltaTime);
521  }
522  if (progressBarRemovals.Any())
523  {
524  progressBarRemovals.ForEach(pb => hudProgressBars.Remove(pb.Key));
525  progressBarRemovals.Clear();
526  }
527  }
528  }
529 
530  public void EmulateInput(InputType input)
531  {
532  keys[(int)input].Hit = true;
533  }
534 
535  partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun)
536  {
537  if (IsDead) { return; }
538  if (attacker != null)
539  {
540  if (attackResult.Damage <= 0.01f) { return; }
541  }
542  else
543  {
544  if (attackResult.Damage <= 1.0f) { return; }
545  }
546  PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2);
547  }
548 
549  partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log)
550  {
551  HintManager.OnCharacterKilled(this);
552 
553  if (GameMain.NetworkMember != null && controlled == this)
554  {
555  LocalizedString chatMessage = CauseOfDeath.Type == CauseOfDeathType.Affliction ?
556  CauseOfDeath.Affliction.SelfCauseOfDeathDescription :
557  TextManager.Get("Self_CauseOfDeathDescription." + CauseOfDeath.Type.ToString(), "Self_CauseOfDeathDescription.Damage");
558 
559  if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); }
560 
562 
563  GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead);
564  GameMain.LightManager.LosEnabled = false;
565  controlled = null;
566  if (Screen.Selected?.Cam is Camera cam)
567  {
568  cam.TargetPos = Vector2.Zero;
569  //briefly lock moving the camera with arrow keys
570  //(it's annoying to have the camera fly off when you die while trying to move to safety)
571  cam.MovementLockTimer = 2.0f;
572  Lights.LightManager.ViewTarget = null;
573  }
574  }
575 
576  PlaySound(CharacterSound.SoundType.Die);
577  }
578 
579  partial void DisposeProjSpecific()
580  {
581  if (controlled == this)
582  {
583  controlled = null;
584  if (Screen.Selected?.Cam is not null)
585  {
586  Screen.Selected.Cam.TargetPos = Vector2.Zero;
587  Lights.LightManager.ViewTarget = null;
588  }
589  }
590 
591  sounds.ForEach(s => s.Sound?.Dispose());
592  sounds.Clear();
593 
594  if (GameMain.GameSession?.CrewManager != null &&
595  GameMain.GameSession.CrewManager.GetCharacters().Contains(this))
596  {
597  GameMain.GameSession.CrewManager.RemoveCharacter(this);
598  }
599 
600  if (GameMain.Client?.Character == this) { GameMain.Client.Character = null; }
601 
602  if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; }
603  }
604 
605  private void UpdateInteractablesInRange()
606  {
607  // keep two lists to detect changes to the current state of interactables in range
608  previousInteractablesInRange.Clear();
609  previousInteractablesInRange.AddRange(interactablesInRange);
610 
611  interactablesInRange.Clear();
612 
613  //use the list of visible entities if it exists
614  var entityList = Submarine.VisibleEntities ?? Item.ItemList;
615 
616  foreach (MapEntity entity in entityList)
617  {
618  if (entity is not Item item) { continue; }
619 
620  if (item.body != null && !item.body.Enabled) { continue; }
621 
622  if (item.ParentInventory != null) { continue; }
623 
624  if (item.Prefab.RequireCampaignInteract &&
625  item.CampaignInteractionType == CampaignMode.InteractionType.None)
626  {
627  continue;
628  }
629 
630  if (Screen.Selected is SubEditorScreen { WiringMode: true } &&
631  item.GetComponent<ConnectionPanel>() == null)
632  {
633  continue;
634  }
635 
636  if (CanInteractWith(item))
637  {
638  interactablesInRange.Add(item);
639  }
640  }
641 
642  if (!interactablesInRange.SequenceEqual(previousInteractablesInRange))
643  {
644  InteractionLabelManager.RefreshInteractablesInRange(interactablesInRange);
645  }
646  }
647 
648  private readonly List<Item> debugInteractablesInRange = new List<Item>();
649  private readonly List<Item> debugInteractablesAtCursor = new List<Item>();
650  private readonly List<(Item item, float dist)> debugInteractablesNearCursor = new List<(Item item, float dist)>();
651 
659  public Item FindClosestItem(List<Item> itemCollection, Vector2 simPosition, float aimAssistModifier = 0.0f)
660  {
661  if (Submarine != null)
662  {
663  simPosition += Submarine.SimPosition;
664  }
665 
666  debugInteractablesInRange.Clear();
667  debugInteractablesAtCursor.Clear();
668  debugInteractablesNearCursor.Clear();
669 
670  bool draggingItemToWorld = CharacterInventory.DraggingItemToWorld;
671 
672  //reduce the amount of aim assist if an item has been selected
673  //= can't switch selection to another item without deselecting the current one first UNLESS the cursor is directly on the item
674  //otherwise it would be too easy to accidentally switch the selected item when rewiring items
675  float aimAssistAmount = SelectedItem == null ? 100.0f * aimAssistModifier : 1.0f;
676 
677  Vector2 displayPosition = ConvertUnits.ToDisplayUnits(simPosition);
678 
679  Item closestItem = null;
680  float closestItemDistance = Math.Max(aimAssistAmount, 2.0f);
681  foreach (var item in itemCollection)
682  {
683  if (draggingItemToWorld)
684  {
685  if (item.OwnInventory == null ||
686  !item.OwnInventory.Container.AllowDragAndDrop ||
687  !item.OwnInventory.CanBePut(CharacterInventory.DraggingItems.First()) ||
688  !CanAccessInventory(item.OwnInventory))
689  {
690  continue;
691  }
692  }
693 
694  float distanceToItem = float.PositiveInfinity;
695  if (item.IsInsideTrigger(displayPosition, out Rectangle transformedTrigger))
696  {
697  debugInteractablesAtCursor.Add(item);
698  //distance is between 0-1 when the cursor is directly on the item
699  distanceToItem =
700  Math.Abs(transformedTrigger.Center.X - displayPosition.X) / transformedTrigger.Width +
701  Math.Abs((transformedTrigger.Y - transformedTrigger.Height / 2.0f) - displayPosition.Y) / transformedTrigger.Height;
702  //modify the distance based on the size of the trigger (preferring smaller items)
703  distanceToItem *= MathHelper.Lerp(0.05f, 2.0f, (transformedTrigger.Width + transformedTrigger.Height) / 250.0f);
704  }
705  else if (!item.Prefab.RequireCursorInsideTrigger)
706  {
707  Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
708 
709  if (itemDisplayRect.Contains(displayPosition))
710  {
711  debugInteractablesAtCursor.Add(item);
712  //distance is between 0-1 when the cursor is directly on the item
713  distanceToItem =
714  Math.Abs(itemDisplayRect.Center.X - displayPosition.X) / itemDisplayRect.Width +
715  Math.Abs(itemDisplayRect.Center.Y - displayPosition.Y) / itemDisplayRect.Height;
716  //modify the distance based on the size of the item (preferring smaller ones)
717  distanceToItem *= MathHelper.Lerp(0.05f, 2.0f, (itemDisplayRect.Width + itemDisplayRect.Height) / 250.0f);
718  }
719  else
720  {
721  if (closestItemDistance < 2.0f) { continue; }
722  //get the point on the itemDisplayRect which is closest to the cursor
723  Vector2 rectIntersectionPoint = new Vector2(
724  MathHelper.Clamp(displayPosition.X, itemDisplayRect.X, itemDisplayRect.Right),
725  MathHelper.Clamp(displayPosition.Y, itemDisplayRect.Y, itemDisplayRect.Bottom));
726  distanceToItem = 2.0f + Vector2.Distance(rectIntersectionPoint, displayPosition);
727  }
728  }
729 
730  if (distanceToItem > closestItemDistance) { continue; }
731  if (!CanInteractWith(item)) { continue; }
732 
733  debugInteractablesNearCursor.Add((item, 1.0f - distanceToItem / (100.0f * aimAssistModifier)));
734  closestItem = item;
735  closestItemDistance = distanceToItem;
736  }
737 
738  return closestItem;
739  }
740 
741  private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = MaxHighlightDistance)
742  {
743  Character closestCharacter = null;
744 
745  maxDist = ConvertUnits.ToSimUnits(maxDist);
746  float closestDist = maxDist;
747  foreach (Character c in CharacterList)
748  {
749  if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; }
750 
751  float dist = c.GetDistanceToClosestLimb(mouseSimPos);
752  if (dist < closestDist ||
753  (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist))
754  {
755  closestCharacter = c;
756  closestDist = dist;
757  }
758  }
759 
760  return closestCharacter;
761  }
762 
763  public bool ShouldLockHud()
764  {
765  if (this != controlled) { return false; }
766  if (GameMain.GameSession?.Campaign != null && GameMain.GameSession.Campaign.ShowCampaignUI) { return true; }
767  var controller = SelectedItem?.GetComponent<Controller>();
768  //lock if using a controller, except if we're also using a connection panel in the same item
769  return
770  SelectedItem != null &&
771  controller?.User == this && controller.HideHUD &&
772  SelectedItem?.GetComponent<ConnectionPanel>()?.User != this;
773  }
774 
775 
776  partial void UpdateProjSpecific(float deltaTime, Camera cam)
777  {
778  foreach (GUIMessage message in guiMessages)
779  {
780  bool wasPending = message.Timer < 0.0f;
781  message.Timer += deltaTime;
782  if (wasPending && message.Timer >= 0.0f && message.PlaySound)
783  {
784  SoundPlayer.PlayUISound(GUISoundType.UIMessage);
785  }
786  }
787  guiMessages.RemoveAll(m => m.Timer >= m.Lifetime);
788 
789  if (textlessSpeechBubble != null)
790  {
791  textlessSpeechBubble.LifeTime -= deltaTime;
792  if (textlessSpeechBubble.LifeTime <= 0) { textlessSpeechBubble = null; }
793  }
794 
795  if (!enabled) { return; }
796 
797  if (!IsIncapacitated)
798  {
799  if (soundTimer > 0)
800  {
801  soundTimer -= deltaTime;
802  }
803  else if (AIController is EnemyAIController enemyAI)
804  {
805  switch (enemyAI.State)
806  {
807  case AIState.Attack:
808  if (Rand.Value() > 0.5f)
809  {
810  PlaySound(CharacterSound.SoundType.Attack);
811  }
812  else
813  {
814  PlaySound(CharacterSound.SoundType.Idle);
815  }
816  break;
817  default:
818  var petBehavior = enemyAI.PetBehavior;
819  if (petBehavior != null &&
820  (petBehavior.Happiness < petBehavior.UnhappyThreshold || petBehavior.Hunger > petBehavior.HungryThreshold))
821  {
822  PlaySound(CharacterSound.SoundType.Unhappy);
823  }
824  else
825  {
826  PlaySound(CharacterSound.SoundType.Idle);
827  }
828  break;
829  }
830  }
831  }
832 
833  if (info != null || Vitality < MaxVitality * 0.98f || IsPet)
834  {
835  hudInfoTimer -= deltaTime;
836  if (hudInfoTimer <= 0.0f)
837  {
838  if (controlled == null)
839  {
840  hudInfoVisible = true;
841  }
842 
843  //if the character is not in the camera view, the name can't be visible and we can avoid the expensive visibility checks
844  else if (WorldPosition.X < cam.WorldView.X || WorldPosition.X > cam.WorldView.Right ||
845  WorldPosition.Y > cam.WorldView.Y || WorldPosition.Y < cam.WorldView.Y - cam.WorldView.Height)
846  {
847  hudInfoVisible = false;
848  }
849  else
850  {
851  //Ideally it shouldn't send the character entirely if we can't see them but /shrug, this isn't the most hacker-proof game atm
852  hudInfoVisible = controlled.CanSeeTarget(this, controlled.ViewTarget);
853  }
854  hudInfoTimer = Rand.Range(0.5f, 1.0f);
855  }
856  }
857 
859  if (controlled == this)
860  {
861  CharacterHealth.UpdateHUD(deltaTime);
862  }
863  }
864 
865  partial void SetOrderProjSpecific(Order order)
866  {
867  GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order);
868  }
869 
870  public static void AddAllToGUIUpdateList()
871  {
872  for (int i = 0; i < CharacterList.Count; i++)
873  {
874  CharacterList[i].AddToGUIUpdateList();
875  }
876  }
877 
878  public virtual void AddToGUIUpdateList()
879  {
880  if (controlled == this)
881  {
884  }
885  }
886 
887  public void DoVisibilityCheck(Camera cam)
888  {
889  IsVisible = false;
890  if (!Enabled || AnimController.SimplePhysicsEnabled) { return; }
891 
892  foreach (Limb limb in AnimController.Limbs)
893  {
894  float maxExtent = ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent());
895  if (limb.LightSource != null) { maxExtent = Math.Max(limb.LightSource.Range, maxExtent); }
896  if (limb.body.DrawPosition.X < cam.WorldView.X - maxExtent || limb.body.DrawPosition.X > cam.WorldView.Right + maxExtent) { continue; }
897  if (limb.body.DrawPosition.Y < cam.WorldView.Y - cam.WorldView.Height - maxExtent || limb.body.DrawPosition.Y > cam.WorldView.Y + maxExtent) { continue; }
898  IsVisible = true;
899  return;
900  }
901  }
902 
903  public void Draw(SpriteBatch spriteBatch, Camera cam)
904  {
905  if (!Enabled || InvisibleTimer > 0.0f) { return; }
906  AnimController.Draw(spriteBatch, cam);
907  }
908 
909  public void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth = true)
910  {
911  CharacterHUD.Draw(spriteBatch, this, cam);
912  if (drawHealth && !CharacterHUD.IsCampaignInterfaceOpen) { CharacterHealth.DrawHUD(spriteBatch); }
913  }
914 
915  public void DrawGUIMessages(SpriteBatch spriteBatch, Camera cam)
916  {
917  if (info == null || !Enabled || InvisibleTimer > 0.0f)
918  {
919  return;
920  }
921 
922  Vector2 messagePos = DrawPosition;
923  messagePos.Y += hudInfoHeight;
924  messagePos = cam.WorldToScreen(messagePos) - Vector2.UnitY * GUI.IntScale(60);
925  foreach (GUIMessage message in guiMessages)
926  {
927  if (message.Timer < 0) { continue; }
928  Vector2 drawPos = messagePos + Vector2.UnitX * (GUI.IntScale(60) - message.Size.X);
929  drawPos = new Vector2((int)drawPos.X, (int)drawPos.Y);
930  float alpha = MathHelper.SmoothStep(1.0f, 0.0f, message.Timer / message.Lifetime);
931  GUI.DrawString(spriteBatch, drawPos, message.Text, message.Color * alpha);
932  messagePos -= Vector2.UnitY * message.Size.Y * 1.2f;
933  }
934  }
935 
936  public virtual void DrawFront(SpriteBatch spriteBatch, Camera cam)
937  {
938  if (!Enabled || InvisibleTimer > 0.0f || (AnimController?.SimplePhysicsEnabled ?? true)) { return; }
939 
940  if (GameMain.DebugDraw)
941  {
942  AnimController.DebugDraw(spriteBatch);
943  }
944 
945  if (GUI.DisableHUD) { return; }
946 
947  if (Controlled != null &&
948  Controlled != this &&
949  Submarine != null &&
951  GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None)
952  {
953  float yPos = Controlled.AnimController.FloorY - 1.5f;
954 
955  if (Controlled.AnimController.Stairs != null)
956  {
957  //consider the bottom of the stairs the "floor of the room the controlled character is in"
959  }
960 
961  //don't show the HUD texts if the character is below the floor of the room the controlled character is in
962  if (AnimController.FloorY < yPos) { return; }
963  }
964 
965  Vector2 pos = DrawPosition;
966  pos.Y += hudInfoHeight;
967 
968  if (CurrentHull != null && DrawPosition.Y > CurrentHull.WorldRect.Y - 130.0f)
969  {
970  float lowerAmount = DrawPosition.Y - (CurrentHull.WorldRect.Y - 130.0f);
971  hudInfoHeight = MathHelper.Lerp(hudInfoHeight, 100.0f - lowerAmount, 0.1f);
972  hudInfoHeight = Math.Max(hudInfoHeight, 20.0f);
973  }
974  else
975  {
976  hudInfoHeight = MathHelper.Lerp(hudInfoHeight, 100.0f, 0.1f);
977  }
978 
979  pos.Y = -pos.Y;
980 
981  if (this == controlled)
982  {
983  if (DebugDrawInteract)
984  {
985  Vector2 cursorPos = cam.ScreenToWorld(PlayerInput.MousePosition);
986  cursorPos.Y = -cursorPos.Y;
987  foreach (Item item in debugInteractablesAtCursor)
988  {
989  GUI.DrawLine(spriteBatch, cursorPos,
990  new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), Color.LightGreen, width: 4);
991  }
992  foreach (Item item in debugInteractablesInRange)
993  {
994  GUI.DrawLine(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y),
995  new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), Color.White * 0.1f, width: 4);
996  }
997  foreach ((Item item, float dist) in debugInteractablesNearCursor)
998  {
999  GUI.DrawLine(spriteBatch,
1000  cursorPos,
1001  new Vector2(item.DrawPosition.X, -item.DrawPosition.Y),
1002  ToolBox.GradientLerp(dist, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green), width: 2);
1003  }
1004  }
1005  }
1006  else
1007  {
1008 
1009  float hoverRange = 300.0f;
1010  float fadeOutRange = 200.0f;
1011  float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition));
1012  float hudInfoAlpha =
1014  MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) :
1015  1.0f;
1016 
1017  if (!GUI.DisableCharacterNames && hudInfoVisible &&
1018  (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f)
1019  {
1020  if (info != null)
1021  {
1023  if (controlled == null && name != Info.Name)
1024  {
1025  name += " " + TextManager.Get("Disguised");
1026  }
1027  else if (Info.Title != null && TeamID != CharacterTeamType.Team1)
1028  {
1029  name += '\n' + Info.Title;
1030  }
1031 
1032  Vector2 nameSize = GUIStyle.Font.MeasureString(name);
1033  Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom;
1034  Color nameColor = GetNameColor();
1035 
1036  Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
1037  Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height);
1038  namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y;
1039  namePos *= screenSize / viewportSize;
1040  namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y);
1041  namePos *= viewportSize / screenSize;
1042  namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y;
1043 
1045  {
1046  var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType);
1047  if (iconStyle != null)
1048  {
1049  Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f;
1050  Vector2 iconPos = headPos;
1051  iconPos.Y = -iconPos.Y;
1052  nameColor = iconStyle.Color;
1053  var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
1054  float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale;
1055  icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
1056  }
1057  }
1058 
1059  GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f);
1060  GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f);
1061  if (GameMain.DebugDraw)
1062  {
1063  GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White);
1064  }
1065  }
1066 
1067  var petBehavior = (AIController as EnemyAIController)?.PetBehavior;
1068  if (petBehavior != null && !IsDead && !IsUnconscious)
1069  {
1070  var petStatus = petBehavior.GetCurrentStatusIndicatorType();
1071  var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus);
1072  if (iconStyle != null)
1073  {
1074  Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f;
1075  Vector2 iconPos = headPos;
1076  iconPos.Y = -iconPos.Y;
1077  var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
1078  float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom;
1079  icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
1080  }
1081  }
1082  }
1083 
1084  if (IsDead) { return; }
1085 
1086  var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars;
1087  if (healthBarMode != EnemyHealthBarMode.ShowAll)
1088  {
1089  if (Controlled == null)
1090  {
1091  if (!IsOnPlayerTeam) { return; }
1092  }
1093  else
1094  {
1095  if (!HumanAIController.IsFriendly(Controlled, this) ||
1096  (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy)))
1097  {
1098  return;
1099  }
1100  }
1101  }
1102 
1104  {
1105  hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f));
1106 
1107  Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y);
1108  GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f),
1110  Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha,
1111  new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha);
1112  }
1113  }
1114 
1115  if (textlessSpeechBubble != null)
1116  {
1117  Vector2 iconPos = pos - Vector2.UnitY * 5;
1118 
1119  GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, iconPos,
1120  textlessSpeechBubble.Color * Math.Min(textlessSpeechBubble.LifeTime, 1.0f), 0.0f,
1121  Math.Min(textlessSpeechBubble.LifeTime, 1.0f));
1122  }
1123  }
1124 
1125  public void ShowSpeechBubble(Color color, string text)
1126  {
1127  if (!GameSettings.CurrentConfig.ChatSpeechBubbles)
1128  {
1129  ShowTextlessSpeechBubble(1.0f, color);
1130  return;
1131  }
1132  float duration = MathHelper.Lerp(1.0f, 8.0f, Math.Min(text.Length / 100.0f, 1.0f));
1133  speechBubbles.Add(new SpeechBubble(this, duration, color, text));
1134  textlessSpeechBubble = null;
1135  }
1136 
1137  public void ShowTextlessSpeechBubble(float duration, Color color)
1138  {
1139  if (speechBubbles.Any(sb => sb.Character == this)) { return; }
1140  if (textlessSpeechBubble == null)
1141  {
1142  textlessSpeechBubble = new SpeechBubble(this, duration, color);
1143  }
1144  else
1145  {
1146  textlessSpeechBubble.Color = color;
1147  textlessSpeechBubble.LifeTime = Math.Max(textlessSpeechBubble.LifeTime, duration);
1148  }
1149  }
1150 
1151  public static void DrawSpeechBubbles(SpriteBatch spriteBatch, Camera cam)
1152  {
1153  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform);
1154  foreach (var bubble in speechBubbles)
1155  {
1156  Vector2 iconPos = Timing.Interpolate(bubble.PrevPosition, bubble.Position);
1157  iconPos += Vector2.UnitY * bubble.MoveUpAmount;
1158  if (bubble.Submarine != null)
1159  {
1160  iconPos += bubble.Submarine.DrawPosition;
1161  }
1162 
1163  float alpha = 1.0f;
1164  float mouseDist = Vector2.Distance(cam.WorldToScreen(iconPos), PlayerInput.MousePosition);
1165  //treat the size of the bubble from corner to corner as the
1166  float textSize = bubble.TextSize.Length();
1167  if (mouseDist < textSize)
1168  {
1169  alpha *= Math.Max(mouseDist / textSize, 0.5f);
1170  }
1171 
1172  iconPos.Y = -iconPos.Y;
1173  if (GUIStyle.SpeechBubbleIconSliced.Value is { } speechBubbleIconSliced)
1174  {
1175  Vector2 bubbleSize = bubble.TextSize + Vector2.One * GUI.IntScale(15);
1176  speechBubbleIconSliced.Draw(spriteBatch, new RectangleF(iconPos - bubbleSize / 2, bubbleSize), bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha);
1177  }
1178  GUI.DrawString(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, font: GUIStyle.SmallFont);
1179  }
1180  spriteBatch.End();
1181  }
1182 
1183  static partial void UpdateSpeechBubbles(float deltaTime)
1184  {
1185  for (int i = speechBubbles.Count - 1; i >= 0; i--)
1186  {
1187  var bubble = speechBubbles[i];
1188  bubble.LifeTime -= deltaTime;
1189  if (bubble.LifeTime <= 0 || bubble.Character is { Removed: true })
1190  {
1191  speechBubbles.RemoveAt(i);
1192  continue;
1193  }
1194 
1195  bubble.PrevPosition = bubble.Position;
1196 
1197  Vector2 desiredPos = bubble.GetDesiredPosition();
1198  Vector2 diff = desiredPos - bubble.Position;
1199  float dist = diff.Length();
1200  //how far the bubble needs to be from the desired position to start moving
1201  const float MoveThreshold = 100.0f;
1202  const float MaxSpeed = 1000.0f;
1203  if (dist < 1)
1204  {
1205  bubble.Moving = false;
1206  }
1207  else if (dist > MoveThreshold || bubble.Moving)
1208  {
1209  Vector2 moveAmount = diff / dist * MathHelper.Clamp(dist * 5, 0, MaxSpeed) * deltaTime;
1210  //slower vertical movement (don't want to interfere too much with the bubbles floating up
1211  //and the overlap prevention which works vertically)
1212  moveAmount.Y *= 0.1f;
1213  bubble.Position += moveAmount;
1214  bubble.Moving = true;
1215  }
1216 
1217  bubble.MoveUpAmount += deltaTime * 5.0f;
1218  //go through the newer bubbles, move this one out of the way if one is overlapping
1219  for (int j = i + 1; j < speechBubbles.Count; j++)
1220  {
1221  var otherBubble = speechBubbles[j];
1222  {
1223  if (Math.Abs(bubble.Position.X - otherBubble.Position.X) < (bubble.TextSize.X + otherBubble.TextSize.X) / 2 &&
1224  Math.Abs(bubble.Position.Y - otherBubble.Position.Y) < (bubble.TextSize.Y + otherBubble.TextSize.Y) / 2 + 10)
1225  {
1226  bubble.Position += Vector2.UnitY * deltaTime * 50.0f;
1227  }
1228  }
1229  }
1230  }
1231  }
1232 
1233  public Color GetNameColor()
1234  {
1235  CharacterTeamType team = teamID;
1236  if (Info?.IsDisguisedAsAnother != null)
1237  {
1238  var idCard = Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
1239  if (idCard != null)
1240  {
1241  if (team == CharacterTeamType.Team2 && idCard.TeamID != CharacterTeamType.Team2)
1242  {
1243  team = CharacterTeamType.Team1;
1244  }
1245  else if (team == CharacterTeamType.Team1 && idCard.TeamID == CharacterTeamType.Team2)
1246  {
1247  team = CharacterTeamType.Team2;
1248  }
1249  }
1250  }
1251 
1252  Color nameColor = GUIStyle.TextColorNormal;
1253  if (Controlled != null && team != Controlled.TeamID)
1254  {
1255  if (TeamID == CharacterTeamType.FriendlyNPC)
1256  {
1257  nameColor = UniqueNameColor ?? Color.SkyBlue;
1258  }
1259  else
1260  {
1261  nameColor = GUIStyle.Red;
1262  }
1263  }
1264  return nameColor;
1265  }
1266 
1267  public void AddMessage(string rawText, Color color, bool playSound, Identifier identifier = default, int? value = null, float lifetime = 3.0f)
1268  {
1269  GUIMessage existingMessage = null;
1270 
1271  float delay = 0.0f;
1272  if (guiMessages.Any())
1273  {
1274  delay = guiMessages.Min(m => m.Timer) - 0.5f;
1275  if (delay < 0)
1276  {
1277  delay = -delay;
1278  if (guiMessages.Count > 5)
1279  {
1280  //reduce delays if there's lots of messages
1281  guiMessages.Where(m => m.Timer < 0.0f).ForEach(m => m.Timer *= 0.9f);
1282  }
1283  }
1284  else
1285  {
1286  delay = 0;
1287  }
1288  }
1289 
1290  if (identifier != null)
1291  {
1292  existingMessage = guiMessages.Find(m => m.Identifier == identifier && m.Timer < m.Lifetime * 0.5f);
1293  }
1294  if (existingMessage == null || !value.HasValue)
1295  {
1296  var newMessage = new GUIMessage(rawText, color, delay, identifier, value, lifetime);
1297  guiMessages.Insert(0, newMessage);
1298  if (playSound)
1299  {
1300  if (delay > 0.0f)
1301  {
1302  newMessage.PlaySound = true;
1303  }
1304  else
1305  {
1306  SoundPlayer.PlayUISound(GUISoundType.UIMessage);
1307  }
1308  }
1309  }
1310  else
1311  {
1312  existingMessage.Value += value.Value;
1313  }
1314  }
1315 
1320  public HUDProgressBar UpdateHUDProgressBar(object linkedObject, Vector2 worldPosition, float progress, Color emptyColor, Color fullColor, string textTag = "")
1321  {
1322  if (controlled != this) { return null; }
1323 
1324  if (!hudProgressBars.TryGetValue(linkedObject, out HUDProgressBar progressBar))
1325  {
1326  progressBar = new HUDProgressBar(worldPosition, Submarine, emptyColor, fullColor, textTag);
1327  hudProgressBars.Add(linkedObject, progressBar);
1328  }
1329  else
1330  {
1331  progressBar.TextTag = textTag;
1332  }
1333 
1334  progressBar.WorldPosition = worldPosition;
1335  progressBar.FadeTimer = Math.Max(progressBar.FadeTimer, 1.0f);
1336  progressBar.Progress = progress;
1337 
1338  return progressBar;
1339  }
1340 
1341  private readonly List<CharacterSound> matchingSounds = new List<CharacterSound>();
1342  private SoundChannel soundChannel;
1343  public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f, float maxInterval = 0)
1344  {
1345  if (Removed) { return; }
1346  if (sounds == null || sounds.Count == 0) { return; }
1347  if (soundChannel != null && soundChannel.IsPlaying) { return; }
1348  if (GameMain.SoundManager?.Disabled ?? true) { return; }
1349  if (soundTimer > Params.SoundInterval * soundIntervalFactor) { return; }
1350  if (Params.SoundInterval - soundTimer < maxInterval) { return; }
1351  matchingSounds.Clear();
1352  foreach (var s in sounds)
1353  {
1354  if (s.Type == soundType && (s.TagSet.None() || (info != null && s.TagSet.IsSubsetOf(info.Head.Preset.TagSet))))
1355  {
1356  matchingSounds.Add(s);
1357  }
1358  }
1359  var selectedSound = matchingSounds.GetRandomUnsynced();
1360  if (selectedSound?.Sound == null) { return; }
1361  soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull, ignoreMuffling: selectedSound.IgnoreMuffling);
1363  }
1364 
1365  public void AddActiveObjectiveEntity(Entity entity, Sprite sprite, Color? color = null)
1366  {
1367  if (activeObjectiveEntities.Any(aoe => aoe.Entity == entity)) return;
1368  ObjectiveEntity objectiveEntity = new ObjectiveEntity(entity, sprite, color);
1369  activeObjectiveEntities.Add(objectiveEntity);
1370  }
1371 
1373  {
1374  ObjectiveEntity found = activeObjectiveEntities.Find(aoe => aoe.Entity == entity);
1375  if (found == null) return;
1376  activeObjectiveEntities.Remove(found);
1377  }
1378 
1382  public CharacterSound GetSound(Func<CharacterSound, bool> predicate = null, bool random = false) => random ? sounds.GetRandomUnsynced(predicate) : sounds.FirstOrDefault(predicate);
1383 
1384  partial void ImplodeFX()
1385  {
1386  Vector2 centerOfMass = AnimController.GetCenterOfMass();
1387 
1388  SoundPlayer.PlaySound("implode", WorldPosition);
1389 
1390  for (int i = 0; i < 10; i++)
1391  {
1393  WorldPosition + Rand.Vector(5.0f),
1394  Rand.Vector(10.0f));
1395  if (p != null) p.Size *= 2.0f;
1396 
1398  ConvertUnits.ToDisplayUnits(centerOfMass) + Rand.Vector(5.0f),
1399  new Vector2(Rand.Range(-50f, 50f), Rand.Range(-100f, 50f)));
1400 
1402  WorldPosition + Rand.Vector(Rand.Range(0.0f, 50.0f)),
1403  Rand.Range(0.0f, MathHelper.TwoPi),
1404  Rand.Range(200.0f, 700.0f), null);
1405  }
1406 
1407  for (int i = 0; i < 30; i++)
1408  {
1410  WorldPosition + Rand.Vector(Rand.Range(0.0f, 50.0f)),
1411  Rand.Range(0.0f, MathHelper.TwoPi),
1412  Rand.Range(50.0f, 500.0f), null);
1413  }
1414  }
1415 
1416  partial void OnMoneyChanged(int prevAmount, int newAmount) { }
1417 
1418  partial void OnTalentGiven(TalentPrefab talentPrefab)
1419  {
1420  AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled);
1421  }
1422  }
1423 }
AIObjective CurrentObjective
Includes orders.
Vector2 WorldToScreen(Vector2 coords)
Definition: Camera.cs:416
float? Zoom
Definition: Camera.cs:78
Matrix Transform
Definition: Camera.cs:136
float Rotation
Definition: Camera.cs:99
float??? DefaultZoom
Definition: Camera.cs:15
Vector2 ScreenToWorld(Vector2 coords)
Definition: Camera.cs:410
float OffsetAmount
Definition: Camera.cs:118
Rectangle WorldView
Definition: Camera.cs:123
Vector2 TargetPos
Definition: Camera.cs:156
readonly AfflictionPrefab Affliction
Definition: CauseOfDeath.cs:13
readonly CauseOfDeathType Type
Definition: CauseOfDeath.cs:12
ObjectiveEntity(Entity entity, Sprite sprite, Color? color=null)
static void Draw(SpriteBatch spriteBatch, Character character, Camera cam)
void ShowTextlessSpeechBubble(float duration, Color color)
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
static void DrawSpeechBubbles(SpriteBatch spriteBatch, Camera cam)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
void AddActiveObjectiveEntity(Entity entity, Sprite sprite, Color? color=null)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
float????? CameraShake
Can be used to set camera shake from status effects
void AddMessage(string rawText, Color color, bool playSound, Identifier identifier=default, int? value=null, float lifetime=3.0f)
float???????? CollapseEffectStrength
Can be used by status effects
bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel=CharacterInventory.AccessLevel.Limited)
Dictionary< object, HUDProgressBar > HUDProgressBars
Item FindClosestItem(List< Item > itemCollection, Vector2 simPosition, float aimAssistModifier=0.0f)
Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means tha...
void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor=1.0f, float maxInterval=0)
void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam=true)
Control the Character according to player input
void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
HUDProgressBar UpdateHUDProgressBar(object linkedObject, Vector2 worldPosition, float progress, Color emptyColor, Color fullColor, string textTag="")
Creates a progress bar that's "linked" to the specified object (or updates an existing one if there's...
bool IsVisible
Is the character currently visible on the camera. Refresh the value by calling DoVisibilityCheck.
CharacterSound GetSound(Func< CharacterSound, bool > predicate=null, bool random=false)
Note that when a predicate is provided, the random option uses Linq.Where() extension method,...
virtual void DrawFront(SpriteBatch spriteBatch, Camera cam)
void DrawGUIMessages(SpriteBatch spriteBatch, Camera cam)
void Draw(SpriteBatch spriteBatch, Camera cam)
bool IsProtectedFromPressure
Is the character currently protected from the pressure by immunity/ability or a status effect (e....
void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth=true)
readonly List< SoundParams > Sounds
virtual Vector2 WorldPosition
Definition: Entity.cs:49
virtual Vector2 DrawPosition
Definition: Entity.cs:51
Submarine Submarine
Definition: Entity.cs:53
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing=false)
Definition: GUIPrefab.cs:284
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static int GraphicsHeight
Definition: GameMain.cs:168
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static bool DebugDraw
Definition: GameMain.cs:29
static ParticleManager ParticleManager
Definition: GameMain.cs:101
static GameClient Client
Definition: GameMain.cs:188
static Sounds.SoundManager SoundManager
Definition: GameMain.cs:80
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
IEnumerable< ItemComponent > ActiveHUDs
Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess=null, float collisionIgnoreTimer=0f, Tuple< Vector2, Vector2 > tracerPoints=null)
StatusIndicatorType GetCurrentStatusIndicatorType()
Definition: PetBehavior.cs:228
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.
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=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).
static IEnumerable< MapEntity > VisibleEntities
GUISoundType
Definition: GUI.cs:21