Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Door.cs
2 using FarseerPhysics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using FarseerPhysics.Dynamics;
8 #if CLIENT
9 using Barotrauma.Lights;
10 #endif
12 
14 {
16  {
17  private static readonly HashSet<Door> doorList = new HashSet<Door>();
18 
19  public static IReadOnlyCollection<Door> DoorList { get { return doorList; } }
20 
21  private Gap linkedGap;
22  private bool isOpen;
23 
24  private float openState;
25  private readonly Sprite doorSprite, weldedSprite, brokenSprite;
26  private readonly bool scaleBrokenSprite, fadeBrokenSprite;
27  private readonly bool autoOrientGap;
28 
29  private bool isJammed;
30  public bool IsJammed
31  {
32  get { return isJammed; }
33  set
34  {
35  if (isJammed == value) { return; }
36  isJammed = value;
37 #if SERVER
38  item.CreateServerEvent(this);
39 #endif
40  }
41  }
42 
43  private bool isStuck;
44 
45  [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
46  public bool IsStuck
47  {
48  get { return isStuck; }
49  private set
50  {
51  if (isStuck == value) { return; }
52  isStuck = value;
53 #if SERVER
55  {
56  item.CreateServerEvent(this);
57  }
58 #endif
59  }
60  }
61 
62  //how much "less stuck" partially doors get when opened
63  const float StuckReductionOnOpen = 30.0f;
64 
65  private float resetPredictionTimer;
66  private float toggleCooldownTimer;
67  private Character lastUser;
68 
69  private float damageSoundCooldown;
70 
71  private double lastBrokenTime;
72 
73  private Rectangle doorRect;
74 
75  private bool isBroken;
76 
77  public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen);
78 
79  public bool IsBroken
80  {
81  get { return isBroken; }
82  set
83  {
84  if (isBroken == value) { return; }
85  isBroken = value;
86  if (isBroken)
87  {
88  DisableBody();
89  }
90  else
91  {
92  EnableBody();
93  }
94 #if SERVER
95  item.CreateServerEvent(this);
96 #endif
97  }
98  }
99 
100  public PhysicsBody Body { get; private set; }
101 
102  //the fixture that's part of the submarine's collider (= fixture that things outside the sub can collide with if the door is outside hulls)
103  public Fixture OutsideSubmarineFixture;
104 
105  private float RepairThreshold
106  {
107  get { return item.GetComponent<Repairable>() == null ? 0.0f : item.MaxCondition; }
108  }
109 
110  public bool CanBeWelded = true;
111 
112  private float stuck;
113  [Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
114  public float Stuck
115  {
116  get { return stuck; }
117  set
118  {
119  if (isOpen || isBroken || !CanBeWelded) { return; }
120  stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
121  //don't allow clients to make the door stuck unless the server says so (handled in ClientRead)
122  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
123  if (stuck <= 0.0f) { IsStuck = false; }
124  if (stuck >= 99.0f) { IsStuck = true; }
125  }
126  }
127 
128  [Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door opens."), Editable]
129  public float OpeningSpeed { get; private set; }
130 
131  [Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door closes."), Editable]
132  public float ClosingSpeed { get; private set; }
133 
134  [Serialize(1.0f, IsPropertySaveable.Yes, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable]
135  public float ToggleCoolDown { get; private set; }
136 
137  public bool? PredictedState { get; private set; }
138 
139  public Gap LinkedGap
140  {
141  get
142  {
143  if (linkedGap == null)
144  {
145  GetLinkedGap();
146  }
147  return linkedGap;
148  }
149  }
150 
151  private void GetLinkedGap()
152  {
153  linkedGap = item.linkedTo.FirstOrDefault(e => e is Gap) as Gap;
154  if (linkedGap == null)
155  {
156  Rectangle rect = item.Rect;
157  linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
158  {
160  };
161  item.linkedTo.Add(linkedGap);
162  }
164  }
165 
166  public bool IsHorizontal { get; private set; }
167 
168  public bool IsConvexHullHorizontal => autoOrientGap && linkedGap != null ? !linkedGap.IsHorizontal : IsHorizontal;
169 
170  [Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")]
171  public Rectangle Window { get; set; }
172 
173  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the door currently open.")]
174  public bool IsOpen
175  {
176  get { return isOpen; }
177  set
178  {
179  isOpen = value;
180  OpenState = isOpen ? 1.0f : 0.0f;
181  }
182  }
183 
187  public bool ShouldBeOpen
188  {
189  get { return isOpen; }
190  set
191  {
192  if (isOpen != value)
193  {
194  ToggleState(ActionType.OnUse, user: null);
195  }
196  }
197  }
198 
199  public bool IsClosed => !IsOpen;
200 
201  public bool IsFullyOpen => IsOpen && OpenState >= 1.0f;
202 
203  public bool IsFullyClosed => IsClosed && OpenState <= 0f;
204 
205  public bool HasWindow => Window != Rectangle.Empty;
206 
207  [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
208  public bool HasIntegratedButtons { get; private set; }
209 
211  Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")]
212  public bool ToggleWhenClicked { get; private set; }
213 
214  public float OpenState
215  {
216  get { return openState; }
217  set
218  {
219  openState = MathHelper.Clamp(value, 0.0f, 1.0f);
220 #if CLIENT
221  float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
222  //refresh convex hulls if the body of the door has moved by 5 pixels,
223  //or if it becomes fully closed or fully open
224  if (Math.Abs(lastConvexHullState - openState) * size > 5.0f ||
225  (openState <= 0.0f && lastConvexHullState > 0.0f) ||
226  (openState >= 1.0f && lastConvexHullState < 1.0f))
227  {
228  UpdateConvexHulls();
229  lastConvexHullState = openState;
230  }
231 #endif
232  }
233  }
234 
235  [Serialize(false, IsPropertySaveable.No, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")]
236  public bool Impassable
237  {
238  get;
239  set;
240  }
241 
242  [Editable, Serialize(true, IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)]
243  public bool UseBetweenOutpostModules { get; private set; }
244 
245  [Editable, Serialize(false, IsPropertySaveable.No, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)]
246  public bool BotsShouldKeepOpen { get; private set; }
247 
248  public Door(Item item, ContentXElement element)
249  : base(item, element)
250  {
251  IsHorizontal = element.GetAttributeBool("horizontal", false);
252  canBePicked = element.GetAttributeBool("canbepicked", false);
253  autoOrientGap = element.GetAttributeBool("autoorientgap", false);
254 
255  allowedSlots.Clear();
256 
257  foreach (var subElement in element.Elements())
258  {
259  string textureDir = GetTextureDirectory(subElement);
260  switch (subElement.Name.ToString().ToLowerInvariant())
261  {
262  case "sprite":
263  doorSprite = new Sprite(subElement, path: textureDir);
264  break;
265  case "weldedsprite":
266  weldedSprite = new Sprite(subElement, path: textureDir);
267  break;
268  case "brokensprite":
269  brokenSprite = new Sprite(subElement, path: textureDir);
270  scaleBrokenSprite = subElement.GetAttributeBool("scale", false);
271  fadeBrokenSprite = subElement.GetAttributeBool("fade", false);
272  break;
273  }
274  }
275 
276  IsActive = true;
277  doorList.Add(this);
278  }
279 
280  public override void OnItemLoaded()
281  {
282  //do this here because the scale of the item might not be set to the final value yet in the constructor
283  doorRect = new Rectangle(
284  item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
285  item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
286  (int)(doorSprite.size.X * item.Scale),
287  (int)(doorSprite.size.Y * item.Scale));
288 
289  Body = new PhysicsBody(
290  ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)),
291  ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)),
292  radius: 0.0f,
293  density: 1.5f,
294  BodyType.Static,
295  Physics.CollisionWall,
296  Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile,
297  findNewContacts: false)
298  {
299  UserData = item,
300  Friction = 0.5f
301  };
303  ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
304  0.0f);
305  if (isBroken)
306  {
307  DisableBody();
308  }
309  }
310 
311  public override void Move(Vector2 amount, bool ignoreContacts = false)
312  {
313  if (ignoreContacts)
314  {
315  Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
316  }
317  else
318  {
319  Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
320  }
321 #if CLIENT
322  UpdateConvexHulls();
323 #endif
324  }
325 
326  private readonly LocalizedString accessDeniedTxt = TextManager.Get("AccessDenied");
327  private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
328  public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
329  {
330  Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
331  ParseMsg();
332  if (addMessage)
333  {
334  msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
335  }
336  return isBroken || base.HasRequiredItems(character, addMessage, msg);
337  }
338 
339  public override bool Pick(Character picker)
340  {
341  if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
342  if (RequiredItems.None()) { return false; }
343  if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; }
344  return base.Pick(picker);
345  }
346 
347  public override bool OnPicked(Character picker)
348  {
349  if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
350  if (!HasAccess(picker))
351  {
352  ToggleState(ActionType.OnPicked, picker);
353  }
354  return false;
355  }
356 
357  private void ToggleState(ActionType actionType, Character user)
358  {
359  if (toggleCooldownTimer > 0.0f && user != lastUser)
360  {
361  OnFailedToOpen();
362  return;
363  }
364  if (ToggleWhenClicked)
365  {
366  //do not activate cooldown at this point if the door doesn't get toggled when clicked
367  //(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it)
368  toggleCooldownTimer = ToggleCoolDown;
369  }
370  if (IsStuck || IsJammed)
371  {
372 #if CLIENT
373  if (IsStuck) { HintManager.OnTryOpenStuckDoor(user); }
374 #endif
375  toggleCooldownTimer = 1.0f;
376  OnFailedToOpen();
377  return;
378  }
379  item.SendSignal("1", "activate_out");
380  lastUser = user;
381  if (ToggleWhenClicked)
382  {
383  SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
384  }
385  }
386 
387  public override bool Select(Character character)
388  {
389  if (isBroken) { return true; }
390  bool hasRequiredItems = HasRequiredItems(character, false);
391  if (HasAccess(character))
392  {
393  float originalPickingTime = PickingTime;
394  PickingTime = 0;
395  ToggleState(ActionType.OnUse, character);
396  PickingTime = originalPickingTime;
398  return true;
399  }
400 #if CLIENT
401  else if (hasRequiredItems && character != null && character == Character.Controlled)
402  {
403  GUI.AddMessage(accessDeniedTxt, GUIStyle.Red);
404  }
405 #endif
406  return false;
407  }
408 
414  public bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance = 10.0f)
415  {
416  if (IsHorizontal)
417  {
418  return
419  position.X >= item.Rect.X + Window.X &&
420  position.X <= item.Rect.X + Window.X + Window.Width &&
421  position.Y >= item.Rect.Y - maxPerpendicularDistance &&
422  position.Y <= item.Rect.Y - item.Rect.Height - maxPerpendicularDistance;
423  }
424  else
425  {
426  return
427  position.Y >= item.Rect.Y + Window.Y &&
428  position.Y <= item.Rect.Y + Window.Y + Window.Height &&
429  position.X >= item.Rect.X - maxPerpendicularDistance &&
430  position.X <= item.Rect.Right + maxPerpendicularDistance;
431  }
432  }
433 
434  public override void Update(float deltaTime, Camera cam)
435  {
436  UpdateProjSpecific(deltaTime);
437  toggleCooldownTimer -= deltaTime;
438  damageSoundCooldown -= deltaTime;
439 
440  if (isBroken)
441  {
442  lastBrokenTime = Timing.TotalTime;
443  //the door has to be restored to 50% health before collision detection on the body is re-enabled
444 
445  //multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_
446  //otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider
447  if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
448  (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
449  {
450  IsBroken = false;
451  }
452  return;
453  }
454 
455  bool isClosing = false;
456  if ((!IsStuck && !IsJammed) || !isOpen)
457  {
458  if (PredictedState == null)
459  {
460  OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
461  isClosing = openState > 0.0f && openState < 1.0f && !isOpen;
462  }
463  else
464  {
465  OpenState += deltaTime * ((bool)PredictedState ? OpeningSpeed : -ClosingSpeed);
466  isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState;
467 
468  resetPredictionTimer -= deltaTime;
469  if (resetPredictionTimer <= 0.0f)
470  {
471  PredictedState = null;
472  }
473  }
474  LinkedGap.Open = isBroken ? 1.0f : openState;
475  }
476 
477  if (isClosing)
478  {
479  if (OpenState < 0.9f) { PushCharactersAway(); }
480  if (CheckSubmarinesInDoorWay())
481  {
482  PredictedState = null;
483  isOpen = true;
484  }
485  }
486  else
487  {
488  bool wasEnabled = Body.Enabled;
489  Body.Enabled = Impassable || openState < 1.0f;
490  if (OutsideSubmarineFixture != null)
491  {
492  OutsideSubmarineFixture.CollidesWith = Body.Enabled ? SubmarineBody.CollidesWith : Category.None;
493  }
494  if (wasEnabled && !Body.Enabled && IsHorizontal)
495  {
496  //when opening a hatch, force characters above it to refresh the floor position
497  //(otherwise the character won't fall through the hatch until it moves)
498  foreach (Character c in Character.CharacterList)
499  {
500  if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; }
501  if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; }
503  }
504  }
505  }
506 
507  //don't use the predicted state here, because it might set
508  //other items to an incorrect state if the prediction is wrong
509  item.SendSignal(isOpen ? "1" : "0", "state_out");
510  }
511 
512  partial void UpdateProjSpecific(float deltaTime);
513 
514 
515  public override void UpdateBroken(float deltaTime, Camera cam)
516  {
517  base.UpdateBroken(deltaTime, cam);
518  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
519  {
520  IsBroken = true;
521  }
522  }
523 
524  private void EnableBody()
525  {
526  if (!Impassable)
527  {
528  Body.FarseerBody.SetIsSensor(false);
529  var ce = Body.FarseerBody.ContactList;
530  while (ce != null && ce.Contact != null)
531  {
532  ce.Contact.Enabled = false;
533  ce = ce.Next;
534  }
535  PushCharactersAway();
536  }
537  if (OutsideSubmarineFixture != null && Body.Enabled)
538  {
540  }
541 #if CLIENT
542  UpdateConvexHulls();
543 #endif
544  isBroken = false;
545  }
546 
547  private void DisableBody()
548  {
549  //change the body to a sensor instead of disabling it completely,
550  //because otherwise repairtool raycasts won't hit it
551  if (!Impassable)
552  {
553  Body.FarseerBody.SetIsSensor(true);
554  var ce = Body.FarseerBody.ContactList;
555  while (ce != null && ce.Contact != null)
556  {
557  ce.Contact.Enabled = false;
558  ce = ce.Next;
559  }
560  }
561 
562  if (OutsideSubmarineFixture != null)
563  {
564  OutsideSubmarineFixture.CollidesWith = Category.None;
565  }
566  if (linkedGap != null)
567  {
568  linkedGap.Open = 1.0f;
569  }
570 
571  IsOpen = false;
572 #if CLIENT
573  if (convexHull != null) { convexHull.Enabled = false; }
574  if (convexHull2 != null) { convexHull2.Enabled = false; }
575 #endif
576  }
577 
578  public void RefreshLinkedGap()
579  {
581  LinkedGap.ConnectedDoor = this;
582  if (autoOrientGap)
583  {
585  }
586  LinkedGap.Open = isBroken ? 1.0f : openState;
588  }
589 
590  public override void OnMapLoaded()
591  {
593 #if CLIENT
594  convexHull = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
595  if (Window != Rectangle.Empty)
596  {
597  convexHull2 = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
598  }
599  UpdateConvexHulls();
600 #endif
601  }
602 
603  public override void OnScaleChanged()
604  {
605 #if CLIENT
606  UpdateConvexHulls();
607 #endif
608  if (linkedGap != null)
609  {
611  linkedGap.Rect = item.Rect;
612  }
613  }
614 
615  protected override void RemoveComponentSpecific()
616  {
617  base.RemoveComponentSpecific();
618 
619  if (Body != null)
620  {
621  Body.Remove();
622  Body = null;
623  }
624 
625  foreach (Gap gap in Gap.GapList)
626  {
627  if (gap.ConnectedDoor == this)
628  {
629  gap.ConnectedDoor = null;
630  }
631  }
632 
633  if (OutsideSubmarineFixture != null)
634  {
637  }
638 
639  //no need to remove the gap if we're unloading the whole submarine
640  //otherwise the gap will be removed twice and cause console warnings
641  if (!Submarine.Unloading)
642  {
643  linkedGap?.Remove();
644  }
645  doorSprite?.Remove();
646  weldedSprite?.Remove();
647 
648 #if CLIENT
649  convexHull?.Remove();
650  convexHull2?.Remove();
651 #endif
652 
653  doorList.Remove(this);
654  }
655 
656  private bool CheckSubmarinesInDoorWay()
657  {
658  if (linkedGap != null && linkedGap.IsRoomToRoom) { return false; }
659 
660  Rectangle doorRect = item.WorldRect;
661  if (IsHorizontal)
662  {
663  doorRect.Width = (int)(item.Rect.Width * (1.0f - openState));
664  }
665  else
666  {
667  doorRect.Height = (int)(item.Rect.Height * (1.0f - openState));
668  }
669 
670  foreach (Submarine sub in Submarine.Loaded)
671  {
672  if (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine)) { continue; }
673  Rectangle worldBorders = sub.Borders;
674  worldBorders.Location += sub.WorldPosition.ToPoint();
675  if (!Submarine.RectsOverlap(worldBorders, doorRect)) { continue; }
676  foreach (Hull hull in sub.GetHulls(alsoFromConnectedSubs: false))
677  {
678  if (Submarine.RectsOverlap(hull.WorldRect, doorRect)) { return true; }
679  }
680  }
681  return false;
682  }
683 
684  bool itemPosErrorShown;
685  private readonly HashSet<Character> characterPosErrorShown = new HashSet<Character>();
686  private void PushCharactersAway()
687  {
688  if (!MathUtils.IsValid(item.SimPosition))
689  {
690  if (!itemPosErrorShown)
691  {
692  DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")");
693  GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
694  "Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ").");
695  itemPosErrorShown = true;
696  }
697  return;
698  }
699 
700  Vector2 simPos = ConvertUnits.ToSimUnits(new Vector2(item.Rect.X, item.Rect.Y));
701 
702  Vector2 currSize = IsHorizontal ?
703  new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) :
704  new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState));
705  Vector2 simSize = ConvertUnits.ToSimUnits(currSize);
706 
707  foreach (Character c in Character.CharacterList)
708  {
709  if (!c.Enabled) { continue; }
710  if (!MathUtils.IsValid(c.SimPosition))
711  {
712  if (!characterPosErrorShown.Contains(c))
713  {
714  if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); }
715  GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
716  "Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." +
717  " Removed: " + c.Removed +
718  " Remoteplayer: " + c.IsRemotePlayer);
719  characterPosErrorShown.Add(c);
720  }
721  continue;
722  }
723  int dir = IsHorizontal ? Math.Sign(c.SimPosition.Y - item.SimPosition.Y) : Math.Sign(c.SimPosition.X - item.SimPosition.X);
724 
725  foreach (Limb limb in c.AnimController.Limbs)
726  {
727  if (limb.IsSevered) { continue; }
728  if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f)
729  {
730 #if CLIENT
731  SoundPlayer.PlayDamageSound("LimbBlunt", 1.0f, limb.body);
732 #endif
733  damageSoundCooldown = 0.5f;
734  }
735  }
736  PushBodyOutOfDoorway(c, c.AnimController.Collider, dir, simPos, simSize);
737  }
738  }
739 
740  private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize)
741  {
742  if (!MathUtils.IsValid(body.SimPosition))
743  {
744  DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")");
745  GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
746  "Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + body.SimPosition + ")." +
747  " Removed: " + c.Removed +
748  " Remoteplayer: " + c.IsRemotePlayer);
749  return false;
750  }
751 
752  float diff;
753  if (IsHorizontal)
754  {
755  if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; }
756  diff = body.SimPosition.Y - item.SimPosition.Y;
757  }
758  else
759  {
760  if (body.SimPosition.Y > doorRectSimPos.Y || body.SimPosition.Y < doorRectSimPos.Y - doorRectSimSize.Y) { return false; }
761  diff = body.SimPosition.X - item.SimPosition.X;
762  }
763 
764  //if the limb is at a different side of the door than the character (collider),
765  //immediately teleport it to the correct side
766  if (Math.Sign(diff) != dir)
767  {
768  if (IsHorizontal)
769  {
770  body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
771  }
772  else
773  {
774  body.SetTransform(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
775  }
776  }
777 
778  //apply an impulse to push the limb further from the door
779  if (IsHorizontal)
780  {
781  if (Math.Abs(body.SimPosition.Y - item.SimPosition.Y) > doorRectSimSize.Y * 0.5f) { return false; }
782  body.ApplyLinearImpulse(new Vector2(isOpen ? 0.0f : 1.0f, dir * 2.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
783  }
784  else
785  {
786  if (Math.Abs(body.SimPosition.X - item.SimPosition.X) > doorRectSimSize.X * 0.5f) { return false; }
787  body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
788  }
789 
790  //don't stun if the door was broken a moment ago
791  //otherwise enabling the door's collider and pushing the character away will interrupt repairing
792  if (lastBrokenTime < Timing.TotalTime - 1.0f)
793  {
794  c.SetStun(0.2f);
795  }
796  return true;
797  }
798 
799  partial void OnFailedToOpen();
800 
801  public override bool HasAccess(Character character)
802  {
803  if (!item.IsInteractable(character)) { return false; }
805  {
806  return base.HasAccess(character);
807  }
808  else
809  {
810  return base.HasAccess(character) && Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character));
811  }
812  }
813 
814  public override void ReceiveSignal(Signal signal, Connection connection)
815  {
816  if (IsStuck || IsJammed) { return; }
817 
818  bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
819 
820  if (connection.Name == "toggle")
821  {
822  if (signal.value == "0") { return; }
823  if (toggleCooldownTimer > 0.0f && signal.sender != lastUser) { OnFailedToOpen(); return; }
824  if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
825  toggleCooldownTimer = ToggleCoolDown;
826  lastUser = signal.sender;
827  SetState(!wasOpen, false, true, forcedOpen: false);
828  }
829  else if (connection.Name == "set_state")
830  {
831  bool signalOpen = signal.value != "0";
832  if (IsStuck && signalOpen != wasOpen) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
833  SetState(signalOpen, false, true, forcedOpen: false);
834  }
835 
836 #if SERVER
837  if (signal.sender != null && wasOpen != isOpen)
838  {
839  GameServer.Log(GameServer.CharacterLogName(signal.sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
840  }
841 #endif
842  }
843 
844  public void TrySetState(bool open, bool isNetworkMessage, bool sendNetworkMessage = false)
845  {
846  SetState(open, isNetworkMessage, sendNetworkMessage, forcedOpen: false);
847  }
848 
849  partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen);
850  }
851 }
bool GetAttributeBool(string key, bool def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static NetworkMember NetworkMember
Definition: GameMain.cs:190
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
bool FullyInitialized
Has everything in the item been loaded/instantiated/initialized (basically, can be used to check if t...
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
void SendSignal(string signal, string connectionName)
override void Move(Vector2 amount, bool ignoreContacts=false)
override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance=10.0f)
Is the given position inside the vertical bounds of the window, and roughly on the door horizontally?...
override bool Pick(Character picker)
a Character has picked the item
override void ReceiveSignal(Signal signal, Connection connection)
override bool HasAccess(Character character)
Only checks if any of the Picked requirements are matched (used for checking id card(s))....
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
void TrySetState(bool open, bool isNetworkMessage, bool sendNetworkMessage=false)
bool ShouldBeOpen
Can be used by status effects to tell the door to open (setting IsOpen directly would make it immedia...
virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
string GetTextureDirectory(ContentXElement subElement)
Dictionary< RelatedItem.RelationType, List< RelatedItem > > RequiredItems
List< InvSlotType > allowedSlots
Definition: Pickable.cs:14
void StopPicking(Character picker)
Definition: Pickable.cs:232
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform=true)
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19