Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs
1 using FarseerPhysics;
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Xml.Linq;
5 
7 {
8  partial class MotionSensor : ItemComponent
9  {
10  private float rangeX, rangeY;
11 
12  private Vector2 detectOffset;
13 
14  private float updateTimer;
15 
16  [Flags]
17  public enum TargetType
18  {
19  Human = 1,
20  Monster = 2,
21  Wall = 4,
22  Pet = 8,
23  Any = Human | Monster | Wall | Pet,
24  }
25 
26  [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
27  public bool MotionDetected { get; set; }
28 
29  [InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)]
31  {
32  get;
33  set;
34  }
35 
36  [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)]
37  public bool IgnoreDead
38  {
39  get;
40  set;
41  }
42 
43 
44  [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)]
45  public float RangeX
46  {
47  get { return rangeX; }
48  set
49  {
50  rangeX = MathHelper.Clamp(value, 0.0f, 1000.0f);
51 #if CLIENT
53 #endif
54  }
55  }
56  [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)]
57  public float RangeY
58  {
59  get { return rangeY; }
60  set
61  {
62  rangeY = MathHelper.Clamp(value, 0.0f, 1000.0f);
63  }
64  }
65 
66  [InGameEditable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")]
67  public Vector2 DetectOffset
68  {
69  get { return detectOffset; }
70  set
71  {
72  detectOffset = value;
73  detectOffset.X = MathHelper.Clamp(value.X, -rangeX, rangeX);
74  detectOffset.Y = MathHelper.Clamp(value.Y, -rangeY, rangeY);
75  }
76  }
77 
78  public Vector2 TransformedDetectOffset
79  {
80  get
81  {
82  Vector2 transformedDetectOffset = detectOffset;
83  if (item.FlippedX) { transformedDetectOffset.X = -transformedDetectOffset.X; }
84  if (item.FlippedY) { transformedDetectOffset.Y = -transformedDetectOffset.Y; }
85  return transformedDetectOffset;
86  }
87  }
88 
89  [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)]
90  public float UpdateInterval
91  {
92  get;
93  set;
94  }
95 
96  private int maxOutputLength;
97  [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
98  public int MaxOutputLength
99  {
100  get { return maxOutputLength; }
101  set
102  {
103  maxOutputLength = Math.Max(value, 0);
104  }
105  }
106 
107  private string output;
108  [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)]
109  public string Output
110  {
111  get { return output; }
112  set
113  {
114  if (value == null) { return; }
115  output = value;
116  if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading))
117  {
118  output = output.Substring(0, MaxOutputLength);
119  }
120  }
121  }
122 
123  private string falseOutput;
124  [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
125  public string FalseOutput
126  {
127  get { return falseOutput; }
128  set
129  {
130  if (value == null) { return; }
131  falseOutput = value;
132  if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading))
133  {
134  falseOutput = falseOutput.Substring(0, MaxOutputLength);
135  }
136  }
137  }
138 
139  [InGameEditable(DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
140  public float MinimumVelocity
141  {
142  get;
143  set;
144  }
145 
146  [Serialize(true, IsPropertySaveable.Yes, description: "Should the sensor trigger when the item itself moves.")]
147  public bool DetectOwnMotion
148  {
149  get;
150  set;
151  }
152 
154  : base(item, element)
155  {
156  IsActive = true;
157 
158  //backwards compatibility
159  if (element.GetAttribute("range") != null)
160  {
161  rangeX = rangeY = element.GetAttributeFloat("range", 0.0f);
162  }
163 
164  //randomize update timer so all sensors aren't updated during the same frame
165  updateTimer = Rand.Range(0.0f, UpdateInterval);
166  }
167 
168  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
169  {
170  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
171  //backwards compatibility
172  if (componentElement.GetAttributeBool("onlyhumans", false))
173  {
174  Target = TargetType.Human;
175  }
176  }
177 
178  public override void Update(float deltaTime, Camera cam)
179  {
180  string signalOut = MotionDetected ? Output : FalseOutput;
181 
182  if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); }
183 
184  if (MotionDetected)
185  {
186  ApplyStatusEffects(ActionType.OnUse, deltaTime);
187  }
188 
189  updateTimer -= deltaTime;
190  if (updateTimer > 0.0f) { return; }
191 
192  MotionDetected = false;
193  updateTimer = UpdateInterval;
194 
195  if (item.body != null && item.body.Enabled && DetectOwnMotion)
196  {
197  if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity)
198  {
199  MotionDetected = true;
200  return;
201  }
202  }
203 
204  Vector2 detectPos = item.WorldPosition + TransformedDetectOffset;
205  Rectangle detectRect = new Rectangle((int)(detectPos.X - rangeX), (int)(detectPos.Y - rangeY), (int)(rangeX * 2), (int)(rangeY * 2));
206  float broadRangeX = Math.Max(rangeX * 2, 500);
207  float broadRangeY = Math.Max(rangeY * 2, 500);
208 
209  if (item.CurrentHull == null && item.Submarine != null && Target.HasFlag(TargetType.Wall))
210  {
211  if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity))
212  {
213  var cells = Level.Loaded.GetCells(item.WorldPosition, 1);
214  foreach (var cell in cells)
215  {
216  if (cell.IsPointInside(item.WorldPosition))
217  {
218  MotionDetected = true;
219  return;
220  }
221  foreach (var edge in cell.Edges)
222  {
223  Vector2 e1 = edge.Point1 + cell.Translation;
224  Vector2 e2 = edge.Point2 + cell.Translation;
225  if (MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) ||
226  MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) ||
227  MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) ||
228  MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom)))
229  {
230  MotionDetected = true;
231  return;
232  }
233  }
234  }
235  }
236  foreach (Submarine sub in Submarine.Loaded)
237  {
238  if (sub == item.Submarine) { continue; }
239 
240  Vector2 relativeVelocity = item.Submarine.Velocity - sub.Velocity;
241  if (Math.Abs(relativeVelocity.X) < MinimumVelocity && Math.Abs(relativeVelocity.Y) < MinimumVelocity) { continue; }
242 
243  Rectangle worldBorders = new Rectangle(
244  sub.Borders.X + (int)sub.WorldPosition.X,
245  sub.Borders.Y + (int)sub.WorldPosition.Y - sub.Borders.Height,
246  sub.Borders.Width,
247  sub.Borders.Height);
248 
249  if (worldBorders.Intersects(detectRect))
250  {
251  MotionDetected = true;
252  return;
253  }
254  }
255  }
256 
257  bool triggerFromHumans = Target.HasFlag(TargetType.Human);
258  bool triggerFromPets = Target.HasFlag(TargetType.Pet);
259  bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
260  bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
261  if (!hasTriggers) { return; }
262  foreach (Character c in Character.CharacterList)
263  {
264  if (IgnoreDead && c.IsDead) { continue; }
265 
266  //ignore characters that have spawned a second or less ago
267  //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
268  if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
269  if (c.IsHuman)
270  {
271  if (!triggerFromHumans) { continue; }
272  }
273  else if (c.IsPet)
274  {
275  if (!triggerFromPets) { continue; }
276  }
277  else
278  {
279  // Not a human or a pet -> monster?
280  if (!triggerFromMonsters) { continue; }
282  {
283  //characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
284  continue;
285  }
286  }
287 
288  //do a rough check based on the position of the character's collider first
289  //before the more accurate limb-based check
290  if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY)
291  {
292  continue;
293  }
294 
295  foreach (Limb limb in c.AnimController.Limbs)
296  {
297  if (limb.IsSevered) { continue; }
298  if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
299  if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
300  {
301  MotionDetected = true;
302  return;
303  }
304  }
305  }
306  }
307 
308  public override XElement Save(XElement parentElement)
309  {
310  Vector2 prevDetectOffset = detectOffset;
311  XElement element = base.Save(parentElement);
312  detectOffset = prevDetectOffset;
313  return element;
314  }
315  }
316 }
Contains character data that should be editable in the character editor.
static bool CompareGroup(Identifier group1, Identifier group2)
static readonly Identifier HumanGroup
float GetAttributeFloat(string key, float def)
bool GetAttributeBool(string key, bool def)
XAttribute? GetAttribute(string name)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
double SpawnTime
Definition: Entity.cs:79
void SendSignal(string signal, string connectionName)
The base class for components holding the different functionalities of the item
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
List< VoronoiCell > GetCells(Vector2 worldPos, int searchDepth=2)
Rectangle? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19