Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs
1 using FarseerPhysics;
2 using FarseerPhysics.Dynamics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
9 
11 {
12  partial class RepairTool : ItemComponent
13  {
14  public enum UseEnvironment
15  {
16  Air, Water, Both, None
17  };
18 
19  private readonly HashSet<Identifier> fixableEntities;
20  private readonly HashSet<Identifier> nonFixableEntities;
21  private Vector2 pickedPosition;
22  private float activeTimer;
23 
24  private Vector2 debugRayStartPos, debugRayEndPos;
25 
26  private readonly List<Body> ignoredBodies = new List<Body>();
27 
28  [Serialize("Both", IsPropertySaveable.No, description: "Can the item be used in air, water or both.")]
30  {
31  get; set;
32  }
33 
34  [Serialize(0.0f, IsPropertySaveable.No, description: "The distance at which the item can repair targets.")]
35  public float Range { get; set; }
36 
37  [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")]
38  public float Spread
39  {
40  get;
41  set;
42  }
43 
44  [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")]
45  public float UnskilledSpread
46  {
47  get;
48  set;
49  }
50 
51  [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from structures per second.")]
52  public float StructureFixAmount
53  {
54  get; set;
55  }
56 
57  [Serialize(0.0f, IsPropertySaveable.No, description: "How much damage is applied to ballast flora.")]
58  public float FireDamage
59  {
60  get; set;
61  }
62 
63  [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from destructible level walls per second.")]
64  public float LevelWallFixAmount
65  {
66  get; set;
67  }
68 
69  [Serialize(0.0f, IsPropertySaveable.No, description: "How much the item decreases the size of fires per second.")]
70  public float ExtinguishAmount
71  {
72  get; set;
73  }
74 
75  [Serialize(0.0f, IsPropertySaveable.No, description: "How much water the item provides to planters per second.")]
76  public float WaterAmount { get; set; }
77 
78  [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels).")]
79  public Vector2 BarrelPos { get; set; }
80 
81  [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through walls.")]
82  public bool RepairThroughWalls { get; set; }
83 
84  [Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
85  public bool RepairMultiple { get; set; }
86 
87  [Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")]
88  public bool RepairMultipleWalls { get; set; }
89 
90  [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
91  public bool RepairThroughHoles { get; set; }
92 
93  [Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")]
94  public float MaxOverlappingWallDist { get; set; }
95 
96  [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the tool detaches level resources (e.g. minerals). Acts as a multiplier on the speed: with a value of 2, detaching an item whose DeattachDuration is set to 30 seconds would take 15 seconds.")]
97  public float DeattachSpeed { get; set; }
98 
99  [Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")]
100  public bool HitItems { get; set; }
101 
102  [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")]
103  public bool HitBrokenDoors { get; set; }
104 
105  [Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")]
106  public bool IgnoreCharacters { get; set; }
107 
108  [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")]
109  public float FireProbability { get; set; }
110 
111  [Serialize(0.0f, IsPropertySaveable.No, description: "Force applied to the entity the ray hits.")]
112  public float TargetForce { get; set; }
113 
114  [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })]
115  public float BarrelRotation
116  {
117  get; set;
118  }
119 
120  public Vector2 TransformedBarrelPos
121  {
122  get
123  {
124  if (item.body == null) { return BarrelPos; }
125  Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation));
126  Vector2 flippedPos = BarrelPos;
127  if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
128  return Vector2.Transform(flippedPos, bodyTransform);
129  }
130  }
131 
133  : base(item, element)
134  {
135  this.item = item;
136 
137  if (element.GetAttribute("limbfixamount") != null)
138  {
139  DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.",
140  contentPackage: element.ContentPackage);
141  }
142 
143  fixableEntities = new HashSet<Identifier>();
144  nonFixableEntities = new HashSet<Identifier>();
145  foreach (var subElement in element.Elements())
146  {
147  switch (subElement.Name.ToString().ToLowerInvariant())
148  {
149  case "fixable":
150  if (subElement.GetAttribute("name") != null)
151  {
152  DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.",
153  contentPackage: element.ContentPackage);
154  fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier());
155  }
156  else
157  {
158  foreach (Identifier id in subElement.GetAttributeIdentifierArray("identifier", Array.Empty<Identifier>()))
159  {
160  fixableEntities.Add(id);
161  }
162  }
163  break;
164  case "nonfixable":
165  foreach (Identifier id in subElement.GetAttributeIdentifierArray("identifier", Array.Empty<Identifier>()))
166  {
167  nonFixableEntities.Add(id);
168  }
169  break;
170  }
171  }
172  item.IsShootable = true;
173  item.RequireAimToUse = element.Parent.GetAttributeBool(nameof(item.RequireAimToUse), true);
174  InitProjSpecific(element);
175  }
176 
177  partial void InitProjSpecific(ContentXElement element);
178 
179  public override void Update(float deltaTime, Camera cam)
180  {
181  activeTimer -= deltaTime;
182  if (activeTimer <= 0.0f) { IsActive = false; }
183  }
184 
185  public override bool Use(float deltaTime, Character character = null)
186  {
187  if (character != null)
188  {
189  if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) { return false; }
190  }
191 
192  float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character);
193 
194  bool failed = false;
195  if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
196  {
197  ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
198  failed = true;
199  }
200  if (UsableIn == UseEnvironment.None)
201  {
202  ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
203  failed = true;
204  }
205  if (item.InWater)
206  {
207  if (UsableIn == UseEnvironment.Air)
208  {
209  ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
210  failed = true;
211  }
212  }
213  else
214  {
215  if (UsableIn == UseEnvironment.Water)
216  {
217  ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
218  failed = true;
219  }
220  }
221  if (failed)
222  {
223  // Always apply ActionType.OnUse. If doesn't fail, the effect is called later.
224  ApplyStatusEffects(ActionType.OnUse, deltaTime, character);
225  return false;
226  }
227 
228  Vector2 rayStart;
229  Vector2 rayStartWorld;
230  Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
231  Vector2 barrelPos = item.SimPosition + ConvertUnits.ToSimUnits(TransformedBarrelPos);
232  //make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel
233  if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
234  {
235  //no obstacles -> we start the raycast at the end of the barrel
236  rayStart = ConvertUnits.ToSimUnits(item.Position + TransformedBarrelPos);
237  rayStartWorld = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos);
238  }
239  else
240  {
241  rayStart = rayStartWorld = Submarine.LastPickedPosition + Submarine.LastPickedNormal * 0.1f;
242  if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; }
243  }
244 
245  //if the calculated barrel pos is in another hull, use the origin of the item to make sure the particles don't end up in an incorrect hull
246  if (item.CurrentHull != null)
247  {
248  var barrelHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(rayStartWorld), item.CurrentHull, useWorldCoordinates: true);
249  if (barrelHull != null && barrelHull != item.CurrentHull)
250  {
251  if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
252  {
253  if (!item.CurrentHull.ConnectedGaps.Any(g => g.Open > 0.0f && Submarine.RectContains(g.Rect, hullIntersection)))
254  {
255  Vector2 rayDir = rayStart.NearlyEquals(sourcePos) ? Vector2.Zero : Vector2.Normalize(rayStart - sourcePos);
256  rayStartWorld = ConvertUnits.ToSimUnits(hullIntersection - rayDir * 5.0f);
257  if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; }
258  }
259  }
260  }
261  }
262 
263  float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
264 
265  float angle = MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f);
266  float dir = 1;
267  if (item.body != null)
268  {
269  angle += item.body.Rotation;
270  dir = item.body.Dir;
271  }
272  Vector2 rayEnd = rayStartWorld + ConvertUnits.ToSimUnits(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Range * dir);
273 
274  ignoredBodies.Clear();
275  if (character != null)
276  {
277  foreach (Limb limb in character.AnimController.Limbs)
278  {
279  if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
280  ignoredBodies.Add(limb.body.FarseerBody);
281  }
282  ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
283  }
284 
285  IsActive = true;
286  activeTimer = 0.1f;
287 
288  debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStartWorld);
289  debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd);
290 
291  Submarine parentSub = character?.Submarine ?? item.Submarine;
292  if (parentSub == null)
293  {
294  foreach (Submarine sub in Submarine.Loaded)
295  {
296  Rectangle subBorders = sub.Borders;
297  subBorders.Location += new Point((int)sub.WorldPosition.X, (int)sub.WorldPosition.Y - sub.Borders.Height);
298  if (!MathUtils.CircleIntersectsRectangle(item.WorldPosition, Range * 5.0f, subBorders))
299  {
300  continue;
301  }
302  Repair(rayStartWorld - sub.SimPosition, rayEnd - sub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
303  }
304  Repair(rayStartWorld, rayEnd, deltaTime, character, degreeOfSuccess, ignoredBodies);
305  }
306  else
307  {
308  Repair(rayStartWorld - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
309  }
310 
311  UseProjSpecific(deltaTime, rayStartWorld);
312 
313  return true;
314  }
315 
316  partial void UseProjSpecific(float deltaTime, Vector2 raystart);
317 
318  private static readonly List<Body> hitBodies = new List<Body>();
319  private readonly HashSet<Character> hitCharacters = new HashSet<Character>();
320  private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
321  private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
322  {
323  var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall;
324  if (!IgnoreCharacters)
325  {
326  collisionCategories |= Physics.CollisionCharacter;
327  }
328 
329  //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
330  if (statusEffectLists != null)
331  {
332  static bool CanSeverJoints(ActionType type, Dictionary<ActionType, List<StatusEffect>> effectList) =>
333  effectList.TryGetValue(type, out List<StatusEffect> effects) && effects.Any(e => e.SeverLimbsProbability > 0);
334 
335  if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists))
336  {
337  float rangeSqr = ConvertUnits.ToSimUnits(Range);
338  rangeSqr *= rangeSqr;
339  foreach (Character c in Character.CharacterList)
340  {
341  if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
342  //do a broad check first
343  if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
344  if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
345  foreach (Limb limb in c.AnimController.Limbs)
346  {
347  if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < rangeSqr && Vector2.Dot(rayEnd - rayStart, limb.SimPosition - rayStart) > 0)
348  {
349  c.AnimController.BodyInRest = false;
350  break;
351  }
352  }
353  }
354  }
355  }
356 
357  float lastPickedFraction = 0.0f;
358  if (RepairMultiple)
359  {
360  var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories,
361  ignoreSensors: false,
362  customPredicate: (Fixture f) =>
363  {
364  if (f.IsSensor)
365  {
366  if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; }
367  if (f.Body?.UserData is PhysicsBody) { return false; }
368  }
369  if (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null) { return false; }
370  if (f.Body?.UserData as string == "ruinroom") { return false; }
371  if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; }
372  return true;
373  },
374  allowInsideFixture: true);
375 
376  hitBodies.Clear();
377  hitBodies.AddRange(bodies.Distinct());
378 
379  lastPickedFraction = Submarine.LastPickedFraction;
380  Type lastHitType = null;
381  hitCharacters.Clear();
382  foreach (Body body in hitBodies)
383  {
384  Type bodyType = body.UserData?.GetType();
385  if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
386  {
387  //stop the ray if it already hit a door/wall and is now about to hit some other type of entity
388  if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
389  }
390  if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent<Door>() != null)) { break; }
391 
392  Character hitCharacter = null;
393  if (body.UserData is Limb limb)
394  {
395  hitCharacter = limb.character;
396  }
397  else if (body.UserData is Character character)
398  {
399  hitCharacter = character;
400  }
401  //only do damage once to each character even if they ray hit multiple limbs
402  if (hitCharacter != null)
403  {
404  if (hitCharacters.Contains(hitCharacter)) { continue; }
405  hitCharacters.Add(hitCharacter);
406  }
407 
408  //if repairing through walls is not allowed and the next wall is more than 100 pixels away from the previous one, stop here
409  //(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than MaxOverlappingWallDist pixels apart)
410  float thisBodyFraction = Submarine.LastPickedBodyDist(body);
411  if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > MaxOverlappingWallDist)
412  {
413  break;
414  }
415  pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction;
416  if (FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, body))
417  {
418  lastPickedFraction = thisBodyFraction;
419  if (bodyType != null) { lastHitType = bodyType; }
420  }
421  }
422  }
423  else
424  {
425  var pickedBody = Submarine.PickBody(rayStart, rayEnd,
426  ignoredBodies, collisionCategories,
427  ignoreSensors: false,
428  customPredicate: (Fixture f) =>
429  {
430  if (f.IsSensor)
431  {
432  if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; }
433  if (f.Body?.UserData is PhysicsBody) { return false; }
434  }
435  if (f.Body?.UserData as string == "ruinroom") { return false; }
436  if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; }
437 
438  if (f.Body?.UserData is Item targetItem)
439  {
440  if (!HitItems) { return false; }
441  if (HitBrokenDoors)
442  {
443  if (targetItem.GetComponent<Door>() == null && targetItem.Condition <= 0) { return false; }
444  }
445  else
446  {
447  if (targetItem.Condition <= 0) { return false; }
448  }
449  }
450  return f.Body?.UserData != null;
451  },
452  allowInsideFixture: true);
453  pickedPosition = Submarine.LastPickedPosition;
454  FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, pickedBody);
455  lastPickedFraction = Submarine.LastPickedFraction;
456  }
457 
458  if (ExtinguishAmount > 0.0f && item.CurrentHull != null)
459  {
460  fireSourcesInRange.Clear();
461  //step along the ray in 10% intervals, collecting all fire sources in the range
462  for (float x = 0.0f; x <= lastPickedFraction; x += 0.1f)
463  {
464  Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * x);
465  if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
466 
467  Hull hull = Hull.FindHull(displayPos, item.CurrentHull);
468  if (hull == null) continue;
469  foreach (FireSource fs in hull.FireSources)
470  {
471  if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs))
472  {
473  fireSourcesInRange.Add(fs);
474  }
475  }
476  foreach (FireSource fs in hull.FakeFireSources)
477  {
478  if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs))
479  {
480  fireSourcesInRange.Add(fs);
481  }
482  }
483  }
484 
485  foreach (FireSource fs in fireSourcesInRange)
486  {
487  fs.Extinguish(deltaTime, ExtinguishAmount);
488 #if SERVER
489  if (!(fs is DummyFireSource))
490  {
491  GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime);
492  }
493 #endif
494  }
495  }
496 
497  if (WaterAmount > 0.0f && item.Submarine != null)
498  {
499  Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition);
500 
501  // Could probably be done much efficiently here
502  foreach (Item it in Item.ItemList)
503  {
504  if (it.Submarine == item.Submarine && it.GetComponent<Planter>() is { } planter)
505  {
506  if (it.GetComponent<Holdable>() is { } holdable && holdable.Attachable && !holdable.Attached) { continue; }
507 
508  Rectangle collisionRect = it.WorldRect;
509  collisionRect.Y -= collisionRect.Height;
510  if (collisionRect.Left < pos.X && collisionRect.Right > pos.X && collisionRect.Bottom < pos.Y)
511  {
512  Body collision = Submarine.PickBody(rayStart, it.SimPosition, ignoredBodies, collisionCategories);
513  if (collision == null)
514  {
515  for (var i = 0; i < planter.GrowableSeeds.Length; i++)
516  {
517  Growable seed = planter.GrowableSeeds[i];
518  if (seed == null || seed.Decayed) { continue; }
519 
520  seed.Health += WaterAmount * deltaTime;
521 
522 #if CLIENT
523  float barOffset = 10f * GUI.Scale;
524  Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
525  user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
526 #endif
527  }
528  }
529  }
530  }
531  }
532  }
533 
534  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
535  {
536  if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime && item.CurrentHull != null)
537  {
538  Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f);
539  if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
540  new FireSource(displayPos, sourceCharacter: user);
541  }
542  }
543  }
544 
545  private bool FixBody(Character user, Vector2 hitPosition, float deltaTime, float degreeOfSuccess, Body targetBody)
546  {
547  if (targetBody?.UserData == null) { return false; }
548 
549  if (targetBody.UserData is Structure targetStructure)
550  {
551  if (targetStructure.IsPlatform) { return false; }
552  int sectionIndex = targetStructure.FindSectionIndex(ConvertUnits.ToDisplayUnits(pickedPosition));
553  if (sectionIndex < 0) { return false; }
554 
555  if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; }
556  if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; }
557 
558  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure);
559  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure);
560  FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
561 
562  float structureFixAmount = StructureFixAmount;
563  if (structureFixAmount >= 0f)
564  {
565  structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureRepairMultiplier);
566  structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureRepairMultiplier);
567  }
568  else
569  {
570  structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureDamageMultiplier);
571  structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier);
572  }
573 
574  var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex);
575 
576  targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user);
577 
578  if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex))
579  {
580  user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak);
581  }
582 
583  //if the next section is small enough, apply the effect to it as well
584  //(to make it easier to fix a small "left-over" section)
585  for (int i = -1; i < 2; i += 2)
586  {
587  int nextSectionLength = targetStructure.SectionLength(sectionIndex + i);
588  if ((sectionIndex == 1 && i == -1) ||
589  (sectionIndex == targetStructure.SectionCount - 2 && i == 1) ||
590  (nextSectionLength > 0 && nextSectionLength < Structure.WallSectionSize * 0.3f))
591  {
592  //targetStructure.HighLightSection(sectionIndex + i);
593  targetStructure.AddDamage(sectionIndex + i, -structureFixAmount * degreeOfSuccess);
594  }
595  }
596  return true;
597  }
598  else if (targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)
599  {
600  if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall)
601  {
602  levelWall.AddDamage(-LevelWallFixAmount * deltaTime, ConvertUnits.ToDisplayUnits(hitPosition));
603  }
604  return true;
605  }
606  else if (targetBody.UserData is LevelObject levelObject && levelObject.Prefab.TakeLevelWallDamage)
607  {
608  levelObject.AddDamage(-LevelWallFixAmount, deltaTime, item);
609  return true;
610  }
611  else if (targetBody.UserData is Character targetCharacter)
612  {
613  if (targetCharacter.Removed) { return false; }
614  targetCharacter.LastDamageSource = item;
615  Limb closestLimb = null;
616  float closestDist = float.MaxValue;
617  foreach (Limb limb in targetCharacter.AnimController.Limbs)
618  {
619  if (limb.Removed || limb.IgnoreCollisions || limb.Hidden || limb.IsSevered) { continue; }
620  float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition);
621  if (dist < closestDist)
622  {
623  closestLimb = limb;
624  closestDist = dist;
625  }
626  }
627 
628  if (closestLimb != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
629  {
630  Vector2 dir = closestLimb.WorldPosition - item.WorldPosition;
631  dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
632  closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
633  }
634 
635  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb);
636  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb);
637  FixCharacterProjSpecific(user, deltaTime, targetCharacter);
638  return true;
639  }
640  else if (targetBody.UserData is Limb targetLimb)
641  {
642  if (targetLimb.character == null || targetLimb.character.Removed) { return false; }
643 
644  if (!MathUtils.NearlyEqual(TargetForce, 0.0f))
645  {
646  Vector2 dir = targetLimb.WorldPosition - item.WorldPosition;
647  dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
648  targetLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
649  }
650 
651  targetLimb.character.LastDamageSource = item;
652  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb);
653  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb);
654  FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
655  return true;
656  }
657  else if (targetBody.UserData is Item targetItem)
658  {
659  if (!HitItems || !targetItem.IsInteractable(user)) { return false; }
660 
661  var levelResource = targetItem.GetComponent<LevelResource>();
662  if (levelResource != null && levelResource.Attached &&
663  levelResource.RequiredItems.Any() &&
664  levelResource.HasRequiredItems(user, addMessage: false))
665  {
666  float addedDetachTime = deltaTime *
667  DeattachSpeed *
668  (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) *
669  (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
670  levelResource.DeattachTimer += addedDetachTime;
671 #if CLIENT
672  if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
673  (user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
674  {
675  Character.Controlled.UpdateHUDProgressBar(
676  this,
677  targetItem.WorldPosition,
678  levelResource.DeattachTimer / levelResource.DeattachDuration,
679  GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching");
680  }
681 #endif
682  FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false);
683  return true;
684  }
685 
686  if (!targetItem.Prefab.DamagedByRepairTools) { return false; }
687 
688  if (HitBrokenDoors)
689  {
690  if (targetItem.GetComponent<Door>() == null && targetItem.Condition <= 0) { return false; }
691  }
692  else
693  {
694  if (targetItem.Condition <= 0) { return false; }
695  }
696 
697  targetItem.IsHighlighted = true;
698 
699  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem);
700  ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem);
701 
702  if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
703  {
704  Vector2 dir = targetItem.WorldPosition - item.WorldPosition;
705  dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
706  targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
707  }
708 
709  FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: true);
710  return true;
711  }
712  else if (targetBody.UserData is BallastFloraBranch branch)
713  {
714  if (branch.ParentBallastFlora is { } ballastFlora)
715  {
716  ballastFlora.DamageBranch(branch, FireDamage * deltaTime, BallastFloraBehavior.AttackType.Fire, user);
717  }
718  }
719  return false;
720  }
721 
722  partial void FixStructureProjSpecific(Character user, float deltaTime, Structure targetStructure, int sectionIndex);
723  partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter);
724  partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem, bool showProgressBar);
725 
726  private float sinTime;
727  private float repairTimer;
728  private Gap previousGap;
729  private readonly float repairTimeOut = 5;
730  public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
731  {
732  if (objective.OperateTarget is not Gap leak)
733  {
734  Reset();
735  return true;
736  }
737  if (leak.Submarine == null || leak.Submarine != character.Submarine)
738  {
739  Reset();
740  return true;
741  }
742  if (leak != previousGap)
743  {
744  Reset();
745  previousGap = leak;
746  }
747  Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos;
748  float dist = fromCharacterToLeak.Length();
749  float reach = AIObjectiveFixLeak.CalculateReach(this, character);
750  if (dist > reach * 2)
751  {
752  // Too far away -> consider this done and hope the AI is smart enough to move closer
753  Reset();
754  return true;
755  }
756  character.AIController.SteeringManager.Reset();
757  if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering)
758  {
759  pathSteering.ResetPath();
760  }
761  if (!character.AnimController.InWater)
762  {
763  // TODO: use the collider size?
764  if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController humanAnim &&
765  Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
766  {
767  humanAnim.Crouching = true;
768  }
769  }
770  if (!character.IsClimbing)
771  {
772  if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater))
773  {
774  // Steer closer
775  Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
776  if (!character.InWater)
777  {
778  dir.Y = 0;
779  }
780  character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
781  }
782  else if (dist < reach * 0.25f && !character.IsClimbing)
783  {
784  // Too close -> steer away
785  character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
786  }
787  }
788  if (dist <= reach || character.IsClimbing)
789  {
790  // In range
791  character.CursorPosition = leak.WorldPosition;
792  if (character.Submarine != null)
793  {
794  character.CursorPosition -= character.Submarine.Position;
795  }
796  character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
797  if (character.AnimController.InWater)
798  {
799  var torso = character.AnimController.GetLimb(LimbType.Torso);
800  // Turn facing the target when not moving (handled in the animcontroller if not moving)
801  Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
802  Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
803  float newRotation = MathUtils.VectorToAngle(diff);
804  character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
805 
806  if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
807  {
808  // Swim past
809  Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
810  moveDir *= character.AnimController.Dir;
811  character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
812  }
813  }
814  if (item.RequireAimToUse)
815  {
816  character.SetInput(InputType.Aim, false, true);
817  sinTime += deltaTime * 5;
818  }
819  // Press the trigger only when the tool is approximately facing the target.
820  Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
821  var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
822  bool repair = true;
823  if (angle < MathHelper.PiOver4)
824  {
825  if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
826  {
827  if (i.GetComponent<Door>() is Door door && !door.CanBeTraversed )
828  {
829  // Hit a door, don't repair so that we don't weld it shut.
830  if (door.Stuck > 90)
831  {
832  // Almost stuck -> just abandon.
833  return false;
834  }
835  if (door.Stuck > 50)
836  {
837  repair = false;
838  }
839  }
840  }
841  if (repair)
842  {
843  // Check that we don't hit any friendlies
844  if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
845  {
846  if (hit.UserData is Character c)
847  {
848  if (c == character) { return false; }
849  return HumanAIController.IsFriendly(character, c);
850  }
851  return false;
852  }))
853  {
854  character.SetInput(InputType.Shoot, false, true);
855  Use(deltaTime, character);
856  }
857  }
858  repairTimer += deltaTime;
859  if (repairTimer > repairTimeOut)
860  {
861 #if DEBUG
862  DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
863 #endif
864  Reset();
865  return true;
866  }
867  }
868  }
869  else
870  {
871  // Reset the timer so that we don't time out if the water forces push us away
872  repairTimer = 0;
873  }
874 
875  bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
876  (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Max(s => s.damage) < 0.1f);
877 
878  if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam)
879  {
880  if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
881  {
882  character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leaksfixed".ToIdentifier(), 10.0f);
883  }
884  else
885  {
886  character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leakfixed".ToIdentifier(), 10.0f);
887  }
888  }
889 
890  return leakFixed;
891 
892  void Reset()
893  {
894  sinTime = 0;
895  repairTimer = 0;
896  }
897  }
898 
899  private static List<ISerializableEntity> currentTargets = new List<ISerializableEntity>();
900  private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null)
901  {
902  if (statusEffectLists == null) { return; }
903  if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; }
904 
905  foreach (StatusEffect effect in statusEffects)
906  {
907  currentTargets.Clear();
908  effect.SetUser(user);
909  if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
910  {
911  if (targetItem != null)
912  {
913  currentTargets.AddRange(targetItem.AllPropertyObjects);
914  }
915  if (structure != null)
916  {
917  currentTargets.Add(structure);
918  }
919  if (character != null)
920  {
921  currentTargets.Add(character);
922  }
923  effect.Apply(actionType, deltaTime, item, currentTargets);
924  }
925  else if (effect.HasTargetType(StatusEffect.TargetType.Character))
926  {
927  currentTargets.Add(user);
928  effect.Apply(actionType, deltaTime, item, currentTargets);
929  }
930  else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
931  {
932  currentTargets.Add(limb);
933  effect.Apply(actionType, deltaTime, item, currentTargets);
934  }
935 
936 #if CLIENT
937  if (user == null) { return; }
938  // Hard-coded progress bars for welding doors stuck.
939  // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
940  foreach (ISerializableEntity target in currentTargets)
941  {
942  if (target is not Door door) { continue; }
943  if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
944  foreach (var propertyEffect in effect.PropertyEffects)
945  {
946  if (propertyEffect.propertyName != "stuck") { continue; }
947  if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyEffect.propertyName, out SerializableProperty property)) { continue; }
948  object value = property.GetValue(target);
949  if (door.Stuck > 0)
950  {
951  bool isCutting = propertyEffect.value is float and < 0;
952  var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White,
953  textTag: isCutting ? "progressbar.cutting" : "progressbar.welding");
954  if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
955  if (!isCutting) { HintManager.OnWeldingDoor(user, door); }
956  }
957  }
958  }
959 #endif
960  }
961  }
962  }
963 }
static float CalculateReach(RepairTool repairTool, Character character)
void SetInput(InputType inputType, bool hit, bool held)
HUDProgressBar UpdateHUDProgressBar(object linkedObject, Vector2 worldPosition, float progress, Color emptyColor, Color fullColor, string textTag="")
Creates a progress bar that's "linked" to the specified object (or updates an existing one if there's...
ContentPackage? ContentPackage
XAttribute? GetAttribute(string name)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
bool IsShootable
Should the item's Use method be called with the "Use" or with the "Shoot" key?
bool RequireAimToUse
If true, the user has to hold the "aim" key before use is registered. False by default.
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
static readonly List< Item > ItemList
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)
readonly Dictionary< ActionType, List< StatusEffect > > statusEffectLists
float DegreeOfSuccess(Character character)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
float TransformedRotation
Takes flipping (Dir) into account.
void SmoothRotate(float targetRotation, float force=10.0f, bool wrapAngle=true)
Rotate the body towards the target rotation in the "shortest direction", taking into account the curr...
Limb GetLimb(LimbType limbType, bool excludeSevered=true)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
void SteeringManual(float deltaTime, Vector2 velocity)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
static IEnumerable< Body > PickBodies(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
Returns a list of physics bodies the ray intersects with, sorted according to distance (the closest b...
Rectangle? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180