Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Structure.cs
3 using FarseerPhysics;
4 using FarseerPhysics.Dynamics;
5 using FarseerPhysics.Dynamics.Contacts;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Linq;
10 using System.Xml.Linq;
11 using System.Collections.Immutable;
12 using Barotrauma.Abilities;
13 #if CLIENT
14 using System.Diagnostics;
15 using Microsoft.Xna.Framework.Graphics;
16 using Barotrauma.Lights;
17 #endif
18 
19 namespace Barotrauma
20 {
21  partial class WallSection : IIgnorable
22  {
23  public Rectangle rect;
24  public float damage;
25  public Gap gap;
26 
27  public bool NoPhysicsBody;
28 
29  public Structure Wall { get; }
30  public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this));
31  public Vector2 WorldPosition => Wall.SectionPosition(Wall.Sections.IndexOf(this), world: true);
32  public Vector2 SimPosition => ConvertUnits.ToSimUnits(Position);
34  public Rectangle WorldRect => Submarine == null ? rect :
35  new Rectangle((int)(rect.X + Submarine.Position.X), (int)(rect.Y + Submarine.Position.Y), rect.Width, rect.Height);
36  public bool IgnoreByAI(Character character) => OrderedToBeIgnored && character.IsOnPlayerTeam;
37  public bool OrderedToBeIgnored { get; set; }
38 
39  public WallSection(Rectangle rect, Structure wall, float damage = 0.0f)
40  {
41  System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0);
42  this.rect = rect;
43  this.damage = damage;
44  Wall = wall;
45  }
46  }
47 
49  {
50  public const int WallSectionSize = 96;
51  public static List<Structure> WallList = new List<Structure>();
52 
53  const float LeakThreshold = 0.1f;
54  const float BigGapThreshold = 0.7f;
55 
56 #if CLIENT
58 #endif
59 
60  //dimensions of the wall sections' physics bodies (only used for debug rendering)
61  private readonly Dictionary<Body, Vector2> bodyDimensions = new Dictionary<Body, Vector2>();
62 
63  private static Explosion explosionOnBroken;
64 
66  public bool Indestructible
67  {
68  get;
69  set;
70  }
71 
72  //sections of the wall that are supposed to be rendered
74  {
75  get;
76  private set;
77  }
78 
79  public override Sprite Sprite
80  {
81  get { return base.Prefab.Sprite; }
82  }
83 
84  public bool IsPlatform
85  {
86  get { return Prefab.Platform; }
87  }
88 
90  {
91  get;
92  private set;
93  }
94 
95  public override string Name
96  {
97  get { return base.Prefab.Name.Value; }
98  }
99 
100  public bool HasBody
101  {
102  get { return Prefab.Body; }
103  }
104 
105  public List<Body> Bodies { get; private set; }
106 
108  public bool CastShadow
109  {
110  get;
111  set;
112  }
113 
114  public bool IsHorizontal { get; }
115 
116  public int SectionCount
117  {
118  get { return Sections.Length; }
119  }
120 
121  private float? maxHealth;
122 
123  [Serialize(100.0f, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody, MinValueFloat = 0)]
124  public float MaxHealth
125  {
126  get => maxHealth ?? Prefab.Health;
127  set => maxHealth = value;
128  }
129 
130  private float crushDepth;
131 
133  public float CrushDepth
134  {
135  get => crushDepth;
136  set => crushDepth = Math.Max(value, Level.DefaultRealWorldCrushDepth);
137  }
138 
139  public float Health => MaxHealth;
140 
141  public override bool DrawBelowWater
142  {
143  get
144  {
145  return base.DrawBelowWater || Prefab.BackgroundSprite != null;
146  }
147  }
148 
149  public override bool DrawOverWater
150  {
151  get
152  {
153  return (Sprite == null || SpriteDepth <= 0.5f) && !DrawDamageEffect;
154  }
155  }
156 
157  public bool DrawDamageEffect
158  {
159  get
160  {
161  return Prefab.Body && !IsPlatform;// && HasDamage;
162  }
163  }
164 
165  public bool HasDamage
166  {
167  get;
168  private set;
169  }
170 
171  public new StructurePrefab Prefab => base.Prefab as StructurePrefab;
172 
173  public ImmutableHashSet<Identifier> Tags => Prefab.Tags;
174 
175 #if DEBUG
177 #else
178  [Serialize("", IsPropertySaveable.Yes)]
179 #endif
180  public string SpecialTag
181  {
182  get;
183  set;
184  }
185 
186  protected Color spriteColor;
187  [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)]
188  public Color SpriteColor
189  {
190  get { return spriteColor; }
191  set { spriteColor = value; }
192  }
193 
195  public bool UseDropShadow
196  {
197  get;
198  private set;
199  }
200 
201  [ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody), Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")]
202  public Vector2 DropShadowOffset
203  {
204  get;
205  private set;
206  }
207 
208  private float scale = 1.0f;
209  public override float Scale
210  {
211  get { return scale; }
212  set
213  {
214  if (scale == value) { return; }
215  scale = MathHelper.Clamp(value, 0.1f, 10.0f);
216 
217  float relativeScale = scale / base.Prefab.Scale;
218 
220  {
221  int newWidth = Math.Max(ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale), 1);
222  int newHeight = Math.Max(ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale), 1);
223  Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight);
224  if (StairDirection != Direction.None)
225  {
226  CreateStairBodies();
227  }
228  else if (Sections != null)
229  {
230  UpdateSections();
231  }
232  }
233 
234 #if CLIENT
235  foreach (LightSource light in Lights)
236  {
237  light.SpriteScale = scale * textureScale;
238  }
239 #endif
240  }
241  }
242 
243  protected float rotationRad = 0f;
244  [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
245  public float Rotation
246  {
247  get => MathHelper.ToDegrees(rotationRad);
248  set
249  {
250  rotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
251  if (StairDirection != Direction.None)
252  {
253  CreateStairBodies();
254  }
255  else if (Prefab.Body)
256  {
257  CreateSections();
258  UpdateSections();
259  }
260  }
261  }
262 
263  protected Vector2 textureScale = Vector2.One;
264 
265  [Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", IsPropertySaveable.No)]
266  public Vector2 TextureScale
267  {
268  get { return textureScale; }
269  set
270  {
271  textureScale = new Vector2(
272  MathHelper.Clamp(value.X, 0.01f, 10),
273  MathHelper.Clamp(value.Y, 0.01f, 10));
274 
275 #if CLIENT
276  foreach (LightSource light in Lights)
277  {
278  light.LightTextureScale = textureScale * scale;
279  }
280 #endif
281  }
282  }
283 
284  public float ScaleWhenTextureOffsetSet { get; private set; } = 1.0f;
285 
286  protected Vector2 textureOffset = Vector2.Zero;
287  [Editable(ForceShowPlusMinusButtons = true, ValueStep = 1f), Serialize("0.0, 0.0", IsPropertySaveable.Yes)]
288  public Vector2 TextureOffset
289  {
290  get { return textureOffset; }
291  set
292  {
293  textureOffset = value;
294  textureOffset.X =
295  MathUtils.PositiveModulo(textureOffset.X, Sprite.SourceRect.Width * TextureScale.X * Scale);
296  textureOffset.Y =
297  MathUtils.PositiveModulo(textureOffset.Y, Sprite.SourceRect.Height * TextureScale.Y * Scale);
299 #if CLIENT
300  SetLightTextureOffset();
301 #endif
302  }
303  }
304 
305 
306  private Rectangle defaultRect;
311  {
312  get { return defaultRect; }
313  set { defaultRect = value; }
314  }
315 
316  public override Rectangle Rect
317  {
318  get
319  {
320  return base.Rect;
321  }
322  set
323  {
324  Rectangle oldRect = Rect;
325  base.Rect = value;
326  if (Prefab.Body)
327  {
328  CreateSections();
329  UpdateSections();
330  }
331  else
332  {
333  if (Sections == null) { return; }
334  foreach (WallSection sec in Sections)
335  {
336  Rectangle secRect = sec.rect;
337  secRect.X -= oldRect.X; secRect.Y -= oldRect.Y;
338  secRect.X *= value.Width; secRect.X /= oldRect.Width;
339  secRect.Y *= value.Height; secRect.Y /= oldRect.Height;
340  secRect.Width *= value.Width; secRect.Width /= oldRect.Width;
341  secRect.Height *= value.Height; secRect.Height /= oldRect.Height;
342  secRect.X += value.X; secRect.Y += value.Y;
343  sec.rect = secRect;
344  }
345  }
346  }
347  }
348 
349  public float BodyWidth
350  {
351  get { return Prefab.BodyWidth > 0.0f ? Prefab.BodyWidth * scale : rect.Width; }
352  }
353  public float BodyHeight
354  {
355  get { return Prefab.BodyHeight > 0.0f ? Prefab.BodyHeight * scale : rect.Height; }
356  }
357 
361  public float BodyRotation
362  {
363  get
364  {
365  float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.rotationRad;
366  if (IsHorizontal)
367  {
368  if (FlippedX) { rotation = -MathHelper.Pi - rotation; }
369  if (FlippedY) { rotation = -rotation; }
370  }
371  else
372  {
373  if (FlippedX) { rotation = -rotation; }
374  if (FlippedY) { rotation = -MathHelper.Pi -rotation; }
375  }
376  rotation = MathHelper.WrapAngle(rotation);
377  return rotation;
378  }
379  }
383  public Vector2 BodyOffset
384  {
385  get
386  {
387  Vector2 bodyOffset = Prefab.BodyOffset;
388  if (rotationRad != 0f)
389  {
390  bodyOffset = MathUtils.RotatePoint(bodyOffset, -rotationRad);
391  }
392  if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
393  if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
394  return bodyOffset;
395  }
396  }
397 
398  [Serialize(false, IsPropertySaveable.Yes), Editable]
399  public bool NoAITarget
400  {
401  get;
402  private set;
403  }
404 
405  public Dictionary<Identifier, SerializableProperty> SerializableProperties
406  {
407  get;
408  private set;
409  }
410 
411  public override void Move(Vector2 amount, bool ignoreContacts = true)
412  {
413  if (!MathUtils.IsValid(amount))
414  {
415  DebugConsole.ThrowError($"Attempted to move a structure by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
416  return;
417  }
418 
419  base.Move(amount, ignoreContacts);
420 
421  for (int i = 0; i < Sections.Length; i++)
422  {
423  Rectangle r = Sections[i].rect;
424  r.X += (int)amount.X;
425  r.Y += (int)amount.Y;
426  Sections[i].rect = r;
427  }
428 
429  if (Bodies != null)
430  {
431  Vector2 simAmount = ConvertUnits.ToSimUnits(amount);
432  foreach (Body b in Bodies)
433  {
434  Vector2 pos = b.Position + simAmount;
435  if (ignoreContacts)
436  {
437  b.SetTransformIgnoreContacts(ref pos, b.Rotation);
438  }
439  else
440  {
441  b.SetTransform(pos, b.Rotation);
442  }
443  }
444  }
445 
446 #if CLIENT
447  convexHulls?.ForEach(x => x.Move(amount));
448 
449  foreach (LightSource light in Lights)
450  {
451  light.LightTextureTargetSize = rect.Size.ToVector2();
452  light.Position = rect.Location.ToVector2();
453  }
454 #endif
455  }
456 
457  public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id = Entity.NullEntityID, XElement element = null)
458  : base(sp, submarine, id)
459  {
460  System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0);
461  if (rectangle.Width == 0 || rectangle.Height == 0) { return; }
462  defaultRect = rectangle;
463 
464  maxHealth = sp.Health;
465 
466  rect = rectangle;
468 
469  spriteColor = base.Prefab.SpriteColor;
470  if (sp.IsHorizontal.HasValue)
471  {
472  IsHorizontal = sp.IsHorizontal.Value;
473  }
474  else if (ResizeHorizontal && !ResizeVertical)
475  {
476  IsHorizontal = true;
477  }
478  else if (ResizeVertical && !ResizeHorizontal)
479  {
480  IsHorizontal = false;
481  }
482  else
483  {
484  float width = BodyWidth > 0.0f ? BodyWidth : rect.Width;
485  float height = BodyHeight > 0.0f ? BodyHeight : rect.Height;
486  if (BodyWidth > 0.0f && BodyHeight > 0.0f)
487  {
488  IsHorizontal = width > height;
489  }
490  }
491 
492  StairDirection = Prefab.StairDirection;
493  NoAITarget = Prefab.NoAITarget;
494 
495  InitProjSpecific();
496 
498  if (element?.GetAttribute(nameof(CastShadow)) == null)
499  {
500  CastShadow = Prefab.CastShadow;
501  }
502 
503  if (Prefab.Body)
504  {
505  Bodies = new List<Body>();
506  WallList.Add(this);
507  CreateSections();
508  UpdateSections();
509  }
510  else if (StairDirection != Direction.None)
511  {
512  CreateStairBodies();
513  }
514  if (Sections == null)
515  {
516  Sections = new WallSection[1];
517  Sections[0] = new WallSection(rect, this);
518  }
519 
520 #if CLIENT
521  foreach (var subElement in sp.ConfigElement.Elements())
522  {
523  if (subElement.Name.ToString().Equals("light", StringComparison.OrdinalIgnoreCase))
524  {
525  Vector2 pos = rect.Location.ToVector2();
526  pos.Y += rect.Height;
527  LightSource light = new LightSource(subElement)
528  {
529  ParentSub = Submarine,
530  Position = rect.Location.ToVector2(),
531  CastShadows = false,
532  IsBackground = false,
533  Color = subElement.GetAttributeColor("lightcolor", Color.White),
534  SpriteScale = Vector2.One,
535  Range = 0,
536  LightTextureTargetSize = rect.Size.ToVector2(),
537  LightTextureScale = textureScale * scale,
539  {
540  Flicker = subElement.GetAttributeFloat("flicker", 0f),
541  FlickerSpeed = subElement.GetAttributeFloat("flickerspeed", 0f),
542  PulseAmount = subElement.GetAttributeFloat("pulseamount", 0f),
543  PulseFrequency = subElement.GetAttributeFloat("pulsefrequency", 0f),
544  BlinkFrequency = subElement.GetAttributeFloat("blinkfrequency", 0f)
545  }
546  };
547 
548  Lights.Add(light);
549 
550  SetLightTextureOffset();
551  }
552  }
553 #endif
554 
555  // Only add ai targets automatically to submarine/outpost walls
556  if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget)
557  {
558  aiTarget = new AITarget(this)
559  {
560  MinSightRange = 1000,
561  MaxSightRange = 4000,
562  MaxSoundRange = 0
563  };
564  }
565 
566  InsertToList();
567 
568  DebugConsole.Log("Created " + Name + " (" + ID + ")");
569  }
570 
571  partial void InitProjSpecific();
572 
573  public override string ToString()
574  {
575  return Name;
576  }
577 
578  public override MapEntity Clone()
579  {
580  var clone = new Structure(rect, Prefab, Submarine)
581  {
582  defaultRect = defaultRect
583  };
584  foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
585  {
586  if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
587  clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
588  }
589  if (FlippedX) clone.FlipX(false);
590  if (FlippedY) clone.FlipY(false);
591 
592  return clone;
593  }
594 
595  private void CreateStairBodies()
596  {
597  Bodies = new List<Body>();
598  bodyDimensions.Clear();
599 
600  float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f));
601 
602  float bodyWidth = ConvertUnits.ToSimUnits(rect.Width / Math.Cos(stairAngle));
603  float bodyHeight = ConvertUnits.ToSimUnits(10);
604 
605  float stairHeight = rect.Width * (float)Math.Tan(stairAngle);
606 
607  Body newBody = GameMain.World.CreateRectangle(bodyWidth, bodyHeight, 1.5f);
608 
609  var rotationWithFlip = FlippedX ^ FlippedY ? -rotationRad : rotationRad;
610 
611  newBody.BodyType = BodyType.Static;
612  Vector2 stairRectHeightDiff = new Vector2(0f, stairHeight / 2.0f - rect.Height / 2.0f);
613  stairRectHeightDiff = MathUtils.RotatePoint(stairRectHeightDiff, -rotationWithFlip);
614  if (FlippedY) { stairRectHeightDiff = -stairRectHeightDiff; }
615  Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height / 2.0f) + stairRectHeightDiff;
616  newBody.Rotation = ((StairDirection == Direction.Right) ? stairAngle : -stairAngle) - rotationWithFlip;
617  newBody.CollisionCategories = Physics.CollisionStairs;
618  newBody.Friction = 0.8f;
619  newBody.UserData = this;
620 
621  newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale;
622 
623  bodyDimensions.Add(newBody, new Vector2(bodyWidth, bodyHeight));
624 
625  Bodies.Add(newBody);
626  }
627 
628  private void CreateSections()
629  {
630  int xsections = 1, ysections = 1;
631  int width = rect.Width, height = rect.Height;
632 
633  WallSection[] prevSections = null;
634  if (Sections != null)
635  {
636  prevSections = Sections.ToArray();
637  }
638  if (!HasBody)
639  {
640  if (FlippedX && IsHorizontal)
641  {
642  xsections = (int)Math.Ceiling((float)rect.Width / base.Prefab.Sprite.SourceRect.Width);
643  width = base.Prefab.Sprite.SourceRect.Width;
644  }
645  else if (FlippedY && !IsHorizontal)
646  {
647  ysections = (int)Math.Ceiling((float)rect.Height / base.Prefab.Sprite.SourceRect.Height);
648  width = base.Prefab.Sprite.SourceRect.Height;
649  }
650  else
651  {
652  xsections = 1;
653  ysections = 1;
654  }
655  Sections = new WallSection[xsections];
656  }
657  else
658  {
659  if (IsHorizontal)
660  {
661  //equivalent to (int)Math.Ceiling((double)rect.Width / WallSectionSize) without the potential for floating point indeterminism
662  xsections = (rect.Width + WallSectionSize - 1) / WallSectionSize;
663  Sections = new WallSection[xsections];
664  width = WallSectionSize;
665  }
666  else
667  {
668  ysections = (rect.Height + WallSectionSize - 1) / WallSectionSize;
669  Sections = new WallSection[ysections];
670  height = WallSectionSize;
671  }
672  }
673 
674  for (int x = 0; x < xsections; x++)
675  {
676  for (int y = 0; y < ysections; y++)
677  {
678  if (FlippedX || FlippedY)
679  {
680  Rectangle sectionRect = new Rectangle(
681  FlippedX ? rect.Right - (x + 1) * width : rect.X + x * width,
682  FlippedY ? rect.Y - rect.Height + (y + 1) * height : rect.Y - y * height,
683  width, height);
684 
685  if (FlippedX)
686  {
687  int over = Math.Max(rect.X - sectionRect.X, 0);
688  sectionRect.X += over;
689  sectionRect.Width -= over;
690  }
691  else
692  {
693  sectionRect.Width -= (int)Math.Max(sectionRect.Right - rect.Right, 0.0f);
694  }
695  if (FlippedY)
696  {
697  int over = Math.Max(sectionRect.Y - rect.Y, 0);
698  sectionRect.Y -= over;
699  sectionRect.Height -= over;
700  }
701  else
702  {
703  sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
704  }
705 
706  //sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
707  int xIndex = FlippedX && IsHorizontal ? (xsections - 1 - x) : x;
708  int yIndex = FlippedY && !IsHorizontal ? (ysections - 1 - y) : y;
709  Sections[xIndex + yIndex] = new WallSection(sectionRect, this);
710  }
711  else
712  {
713  Rectangle sectionRect = new Rectangle(rect.X + x * width, rect.Y - y * height, width, height);
714  sectionRect.Width -= (int)Math.Max(sectionRect.Right - rect.Right, 0.0f);
715  sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
716 
717  Sections[x + y] = new WallSection(sectionRect, this);
718  }
719  }
720  }
721 
722  if (prevSections != null && Sections.Length == prevSections.Length)
723  {
724  for (int i = 0; i < Sections.Length; i++)
725  {
726  Sections[i].damage = prevSections[i].damage;
727  }
728  }
729  }
730 
731  private Rectangle GenerateMergedRect(List<WallSection> mergedSections)
732  {
733  if (IsHorizontal)
734  return new Rectangle(mergedSections.Min(x => x.rect.Left), mergedSections.Max(x => x.rect.Top),
735  mergedSections.Sum(x => x.rect.Width), mergedSections.First().rect.Height);
736  else
737  {
738  return new Rectangle(mergedSections.Min(x => x.rect.Left), mergedSections.Max(x => x.rect.Top),
739  mergedSections.First().rect.Width, mergedSections.Sum(x => x.rect.Height));
740  }
741  }
742 
743  public override Quad2D GetTransformedQuad()
744  => Quad2D.FromSubmarineRectangle(rect).Rotated(
745  FlippedX != FlippedY
746  ? rotationRad
747  : -rotationRad);
748 
752  public static Structure GetAttachTarget(Vector2 worldPosition)
753  {
754  foreach (MapEntity mapEntity in MapEntityList)
755  {
756  if (!(mapEntity is Structure structure)) { continue; }
757  if (!structure.Prefab.AllowAttachItems) { continue; }
758  if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; }
759  Rectangle worldRect = mapEntity.WorldRect;
760  if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; }
761  if (worldPosition.Y > worldRect.Y || worldPosition.Y < worldRect.Y - worldRect.Height) { continue; }
762  return structure;
763  }
764  return null;
765  }
766 
767  public override bool IsMouseOn(Vector2 position)
768  {
769  if (StairDirection == Direction.None)
770  {
771  Vector2 rectSize = rect.Size.ToVector2();
772  if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; }
773  if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; }
774 
775  Vector2 bodyPos = WorldPosition + BodyOffset * Scale;
776 
777  Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation);
778 
779  return
780  Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
781  Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f;
782  }
783  else
784  {
785  Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(
786  position,
787  WorldRect.Location.ToVector2() + WorldRect.Size.ToVector2().FlipY() * 0.5f,
788  BodyRotation);
789 
790  if (!Submarine.RectContains(WorldRect, position)) { return false; }
791  if (StairDirection == Direction.Left)
792  {
793  return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), transformedMousePos) < 1600.0f;
794  }
795  else
796  {
797  return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), transformedMousePos) < 1600.0f;
798  }
799  }
800  }
801 
802  public override void ShallowRemove()
803  {
804  base.ShallowRemove();
805 
806  if (WallList.Contains(this)) WallList.Remove(this);
807 
808  if (Bodies != null)
809  {
810  foreach (Body b in Bodies)
811  {
812  GameMain.World.Remove(b);
813  }
814  Bodies.Clear();
815  }
816 
817  if (Sections != null)
818  {
819  foreach (WallSection s in Sections)
820  {
821  if (s.gap != null)
822  {
823  s.gap.Remove();
824  s.gap = null;
825  }
826  }
827  }
828 
829 #if CLIENT
830  if (convexHulls != null) convexHulls.ForEach(x => x.Remove());
831  foreach (LightSource light in Lights)
832  {
833  light.Remove();
834  }
835 #endif
836  }
837 
838  public override void Remove()
839  {
840  base.Remove();
841 
842  if (WallList.Contains(this)) WallList.Remove(this);
843 
844  if (Bodies != null)
845  {
846  foreach (Body b in Bodies)
847  {
848  GameMain.World.Remove(b);
849  }
850  Bodies.Clear();
851  }
852 
853  if (Sections != null)
854  {
855  foreach (WallSection s in Sections)
856  {
857  if (s.gap != null)
858  {
859  s.gap.Remove();
860  s.gap = null;
861  }
862  }
863  }
864 
865 #if CLIENT
866  if (convexHulls != null) convexHulls.ForEach(x => x.Remove());
867  foreach (LightSource light in Lights)
868  {
869  light.Remove();
870  }
871 #endif
872  }
873 
874  private bool OnWallCollision(Fixture f1, Fixture f2, Contact contact)
875  {
876  if (Prefab.Platform)
877  {
878  if (f2.Body.UserData is Limb limb)
879  {
880  if (limb.character.AnimController.IgnorePlatforms) return false;
881  }
882  }
883 
884  if (f2.Body.UserData is Limb)
885  {
886  var character = ((Limb)f2.Body.UserData).character;
887  if (character.DisableImpactDamageTimer > 0.0f || ((Limb)f2.Body.UserData).Mass < 100.0f) return true;
888  }
889 
890  OnImpactProjSpecific(f1, f2, contact);
891 
892  return true;
893  }
894 
895  partial void OnImpactProjSpecific(Fixture f1, Fixture f2, Contact contact);
896 
897  public WallSection GetSection(int sectionIndex)
898  {
899  if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return null; }
900  return Sections[sectionIndex];
901 
902  }
903 
904  public bool SectionBodyDisabled(int sectionIndex)
905  {
906  if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; }
907  return (Sections[sectionIndex].damage >= MaxHealth);
908  }
909 
911  {
912  for (int i = 0; i < Sections.Length; i++)
913  {
914  if (Sections[i].damage < MaxHealth) { return false; }
915  }
916  return true;
917  }
918 
922  public bool SectionIsLeaking(int sectionIndex)
923  {
924  if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; }
925  return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold;
926  }
927 
928  public bool SectionIsLeakingFromOutside(int sectionIndex)
929  {
930  if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; }
931  return SectionIsLeaking(sectionIndex) && Sections[sectionIndex].gap is { IsRoomToRoom: false };
932  }
933 
934  public int SectionLength(int sectionIndex)
935  {
936  if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0;
937 
938  return (IsHorizontal ? Sections[sectionIndex].rect.Width : Sections[sectionIndex].rect.Height);
939  }
940 
941  public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false)
942  {
943  if (!upgrade.Prefab.IsWallUpgrade) { return false; }
944 
945  Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier);
946 
947  if (existingUpgrade != null)
948  {
949  existingUpgrade.Level += upgrade.Level;
950  existingUpgrade.ApplyUpgrade();
951  upgrade.Dispose();
952  }
953  else
954  {
955  Upgrades.Add(upgrade);
956  upgrade.ApplyUpgrade();
957  }
958 
959  UpdateSections();
960 
961  return true;
962  }
963 
964  public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true, bool createWallDamageProjectiles = false)
965  {
966  if (!Prefab.Body || Prefab.Platform || Indestructible) { return; }
967 
968  if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; }
969 
970  var section = Sections[sectionIndex];
971  float prevDamage = section.damage;
972  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
973  {
974  SetDamage(sectionIndex, section.damage + damage, attacker, createWallDamageProjectiles: createWallDamageProjectiles);
975  }
976 #if CLIENT
977  if (damage > 0 && emitParticles)
978  {
979  float dmg = Math.Min(section.damage - prevDamage, damage);
980  float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f)));
981  // Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle
982  if (particleAmount < 1 && Rand.Value() < 0.10f)
983  {
984  particleAmount = 1;
985  }
986  for (int i = 1; i <= particleAmount; i++)
987  {
988  var worldRect = section.WorldRect;
989  var directionUnitX = MathUtils.RotatedUnitXRadians(BodyRotation);
990  var directionUnitY = directionUnitX.YX().FlipX();
991  Vector2 particlePos = new Vector2(
992  Rand.Range(0, worldRect.Width + 1),
993  Rand.Range(-worldRect.Height, 1));
994  particlePos -= worldRect.Size.ToVector2().FlipY() * 0.5f;
995 
996  var particlePosFinal = SectionPosition(sectionIndex, world: true);
997  particlePosFinal += particlePos.X * directionUnitX + particlePos.Y * directionUnitY;
998 
999  var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle,
1000  position: particlePosFinal,
1001  velocity: Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f);
1002  if (particle == null) { break; }
1003  }
1004  }
1005 #endif
1006  }
1007 
1008  public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false)
1009  {
1010  if (Sections.None()) { return -1; }
1011 
1012  if (world && Submarine != null)
1013  {
1014  displayPos -= Submarine.Position;
1015  }
1016 
1017  //if the sub has been flipped horizontally, the first section may be smaller than wallSectionSize
1018  //and we need to adjust the position accordingly
1019  if (IsHorizontal)
1020  {
1021  if (Sections[0].rect.Width < WallSectionSize)
1022  {
1023  displayPos += DirectionUnit * (WallSectionSize - Sections[0].rect.Width);
1024  }
1025  }
1026  else
1027  {
1028  if (Sections[0].rect.Height < WallSectionSize)
1029  {
1030  displayPos += DirectionUnit * (WallSectionSize - Sections[0].rect.Height);
1031  }
1032  }
1033 
1034  var leftmostPos = Position - DirectionUnit * (IsHorizontal ? Rect.Width : Rect.Height) * 0.5f;
1035  int index = (int)Math.Floor(Vector2.Dot(DirectionUnit, displayPos - leftmostPos) / WallSectionSize);
1036 
1037  if (clamp)
1038  {
1039  index = MathHelper.Clamp(index, 0, Sections.Length - 1);
1040  }
1041  else if (index < 0 || index > Sections.Length - 1)
1042  {
1043  return -1;
1044  }
1045  return index;
1046  }
1047 
1048  public float SectionDamage(int sectionIndex)
1049  {
1050  if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0.0f;
1051 
1052  return Sections[sectionIndex].damage;
1053  }
1054 
1055  protected Vector2 DirectionUnit
1056  {
1057  get
1058  {
1059  var rotation = IsHorizontal ? -BodyRotation : -MathHelper.PiOver2 - BodyRotation;
1060  if (IsHorizontal && FlippedX) { rotation += MathF.PI; }
1061  if (!IsHorizontal && FlippedY) { rotation += MathF.PI; }
1062  return MathUtils.RotatedUnitXRadians(rotation);
1063  }
1064  }
1065 
1066  public Vector2 SectionPosition(int sectionIndex, bool world = false)
1067  {
1068  if (sectionIndex < 0 || sectionIndex >= Sections.Length)
1069  {
1070  return Vector2.Zero;
1071  }
1072 
1073  if (MathUtils.NearlyEqual(BodyRotation, 0f))
1074  {
1075  Vector2 sectionPos = new Vector2(
1076  Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f,
1077  Sections[sectionIndex].rect.Y - Sections[sectionIndex].rect.Height / 2.0f);
1078 
1079  if (world && Submarine != null)
1080  {
1081  sectionPos += Submarine.Position;
1082  }
1083  return sectionPos;
1084  }
1085  else
1086  {
1087  Rectangle sectionRect = Sections[sectionIndex].rect;
1088  float diffFromCenter;
1089  if (IsHorizontal)
1090  {
1091  diffFromCenter = (sectionRect.Center.X - rect.Center.X) / (float)rect.Width * BodyWidth;
1092  }
1093  else
1094  {
1095  diffFromCenter = ((sectionRect.Y - sectionRect.Height / 2) - (rect.Y - rect.Height / 2)) / (float)rect.Height * BodyHeight;
1096  diffFromCenter = -diffFromCenter;
1097  }
1098 
1099  Vector2 sectionPos = Position + DirectionUnit * diffFromCenter;
1100 
1101  if (world && Submarine != null)
1102  {
1103  sectionPos += Submarine.Position;
1104  }
1105  return sectionPos;
1106  }
1107  }
1108 
1109  public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = false)
1110  {
1111  if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); }
1112  if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); }
1113 
1114  Vector2 transformedPos = worldPosition;
1115  if (Submarine != null) { transformedPos -= Submarine.Position; }
1116 
1117  if (!MathUtils.NearlyEqual(BodyRotation, 0f))
1118  {
1119  var center = Rect.Location.ToVector2() + Rect.Size.ToVector2().FlipY() * 0.5f;
1120  var rotation = BodyRotation;
1121  if (IsHorizontal && FlippedX) { rotation += MathF.PI; }
1122  if (!IsHorizontal && FlippedY) { rotation += MathF.PI; }
1123  transformedPos = MathUtils.RotatePointAroundTarget(transformedPos, center, rotation);
1124  }
1125 
1126  float damageAmount = 0.0f;
1127  for (int i = 0; i < SectionCount; i++)
1128  {
1129  Rectangle sectionRect = Sections[i].rect;
1130  sectionRect.Y -= Sections[i].rect.Height;
1131  if (MathUtils.CircleIntersectsRectangle(transformedPos, attack.DamageRange, sectionRect))
1132  {
1133  damageAmount = attack.GetStructureDamage(deltaTime);
1134  AddDamage(i, damageAmount, attacker, createWallDamageProjectiles: attack.CreateWallDamageProjectiles);
1135 #if CLIENT
1136  if (attack.EmitStructureDamageParticles)
1137  {
1138  GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f);
1139  }
1140 #endif
1141  }
1142  }
1143 #if CLIENT
1144  if (playSound && damageAmount > 0)
1145  {
1146  string damageSound = Prefab.DamageSound;
1147  if (string.IsNullOrWhiteSpace(damageSound))
1148  {
1149  damageSound = attack.StructureSoundType;
1150  }
1151  SoundPlayer.PlayDamageSound(damageSound, damageAmount, worldPosition, tags: Tags);
1152  }
1153 #endif
1154 
1155  if (Submarine != null && damageAmount > 0 && attacker != null)
1156  {
1157  var abilityAttackerSubmarine = new AbilityAttackerSubmarine(attacker, Submarine);
1158  foreach (Character character in Character.CharacterList)
1159  {
1160  character.CheckTalents(AbilityEffectType.AfterSubmarineAttacked, abilityAttackerSubmarine);
1161  }
1162  }
1163 
1164  return new AttackResult(damageAmount, null);
1165  }
1166 
1167  public void SetDamage(int sectionIndex, float damage, Character attacker = null,
1168  bool createNetworkEvent = true,
1169  bool isNetworkEvent = true,
1170  bool createExplosionEffect = true,
1171  bool createWallDamageProjectiles = false)
1172  {
1173  if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; }
1174  if (!Prefab.Body) { return; }
1175  if (!MathUtils.IsValid(damage)) { return; }
1176 
1177  damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
1178 
1179  if (Sections[sectionIndex].NoPhysicsBody) { return; }
1180 
1181 #if SERVER
1182  if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
1183  {
1184  GameMain.Server.CreateEntityEvent(this);
1185  }
1186  bool noGaps = true;
1187  for (int i = 0; i < Sections.Length; i++)
1188  {
1189  if (i != sectionIndex && SectionIsLeaking(i))
1190  {
1191  noGaps = false;
1192  break;
1193  }
1194  }
1195 #endif
1196 
1197  if (damage < MaxHealth * LeakThreshold)
1198  {
1199  if (Sections[sectionIndex].gap != null)
1200  {
1201 #if SERVER
1202  //the structure doesn't have any other gap, log the structure being fixed
1203  if (noGaps && attacker != null)
1204  {
1205  GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall repaired by " + GameServer.CharacterLogName(attacker), ServerLog.MessageType.ItemInteraction);
1206  }
1207 #endif
1208  DebugConsole.Log("Removing gap (ID " + Sections[sectionIndex].gap.ID + ", section: " + sectionIndex + ") from wall " + ID);
1209 
1210  //remove existing gap if damage is below leak threshold
1211  Sections[sectionIndex].gap.Open = 0.0f;
1212  Sections[sectionIndex].gap.Remove();
1213  Sections[sectionIndex].gap = null;
1214  }
1215  }
1216  else
1217  {
1218  float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f;
1219  if (Sections[sectionIndex].gap == null)
1220  {
1221  Rectangle gapRect = Sections[sectionIndex].rect;
1222  float diffFromCenter;
1223  if (IsHorizontal)
1224  {
1225  diffFromCenter = (gapRect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth;
1226  if (BodyWidth > 0.0f) { gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width)); }
1227  if (BodyHeight > 0.0f)
1228  {
1229  gapRect.Y = (gapRect.Y - gapRect.Height / 2) + (int)(BodyHeight / 2 + BodyOffset.Y * scale);
1230  gapRect.Height = (int)BodyHeight;
1231  }
1232  if (FlippedX) { diffFromCenter = -diffFromCenter; }
1233  }
1234  else
1235  {
1236  diffFromCenter = ((gapRect.Y - gapRect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight;
1237  if (BodyWidth > 0.0f)
1238  {
1239  gapRect.X = gapRect.Center.X + (int)(-BodyWidth / 2 + BodyOffset.X * scale);
1240  gapRect.Width = (int)BodyWidth;
1241  }
1242  if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); }
1243  if (FlippedY) { diffFromCenter = -diffFromCenter; }
1244  }
1245 
1246  if (Math.Abs(BodyRotation) > 0.01f)
1247  {
1248  Vector2 structureCenter = Position;
1249  Vector2 gapPos = structureCenter + new Vector2(
1250  (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
1251  (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter + BodyOffset * scale;
1252  gapRect = new Rectangle((int)(gapPos.X - gapRect.Width / 2), (int)(gapPos.Y + gapRect.Height / 2), gapRect.Width, gapRect.Height);
1253  }
1254 
1255  gapRect.X -= 10;
1256  gapRect.Y += 10;
1257  gapRect.Width += 20;
1258  gapRect.Height += 20;
1259 
1260  bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(rotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
1261  if (rotatedEnoughToChangeOrientation)
1262  {
1263  var center = gapRect.Location + gapRect.Size.FlipY() / new Point(2);
1264  var topLeft = gapRect.Location;
1265  var diff = topLeft - center;
1266  diff = diff.FlipY().YX().FlipY();
1267  var newTopLeft = diff + center;
1268  gapRect = new Rectangle(newTopLeft, gapRect.Size.YX());
1269  }
1270  bool horizontalGap = rotatedEnoughToChangeOrientation
1271  ? IsHorizontal
1272  : !IsHorizontal;
1273  bool diagonalGap = false;
1274  if (!MathUtils.NearlyEqual(BodyRotation, 0f))
1275  {
1276  //rotation within a 90 deg sector (e.g. 100 -> 10, 190 -> 10, -10 -> 80)
1277  float sectorizedRotation = MathUtils.WrapAngleTwoPi(BodyRotation) % MathHelper.PiOver2;
1278  //diagonal if 30 < angle < 60
1279  diagonalGap = sectorizedRotation is > MathHelper.Pi / 6 and < MathHelper.Pi / 3;
1280  //gaps on the lower half of a diagonal wall are horizontal, ones on the upper half are vertical
1281  if (diagonalGap)
1282  {
1283  horizontalGap = gapRect.Y - gapRect.Height / 2 < Position.Y;
1284  if (FlippedY) { horizontalGap = !horizontalGap; }
1285  }
1286  }
1287 
1288  Sections[sectionIndex].gap = new Gap(gapRect, horizontalGap, Submarine, isDiagonal: diagonalGap);
1289 
1290  //free the ID, because if we give gaps IDs we have to make sure they always match between the clients and the server and
1291  //that clients create them in the correct order along with every other entity created/removed during the round
1292  //which COULD be done via entityspawner, but it's unnecessary because we never access these gaps by ID
1293  Sections[sectionIndex].gap.FreeID();
1294  Sections[sectionIndex].gap.ShouldBeSaved = false;
1295  Sections[sectionIndex].gap.ConnectedWall = this;
1296  DebugConsole.Log("Created gap (ID " + Sections[sectionIndex].gap.ID + ", section: " + sectionIndex + ") on wall " + ID);
1297  //AdjustKarma(attacker, 300);
1298 
1299 #if SERVER
1300  //the structure didn't have any other gaps yet, log the breach
1301  if (noGaps && attacker != null)
1302  {
1303  GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall breached by " + GameServer.CharacterLogName(attacker), ServerLog.MessageType.ItemInteraction);
1304  }
1305 #endif
1306  }
1307 
1308  var gap = Sections[sectionIndex].gap;
1309  float damageRatio = MaxHealth <= 0.0f ? 0 : damage / MaxHealth;
1310  float gapOpen = 0;
1311  if (damageRatio > BigGapThreshold)
1312  {
1313  gapOpen = MathHelper.Lerp(0.35f, 0.75f, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio));
1314  }
1315  else if (damageRatio > LeakThreshold)
1316  {
1317  gapOpen = MathHelper.Lerp(0f, 0.35f, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio));
1318  }
1319  gap.Open = gapOpen;
1320 
1321  //gap appeared or became much larger -> explosion effect
1322  if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
1323  {
1324  CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles);
1325  }
1326  }
1327 
1328  float damageDiff = damage - Sections[sectionIndex].damage;
1329  bool hadHole = SectionBodyDisabled(sectionIndex);
1330  Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth);
1331  HasDamage = Sections.Any(s => s.damage > 0.0f);
1332 
1333  if (attacker != null && damageDiff != 0.0f)
1334  {
1335  HumanAIController.StructureDamaged(this, damageDiff, attacker);
1336  OnHealthChangedProjSpecific(attacker, damageDiff);
1337  if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
1338  {
1339  if (damageDiff < 0.0f)
1340  {
1341  attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill,
1343  }
1344  }
1345  }
1346 
1347  bool hasHole = SectionBodyDisabled(sectionIndex);
1348 
1349  if (hadHole == hasHole) { return; }
1350 
1351  UpdateSections();
1352  }
1353 
1354  private static void CreateWallDamageExplosion(Gap gap, Character attacker, bool createProjectiles)
1355  {
1356  const float explosionRange = 500.0f;
1357  float explosionStrength = gap.Open;
1358 
1359  var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
1360  if (linkedHull != null)
1361  {
1362  //existing, nearby gaps leading to the same hull reduce the strength of the explosion
1363  // -> the first breached section does most (or all) of the damage, making it more consistent
1364  // (otherwise the damage would depend on how many structures and sections happen to be breached)
1365  foreach (var otherGap in linkedHull.ConnectedGaps)
1366  {
1367  if (otherGap == gap || otherGap.IsRoomToRoom || otherGap.Open < 0.25f) { continue; }
1368  explosionStrength -= Math.Max(0, explosionRange - Vector2.Distance(otherGap.WorldPosition, gap.WorldPosition)) / explosionRange;
1369  if (explosionStrength <= 0.0f) { return; }
1370  }
1371  }
1372 
1373  if (explosionOnBroken == null)
1374  {
1375  explosionOnBroken = new Explosion(explosionRange, force: 5.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f);
1376  if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations))
1377  {
1378  explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(5.0f), null);
1379  }
1380  else
1381  {
1382  explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null);
1383  }
1384  explosionOnBroken.CameraShake = 5.0f;
1385  explosionOnBroken.IgnoreCover = false;
1386  explosionOnBroken.OnlyInside = true;
1387  explosionOnBroken.DistanceFalloff = false;
1388  explosionOnBroken.PlayDamageSounds = true;
1389  explosionOnBroken.DisableParticles();
1390  }
1391  explosionOnBroken.CameraShake = 25.0f;
1392  explosionOnBroken.IgnoredCover = gap.ConnectedWall?.ToEnumerable();
1393  explosionOnBroken.Attack.Range = explosionOnBroken.CameraShakeRange = explosionRange * gap.Open;
1394  explosionOnBroken.Attack.DamageMultiplier = explosionStrength;
1395  explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f);
1396  explosionOnBroken.IgnoredCharacters.Clear();
1397  if (attacker?.AIController is EnemyAIController) { explosionOnBroken.IgnoredCharacters.Add(attacker); }
1398  explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker);
1399 
1400  if (createProjectiles)
1401  {
1402  if (ItemPrefab.Prefabs.TryGet("walldamageprojectile", out var projectilePrefab) && linkedHull != null)
1403  {
1404  float angle = gap.IsHorizontal ?
1405  (linkedHull.WorldPosition.X < gap.WorldPosition.X ? MathHelper.Pi : 0) :
1406  (linkedHull.WorldPosition.Y < gap.WorldPosition.Y ? -MathHelper.PiOver2 : MathHelper.PiOver2);
1407  Spawner.AddItemToSpawnQueue(projectilePrefab, gap.WorldPosition, onSpawned: (item) =>
1408  {
1409  item.body.SetTransformIgnoreContacts(item.body.SimPosition, angle);
1410  var projectile = item.GetComponent<Items.Components.Projectile>();
1411  projectile?.Use();
1412  });
1413  }
1414  }
1415 
1416 #if CLIENT
1417  SoundPlayer.PlaySound("Ricochet", gap.WorldPosition);
1418  if (linkedHull != null)
1419  {
1420  for (int i = 0; i <= 50; i++)
1421  {
1422  var emitDirection = gap.IsHorizontal ?
1423  gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX :
1424  gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY;
1425  Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y));
1426  emitDirection = new Vector2(emitDirection.X + Rand.Range(-0.2f, 0.2f), emitDirection.Y + Rand.Range(-0.2f, 0.2f));
1427  var shrapnelParticle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, emitDirection * Rand.Range(100.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.1f);
1428  var sparkParticle = GameMain.ParticleManager.CreateParticle("whitespark", particlePos, emitDirection * Rand.Range(1000.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.05f);
1429  if (shrapnelParticle == null || sparkParticle == null) { break; }
1430  }
1431  }
1432 #endif
1433  }
1434 
1435  partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount);
1436 
1437  public void SetCollisionCategory(Category collisionCategory)
1438  {
1439  if (Bodies == null) return;
1440  foreach (Body body in Bodies)
1441  {
1442  body.CollisionCategories = collisionCategory;
1443  }
1444  }
1445 
1446  private void UpdateSections()
1447  {
1448  if (Bodies == null) { return; }
1449  foreach (Body b in Bodies)
1450  {
1451  GameMain.World.Remove(b);
1452  }
1453  Bodies.Clear();
1454  bodyDimensions.Clear();
1455 #if CLIENT
1456  convexHulls?.ForEach(ch => ch.Remove());
1457  convexHulls?.Clear();
1458 #endif
1459 
1460  bool hasHoles = false;
1461  var mergedSections = new List<WallSection>();
1462  for (int i = 0; i < Sections.Length; i++ )
1463  {
1464  // if there is a gap and we have sections to merge, do it.
1465  if (SectionBodyDisabled(i))
1466  {
1467  hasHoles = true;
1468 
1469  if (!mergedSections.Any()) { continue; }
1470  var mergedRect = GenerateMergedRect(mergedSections);
1471  mergedSections.Clear();
1472  CreateRectBody(mergedRect, createConvexHull: true);
1473  }
1474  else
1475  {
1476  mergedSections.Add(Sections[i]);
1477  }
1478  }
1479 
1480  // take care of any leftover pieces
1481  if (mergedSections.Count > 0)
1482  {
1483  var mergedRect = GenerateMergedRect(mergedSections);
1484  CreateRectBody(mergedRect, createConvexHull: true);
1485  }
1486 
1487  //if the section has holes (or is just one big hole with no bodies),
1488  //we need a sensor for repairtools to be able to target the structure
1489  if (hasHoles || !Bodies.Any())
1490  {
1491  Body sensorBody = CreateRectBody(rect, createConvexHull: false);
1492  sensorBody.CollisionCategories = Physics.CollisionRepairableWall;
1493  }
1494 
1495  foreach (var section in Sections)
1496  {
1497  bool intersectsWithBody = false;
1498  foreach (var body in Bodies)
1499  {
1500  var bodyRect = new Rectangle(
1501  ConvertUnits.ToDisplayUnits(body.Position - bodyDimensions[body] / 2).ToPoint(),
1502  ConvertUnits.ToDisplayUnits(bodyDimensions[body]).ToPoint());
1503 
1504  Rectangle sectionRect = section.rect;
1505  sectionRect.Y -= section.rect.Height;
1506  if (bodyRect.Intersects(sectionRect))
1507  {
1508  intersectsWithBody = true;
1509  break;
1510  }
1511  }
1512  section.NoPhysicsBody = !intersectsWithBody;
1513  }
1514  }
1515 
1516  private Body CreateRectBody(Rectangle rect, bool createConvexHull)
1517  {
1518  float diffFromCenter;
1519  if (IsHorizontal)
1520  {
1521  diffFromCenter = (rect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth;
1522  if (BodyWidth > 0.0f) rect.Width = Math.Max((int)Math.Round(BodyWidth * (rect.Width / (float)this.rect.Width)), 1);
1523  if (BodyHeight > 0.0f) rect.Height = (int)BodyHeight;
1524  if (FlippedX) { diffFromCenter = -diffFromCenter; }
1525  }
1526  else
1527  {
1528  diffFromCenter = ((rect.Y - rect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight;
1529  if (BodyWidth > 0.0f) rect.Width = (int)BodyWidth;
1530  if (BodyHeight > 0.0f) rect.Height = Math.Max((int)Math.Round(BodyHeight * (rect.Height / (float)this.rect.Height)), 1);
1531  if (FlippedY) { diffFromCenter = -diffFromCenter; }
1532  }
1533 
1534  Vector2 bodyOffset = ConvertUnits.ToSimUnits(BodyOffset) * scale;
1535 
1536  Body newBody = GameMain.World.CreateRectangle(
1537  ConvertUnits.ToSimUnits(rect.Width),
1538  ConvertUnits.ToSimUnits(rect.Height),
1539  1.5f,
1540  bodyType: BodyType.Static,
1541  findNewContacts: false);
1542  newBody.Friction = 0.5f;
1543  newBody.OnCollision += OnWallCollision;
1544  newBody.CollisionCategories = (Prefab.Platform) ? Physics.CollisionPlatform : Physics.CollisionWall;
1545  newBody.UserData = this;
1546 
1547  Vector2 structureCenter = ConvertUnits.ToSimUnits(Position);
1548  if (!MathUtils.NearlyEqual(BodyRotation, 0f))
1549  {
1550  Vector2 pos = structureCenter + bodyOffset + new Vector2(
1551  (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
1552  (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation))
1553  * ConvertUnits.ToSimUnits(diffFromCenter);
1554  newBody.SetTransformIgnoreContacts(ref pos, -BodyRotation);
1555  }
1556  else
1557  {
1558  Vector2 pos = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset;
1559  newBody.SetTransformIgnoreContacts(ref pos, newBody.Rotation);
1560  }
1561 
1562  if (createConvexHull)
1563  {
1564  CreateConvexHull(ConvertUnits.ToDisplayUnits(newBody.Position), rect.Size.ToVector2(), newBody.Rotation);
1565  }
1566 
1567  Bodies.Add(newBody);
1568  bodyDimensions.Add(newBody, new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height)));
1569 
1570  return newBody;
1571  }
1572 
1573  partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation);
1574 
1575  public override void FlipX(bool relativeToSub)
1576  {
1577  base.FlipX(relativeToSub);
1578 
1579 #if CLIENT
1580  if (Prefab.CanSpriteFlipX)
1581  {
1582  SpriteEffects ^= SpriteEffects.FlipHorizontally;
1583  }
1584 #endif
1585 
1586  if (StairDirection != Direction.None)
1587  {
1588  StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left;
1589  Bodies.ForEach(b => GameMain.World.Remove(b));
1590  Bodies.Clear();
1591  bodyDimensions.Clear();
1592 
1593  CreateStairBodies();
1594  }
1595 
1596  if (HasBody)
1597  {
1598  CreateSections();
1599  UpdateSections();
1600  }
1601  }
1602 
1603  public override void FlipY(bool relativeToSub)
1604  {
1605  base.FlipY(relativeToSub);
1606 
1607 #if CLIENT
1608  if (Prefab.CanSpriteFlipY)
1609  {
1610  SpriteEffects ^= SpriteEffects.FlipVertically;
1611  }
1612 #endif
1613 
1614  if (StairDirection != Direction.None)
1615  {
1616  StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left;
1617  Bodies.ForEach(b => GameMain.World.Remove(b));
1618  Bodies.Clear();
1619  bodyDimensions.Clear();
1620 
1621  CreateStairBodies();
1622  }
1623 
1624  if (HasBody)
1625  {
1626  CreateSections();
1627  UpdateSections();
1628  }
1629  }
1630 
1631  public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
1632  {
1633  string name = element.GetAttribute("name").Value;
1634  Identifier identifier = element.GetAttributeIdentifier("identifier", "");
1635 
1636  StructurePrefab prefab = FindPrefab(name, identifier);
1637  if (prefab == null)
1638  {
1639  DebugConsole.ThrowError("Error loading structure - structure prefab \"" + name + "\" (identifier \"" + identifier + "\") not found.");
1640  return null;
1641  }
1642 
1643  Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
1644  Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element), element)
1645  {
1646  Submarine = submarine,
1647  };
1648 
1649  bool flippedX = element.GetAttributeBool(nameof(FlippedX), false);
1650  bool flippedY = element.GetAttributeBool(nameof(FlippedY), false);
1651 
1652  if (submarine?.Info.GameVersion != null)
1653  {
1655  //tier-based upgrade restrictions were added in 0.19.10.0, potentially soft-locking campaigns if you're
1656  //far enough on the map on a submarine that can no longer have as many levels of hull upgrades.
1657  // -> let's ensure that won't happen
1658  if (submarine.Info.GameVersion < new Version(0, 19, 10) && GameMain.GameSession?.LevelData != null)
1659  {
1660  s.CrushDepth = Math.Max(s.CrushDepth, GameMain.GameSession.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio + 500);
1661  }
1662 
1663 #if CLIENT
1665  targetSize: rect.Size.ToVector2(),
1666  originalTextureOffset:
1667  // Note: cannot use s.TextureOffset because wrapping is very weird in the old logic
1668  element.GetAttributeVector2("TextureOffset", Vector2.Zero),
1669  submarineInfo: submarine.Info,
1670  sourceRect: s.Sprite.SourceRect,
1671  scale: s.Scale * s.TextureScale,
1672  flippedX: flippedX,
1673  flippedY: flippedY);
1674 #endif
1675  }
1676 
1677  bool hasDamage = false;
1678  foreach (var subElement in element.Elements())
1679  {
1680  switch (subElement.Name.ToString().ToLowerInvariant())
1681  {
1682  case "section":
1683  int index = subElement.GetAttributeInt("i", -1);
1684  if (index == -1) { continue; }
1685 
1686  if (index < 0 || index >= s.SectionCount)
1687  {
1688  string errorMsg = $"Error while loading structure \"{s.Name}\". Section damage index out of bounds. Index: {index}, section count: {s.SectionCount}.";
1689  DebugConsole.ThrowError(errorMsg);
1690  GameAnalyticsManager.AddErrorEventOnce("Structure.Load:SectionIndexOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1691  }
1692  else
1693  {
1694  float damage = subElement.GetAttributeFloat("damage", 0.0f);
1695  s.Sections[index].damage = damage;
1696  hasDamage |= damage > 0.0f;
1697  }
1698  break;
1699  case "upgrade":
1700  {
1701  var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
1702  UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
1703  int level = subElement.GetAttributeInt("level", 1);
1704  if (upgradePrefab != null)
1705  {
1706  s.AddUpgrade(new Upgrade(s, upgradePrefab, level, subElement));
1707  }
1708  else
1709  {
1710  DebugConsole.ThrowError($"An upgrade with identifier \"{upgradeIdentifier}\" on {s.Name} was not found. " +
1711  "It's effect will not be applied and won't be saved after the round ends.");
1712  }
1713  break;
1714  }
1715  }
1716  }
1717 
1718  if (flippedX) { s.FlipX(false); }
1719  if (flippedY) { s.FlipY(false); }
1720 
1721  //structures with a body drop a shadow by default
1722  if (element.GetAttribute(nameof(UseDropShadow)) == null)
1723  {
1724  s.UseDropShadow = prefab.Body;
1725  }
1726 
1727  if (element.GetAttribute(nameof(NoAITarget)) == null)
1728  {
1729  s.NoAITarget = prefab.NoAITarget;
1730  }
1731 
1732  if (hasDamage)
1733  {
1734  s.UpdateSections();
1735  }
1736 
1737  return s;
1738  }
1739 
1740  public static StructurePrefab FindPrefab(string name, Identifier identifier)
1741  {
1742  StructurePrefab prefab = null;
1743  if (identifier.IsEmpty)
1744  {
1745  //legacy support:
1746  //1. attempt to find a prefab with an empty identifier and a matching name
1747  prefab = MapEntityPrefab.Find(name, "") as StructurePrefab;
1748  //2. not found, attempt to find a prefab with a matching name
1749  if (prefab == null) { prefab = MapEntityPrefab.Find(name) as StructurePrefab; }
1750  //3. not found, attempt to find a prefab that uses the previous name as an identifier
1751  if (prefab == null) { prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; }
1752  }
1753  else
1754  {
1755  prefab = MapEntityPrefab.Find(null, identifier) as StructurePrefab;
1756  }
1757  return prefab;
1758  }
1759 
1760  public override XElement Save(XElement parentElement)
1761  {
1762  XElement element = new XElement("Structure");
1763 
1764  int width = ResizeHorizontal ? rect.Width : defaultRect.Width;
1765  int height = ResizeVertical ? rect.Height : defaultRect.Height;
1766 
1767  element.Add(
1768  new XAttribute("name", base.Prefab.Name),
1769  new XAttribute("identifier", base.Prefab.Identifier),
1770  new XAttribute("ID", ID),
1771  new XAttribute("rect",
1772  (int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
1773  (int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
1774  width + "," + height));
1775 
1776  if (FlippedX) { element.Add(new XAttribute("flippedx", true)); }
1777  if (FlippedY) { element.Add(new XAttribute("flippedy", true)); }
1778 
1779  for (int i = 0; i < Sections.Length; i++)
1780  {
1781  if (Sections[i].damage == 0.0f) { continue; }
1782  var sectionElement =
1783  new XElement("section",
1784  new XAttribute("i", i),
1785  new XAttribute("damage", Sections[i].damage));
1786  element.Add(sectionElement);
1787  }
1788 
1790 
1791  if (CastShadow == Prefab.CastShadow)
1792  {
1793  element.GetAttribute(nameof(CastShadow))?.Remove();
1794  }
1795 
1796  foreach (var upgrade in Upgrades)
1797  {
1798  upgrade.Save(element);
1799  }
1800 
1801  parentElement.Add(element);
1802 
1803  return element;
1804  }
1805 
1806  public override void OnMapLoaded()
1807  {
1808  for (int i = 0; i < Sections.Length; i++)
1809  {
1810  SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false);
1811  }
1812  }
1813 
1814  public virtual void Reset()
1815  {
1817  MaxHealth = Prefab.Health;
1818  Sprite.ReloadXML();
1820  NoAITarget = Prefab.NoAITarget;
1821  }
1822 
1823  public override void Update(float deltaTime, Camera cam)
1824  {
1825  if (aiTarget != null)
1826  {
1827  aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : MathHelper.Lerp(aiTarget.MinSightRange, aiTarget.MaxSightRange, Submarine.Velocity.Length() / 10);
1828  }
1829  }
1830  }
1831 
1833  {
1834  public AbilityAttackerSubmarine(Character character, Submarine submarine)
1835  {
1836  Character = character;
1837  Submarine = submarine;
1838  }
1839  public Character Character { get; set; }
1840  public Submarine Submarine { get; set; }
1841  }
1842 }
AbilityAttackerSubmarine(Character character, Submarine submarine)
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float DamageMultiplier
Used for multiplying all the damage.
readonly Dictionary< Affliction, XElement > Afflictions
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
Vector2 GetAttributeVector2(string key, in Vector2 def)
bool GetAttributeBool(string key, bool def)
Rectangle GetAttributeRect(string key, in Rectangle def)
XAttribute? GetAttribute(string name)
Identifier GetAttributeIdentifier(string key, string def)
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
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
void FreeID()
Removes the entity from the entity dictionary and frees up the ID it was using.
Definition: Entity.cs:302
AITarget aiTarget
Definition: Entity.cs:33
void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition=null, int? quality=null, Action< Item > onSpawned=null)
Explosions are area of effect attacks that can damage characters, items and structures.
bool DistanceFalloff
Does the damage from the explosion decrease with distance from the origin of the explosion?
float CameraShake
Intensity of the screen shake effect.
bool IgnoreCover
When set to true, the explosion don't deal less damage when the target is behind a solid object.
float CameraShakeRange
How far away does the camera shake effect reach.
readonly HashSet< Character > IgnoredCharacters
void Explode(Vector2 worldPosition, Entity damageSource, Character attacker=null)
bool PlayDamageSounds
Should the normal damage sounds be played when the explosion damages something. Usually disabled.
IEnumerable< Structure > IgnoredCover
Structures that don't count as "cover" that reduces damage from the explosion. Only relevant if Ignor...
bool OnlyInside
Whether the explosion only affects characters inside a submarine.
static GameSession?? GameSession
Definition: GameMain.cs:88
static World World
Definition: GameMain.cs:105
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static ParticleManager ParticleManager
Definition: GameMain.cs:101
static void StructureDamaged(Structure structure, float damageAmount, Character character)
ushort GetOffsetId(XElement element)
Definition: IdRemap.cs:86
readonly int InitialDepth
The depth at which the level starts at, in in-game coordinates. E.g. if this was set to 100 000 (= 10...
Definition: LevelData.cs:57
static readonly List< MapEntity > MapEntityList
readonly List< Upgrade > Upgrades
List of upgrades this item has
static MapEntityPrefab Find(string name, string identifier=null, bool showErrorMessages=true)
Find a matching map entity prefab
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
static void UpgradeGameVersion(ISerializableEntity entity, ContentXElement configElement, Version savedVersion)
Upgrade the properties of an entity saved with an older version of the game. Properties that should b...
static Dictionary< Identifier, SerializableProperty > GetProperties(object obj)
static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault=false, bool ignoreEditable=false)
float SkillIncreasePerRepairedStructureDamage
static SkillSettings Current
Sprite(ContentXElement element, string path="", string file="", bool lazyLoad=false, float sourceRectScale=1)
void ReloadXML()
Works only if there is a name attribute defined for the sprite. For items and structures,...
override void FlipX(bool relativeToSub)
Flip the entity horizontally
override void Move(Vector2 amount, bool ignoreContacts=true)
override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent=false)
Adds a new upgrade to the item
static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
float BodyRotation
In radians, takes flipping into account
override void Update(float deltaTime, Camera cam)
Vector2 BodyOffset
Offset of the physics body from the center of the structure. Takes flipping into account.
Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id=Entity.NullEntityID, XElement element=null)
Dictionary< Identifier, SerializableProperty > SerializableProperties
bool SectionIsLeaking(int sectionIndex)
Sections that are leaking have a gap placed on them
override XElement Save(XElement parentElement)
void SetDamage(int sectionIndex, float damage, Character attacker=null, bool createNetworkEvent=true, bool isNetworkEvent=true, bool createExplosionEffect=true, bool createWallDamageProjectiles=false)
void SetCollisionCategory(Category collisionCategory)
override Quad2D GetTransformedQuad()
Vector2 SectionPosition(int sectionIndex, bool world=false)
static Vector2 UpgradeTextureOffset(Vector2 targetSize, Vector2 originalTextureOffset, SubmarineInfo submarineInfo, Rectangle sourceRect, Vector2 scale, bool flippedX, bool flippedY)
override void FlipY(bool relativeToSub)
Flip the entity vertically
void AddDamage(int sectionIndex, float damage, Character attacker=null, bool emitParticles=true, bool createWallDamageProjectiles=false)
static Structure GetAttachTarget(Vector2 worldPosition)
Checks if there's a structure items can be attached to at the given position and returns it.
override void ShallowRemove()
Remove the entity from the entity list without removing links to other entities
int FindSectionIndex(Vector2 displayPos, bool world=false, bool clamp=false)
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound=false)
static StructurePrefab FindPrefab(string name, Identifier identifier)
readonly? bool IsHorizontal
If null, the orientation is determined automatically based on the dimensions of the structure instanc...
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
bool IgnoreByAI(Character character)
WallSection(Rectangle rect, Structure wall, float damage=0.0f)
Interface for entities that the server can send events to the clients
AbilityEffectType
Definition: Enums.cs:125