Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.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.Collections.Immutable;
10 using System.Linq;
11 using System.Xml.Linq;
12 
14 {
16  {
17  private readonly struct EventData : IEventData
18  {
19  public readonly Vector2 AttachPos;
20 
21  public EventData(Vector2 attachPos)
22  {
23  AttachPos = attachPos;
24  }
25  }
26 
27  private const float MaxAttachDistance = ItemPrefab.DefaultInteractDistance * 0.95f;
28 
29  //the position(s) in the item that the Character grabs
30  protected Vector2[] handlePos;
31  private readonly Vector2[] scaledHandlePos;
32 
33  private readonly InputType prevPickKey;
34  private LocalizedString prevMsg;
35  private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
36 
37  private float swingState;
38 
39  private Character prevEquipper;
40 
41  public override bool IsAttached => Attached;
42 
43  private bool attachable, attached, attachedByDefault;
44  private Voronoi2.VoronoiCell attachTargetCell;
45  private PhysicsBody body;
46 
47  public readonly ImmutableDictionary<StatTypes, float> HoldableStatValues;
48 
50  {
51  get;
52  private set;
53  }
54  [Serialize(true, IsPropertySaveable.Yes, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")]
55  public bool CanPush
56  {
57  get;
58  set;
59  }
60 
62  {
63  get { return item.body ?? body; }
64  }
65 
66  [Serialize(false, IsPropertySaveable.Yes, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")]
67  public bool Attached
68  {
69  get { return attached && item.ParentInventory == null; }
70  set
71  {
72  attached = value;
75  }
76  }
77 
78  [Serialize(true, IsPropertySaveable.Yes, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")]
79  public bool Aimable
80  {
81  get;
82  set;
83  }
84 
85  [Serialize(false, IsPropertySaveable.No, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")]
86  public bool ControlPose
87  {
88  get;
89  set;
90  }
91 
92  [Serialize(false, IsPropertySaveable.No, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")]
94  {
95  get;
96  set;
97  }
98 
99  [Serialize(false, IsPropertySaveable.No, description: "Can the item be attached to walls.")]
100  public bool Attachable
101  {
102  get { return attachable; }
103  set { attachable = value; }
104  }
105 
106  [Serialize(true, IsPropertySaveable.No, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")]
107  public bool Reattachable
108  {
109  get;
110  set;
111  }
112 
113  [Serialize(false, IsPropertySaveable.No, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")]
114  public bool LimitedAttachable
115  {
116  get;
117  set;
118  }
119 
120  [Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
121  public bool AttachedByDefault
122  {
123  get { return attachedByDefault; }
124  set { attachedByDefault = value; }
125  }
126 
127  [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
128  " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
129  public Vector2 HoldPos
130  {
131  get { return ConvertUnits.ToDisplayUnits(holdPos); }
132  set { holdPos = ConvertUnits.ToSimUnits(value); }
133  }
134  //the distance from the holding characters elbow to center of the physics body of the item
135  protected Vector2 holdPos;
136 
137 
138  [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
139  " Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
140  public Vector2 AimPos
141  {
142  get { return ConvertUnits.ToDisplayUnits(aimPos); }
143  set { aimPos = ConvertUnits.ToSimUnits(value); }
144  }
145  protected Vector2 aimPos;
146 
147  protected float holdAngle;
148 #if DEBUG
149  [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
150 #else
151  [Serialize(0.0f, IsPropertySaveable.No)]
152 #endif
153  public float HoldAngle
154  {
155  get { return MathHelper.ToDegrees(holdAngle); }
156  set { holdAngle = MathHelper.ToRadians(value); }
157  }
158 
159  protected float aimAngle;
160 #if DEBUG
161  [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item while aiming (in degrees, relative to the rotation of the character's hand).")]
162 #else
163  [Serialize(0.0f, IsPropertySaveable.No)]
164 #endif
165  public float AimAngle
166  {
167  get { return MathHelper.ToDegrees(aimAngle); }
168  set { aimAngle = MathHelper.ToRadians(value); }
169  }
170 
171  private Vector2 swingAmount;
172 #if DEBUG
173  [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
174 #else
175  [Serialize("0.0,0.0", IsPropertySaveable.No)]
176 #endif
177  public Vector2 SwingAmount
178  {
179  get { return ConvertUnits.ToDisplayUnits(swingAmount); }
180  set { swingAmount = ConvertUnits.ToSimUnits(value); }
181  }
182 #if DEBUG
183  [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")]
184 #else
185  [Serialize(0.0f, IsPropertySaveable.No)]
186 #endif
187 
188  public float SwingSpeed { get; set; }
189 
190 #if DEBUG
191  [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")]
192 #else
193  [Serialize(false, IsPropertySaveable.No)]
194 #endif
195  public bool SwingWhenHolding { get; set; }
196 
197 #if DEBUG
198  [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")]
199 #else
200  [Serialize(false, IsPropertySaveable.No)]
201 #endif
202  public bool SwingWhenAiming { get; set; }
203 
204 #if DEBUG
205  [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")]
206 #else
207  [Serialize(false, IsPropertySaveable.No)]
208 #endif
209  public bool SwingWhenUsing { get; set; }
210 
211 #if DEBUG
213 #else
214  [Serialize(false, IsPropertySaveable.No)]
215 #endif
216  public bool DisableHeadRotation { get; set; }
217 
218  [Serialize(false, IsPropertySaveable.No, description: "If true, this item can't be used if the character is also holding a ranged weapon.")]
219  public bool DisableWhenRangedWeaponEquipped { get; set; }
220 
221  [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")]
223  {
224  get;
225  set;
226  }
227 
229  : base(item, element)
230  {
231  body = item.body;
232 
233  Pusher = null;
234  if (element.GetAttributeBool("blocksplayers", false))
235  {
237  item.body.Density,
238  BodyType.Dynamic,
239  Physics.CollisionItemBlocking,
240  Physics.CollisionCharacter | Physics.CollisionProjectile)
241  {
242  Enabled = false,
243  UserData = this
244  };
245  Pusher.FarseerBody.OnCollision += OnPusherCollision;
246  Pusher.FarseerBody.FixedRotation = false;
247  Pusher.FarseerBody.IgnoreGravity = true;
248  }
249 
250  handlePos = new Vector2[2];
251  scaledHandlePos = new Vector2[2];
252  Vector2 previousValue = Vector2.Zero;
253  for (int i = 1; i < 3; i++)
254  {
255  int index = i - 1;
256  string attributeName = "handle" + i;
257  var attribute = element.GetAttribute(attributeName);
258  // If no value is defind for handle2, use the value of handle1.
259  var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
260  handlePos[index] = value;
261  previousValue = value;
262  }
263 
264  canBePicked = true;
265  prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(RequiredItems);
266 
267  if (attachable)
268  {
269  prevMsg = DisplayMsg;
270  prevPickKey = PickKey;
271 
272  if (item.Submarine != null)
273  {
274  if (item.Submarine.Loading)
275  {
276  AttachToWall();
277  Attached = false;
278  }
279  else //the submarine is not being loaded, which means we're either in the sub editor or the item has been spawned mid-round
280  {
282  {
283  //in the sub editor, attach
284  AttachToWall();
285  }
286  else
287  {
288  //spawned mid-round, deattach
290  }
291  }
292  }
293  }
294  characterUsable = element.GetAttributeBool("characterusable", true);
295 
296  Dictionary<StatTypes, float> statValues = new Dictionary<StatTypes, float>();
297  foreach (var subElement in element.GetChildElements("statvalue"))
298  {
299  StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name);
300  float statValue = subElement.GetAttributeFloat("value", 0f);
301  if (statValues.ContainsKey(statType))
302  {
303  statValues[statType] += statValue;
304  }
305  else
306  {
307  statValues.TryAdd(statType, statValue);
308  }
309  }
310  HoldableStatValues = statValues.ToImmutableDictionary();
311  }
312 
313  private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact)
314  {
315  if (other.Body.UserData is Character character)
316  {
317  if (!IsActive) { return false; }
318  if (!CanPush) { return false; }
319  return character != picker;
320  }
321  else
322  {
323  return true;
324  }
325  }
326 
327  private bool loadedFromInstance;
328  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
329  {
330  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
331 
332  loadedFromInstance = true;
333 
334  if (usePrefabValues)
335  {
336  //this needs to be loaded regardless
337  Attached = componentElement.GetAttributeBool("attached", attached);
338  }
339 
340  if (attachable)
341  {
342  prevMsg = DisplayMsg;
343  prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(RequiredItems);
344  }
345  }
346 
347  public override void Drop(Character dropper, bool setTransform = true)
348  {
349  Drop(true, dropper, setTransform);
350  }
351 
352  private void Drop(bool dropConnectedWires, Character dropper, bool setTransform = true)
353  {
354  GetRope()?.Snap();
355  if (dropConnectedWires)
356  {
357  DropConnectedWires(dropper);
358  }
359 
360  if (attachable)
361  {
362  if (body != null)
363  {
364  item.body = body;
365  }
367  }
368 
369  if (Pusher != null) { Pusher.Enabled = false; }
370  if (item.body != null) { item.body.Enabled = true; }
371 
372  IsActive = false;
373  attachTargetCell = null;
374 
375  if (picker == null || picker.Removed)
376  {
377  if (dropper == null || dropper.Removed) { return; }
378  picker = dropper;
379  }
380  if (picker.Inventory == null) { return; }
381 
383 
384  if (item.body != null && setTransform)
385  {
386  if (item.body.Removed)
387  {
388  DebugConsole.ThrowError(
389  "Failed to drop the Holdable component of the item \"" + item.Name + "\" (body has been removed"
390  + (item.Removed ? ", item has been removed)" : ")"));
391  }
392  else
393  {
395  Limb heldHand, arm;
397  {
398  heldHand = picker.AnimController.GetLimb(LimbType.LeftHand);
399  arm = picker.AnimController.GetLimb(LimbType.LeftArm);
400  }
401  else
402  {
403  heldHand = picker.AnimController.GetLimb(LimbType.RightHand);
404  arm = picker.AnimController.GetLimb(LimbType.RightArm);
405  }
406  if (heldHand != null && !heldHand.Removed && arm != null && !arm.Removed)
407  {
408  //hand simPosition is actually in the wrist so need to move the item out from it slightly
409  Vector2 diff = new Vector2(
410  (heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
411  (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f);
412  item.SetTransform(heldHand.SimPosition + diff, 0.0f);
413  }
414  else
415  {
417  }
418  }
419  }
420 
422  picker = null;
423  }
424 
425  public override void Equip(Character character)
426  {
427  //if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
428  if (item.GetComponents<Pickable>().Count() > 0)
429  {
430  bool inSuitableSlot = false;
431  for (int i = 0; i < character.Inventory.Capacity; i++)
432  {
433  if (character.Inventory.GetItemsAt(i).Contains(item))
434  {
435  if (character.Inventory.SlotTypes[i] != InvSlotType.Any &&
436  allowedSlots.Any(a => a.HasFlag(character.Inventory.SlotTypes[i])))
437  {
438  inSuitableSlot = true;
439  break;
440  }
441  }
442  }
443  if (!inSuitableSlot) { return; }
444  }
445 
446  picker = character;
447 
448  if (item.Removed)
449  {
450  DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace.CleanupStackTrace());
451  return;
452  }
453 
454  //cannot hold and wear an item at the same time
455  //(unless the slot in which it's held and worn are equal - e.g. a suit with built-in tool or weapon on one hand)
456  var wearable = item.GetComponent<Wearable>();
457  if (wearable != null && !wearable.AllowedSlots.SequenceEqual(allowedSlots))
458  {
459  wearable.Unequip(character);
460  }
461 
462  if (character != null) { item.Submarine = character.Submarine; }
463  if (item.body == null)
464  {
465  if (body != null)
466  {
467  item.body = body;
468  }
469  else
470  {
471  return;
472  }
473  }
474 
475  if (!item.body.Enabled)
476  {
478  item.SetTransform(hand != null ? hand.SimPosition : character.SimPosition, 0.0f);
479  }
480 
481  bool alreadyEquipped = character.HasEquippedItem(item);
483  {
484  item.body.Enabled = true;
485  item.body.PhysEnabled = false;
486  IsActive = true;
487 
488 #if SERVER
489  if (picker != prevEquipper) { GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); }
490 #endif
491  prevEquipper = picker;
492  }
493  else
494  {
495  prevEquipper = null;
496  }
497  }
498 
499  public override void Unequip(Character character)
500  {
501 #if SERVER
502  if (prevEquipper != null)
503  {
504  GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
505  }
506 #endif
507  prevEquipper = null;
508  if (picker == null) { return; }
509  item.body.PhysEnabled = true;
510  item.body.Enabled = false;
511  IsActive = false;
512  }
513 
514  public bool CanBeAttached(Character user)
515  {
516  if (!attachable || !Reattachable) { return false; }
517 
518  //can be attached anywhere in sub editor
519  if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
520 
521  Vector2 attachPos = user == null ? item.WorldPosition : GetAttachPosition(user, useWorldCoordinates: true);
522 
523  //can be attached anywhere inside hulls
524  if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; }
525 
526  return Structure.GetAttachTarget(attachPos) != null || GetAttachTargetCell(100.0f) != null;
527  }
528 
529  public bool CanBeDeattached()
530  {
531  if (!attachable || !attached) { return true; }
532 
533  //allow deattaching everywhere in sub editor
534  if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
535 
536  if (item.GetComponent<LevelResource>() != null) { return true; }
537 
538  if (item.GetComponent<Planter>() is { } planter && planter.GrowableSeeds.Any(seed => seed != null)) { return false; }
539 
540  //if the item has a connection panel and rewiring is disabled, don't allow deattaching
541  var connectionPanel = item.GetComponent<ConnectionPanel>();
542  if (connectionPanel != null && !connectionPanel.AlwaysAllowRewiring && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
543  {
544  return false;
545  }
546 
547  if (item.CurrentHull == null)
548  {
549  return attachTargetCell != null || Structure.GetAttachTarget(item.WorldPosition) != null;
550  }
551  else
552  {
553  return true;
554  }
555  }
556 
557  public override bool Pick(Character picker)
558  {
559  if (item.Removed)
560  {
561  DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace.CleanupStackTrace());
562  return false;
563  }
564 
565  if (!attachable)
566  {
567  return base.Pick(picker);
568  }
569 
570  if (!CanBeDeattached()) { return false; }
571 
572  if (Attached)
573  {
574  return base.Pick(picker);
575  }
576  else
577  {
578 
579  //not attached -> pick the item instantly, ignoring picking time
580  return OnPicked(picker);
581  }
582  }
583 
584  public override bool OnPicked(Character picker)
585  {
586 #if CLIENT
587  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
588  {
590  {
592  }
593  return false;
594  }
595 #endif
596  bool wasAttached = IsAttached;
597  if (base.OnPicked(picker))
598  {
600 
601 #if SERVER
602  if (GameMain.Server != null && attachable)
603  {
604  item.CreateServerEvent(this);
605  if (picker != null && wasAttached)
606  {
607  GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
608  }
609  }
610 #endif
611  return true;
612  }
613 
614  return false;
615  }
616 
617  public void AttachToWall()
618  {
619  if (!attachable) { return; }
620 
621  if (body == null)
622  {
623  throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
624  }
625 
626  body.Enabled = false;
627  body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
628  item.body = null;
629 
630  //outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
631  if (item.CurrentHull == null && item.Submarine == null)
632  {
634  if (attachTarget != null)
635  {
636  if (attachTarget.Submarine != null)
637  {
638  //set to submarine-relative position
639  item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
641  }
642  item.Submarine = attachTarget.Submarine;
643  }
644  else
645  {
646  attachTargetCell = GetAttachTargetCell(150.0f);
647  if (attachTargetCell != null && attachTargetCell.IsDestructible)
648  {
649  attachTargetCell.OnDestroyed += () =>
650  {
651  if (attachTargetCell != null && attachTargetCell.CellType != Voronoi2.CellType.Solid)
652  {
653  Drop(dropConnectedWires: true, dropper: null);
654  }
655  };
656  }
657  }
658  }
659 
660  var containedItems = item.OwnInventory?.AllItems;
661  if (containedItems != null)
662  {
663  foreach (Item contained in containedItems)
664  {
665  if (contained?.body == null) { continue; }
666  contained.SetTransform(item.SimPosition, contained.body.Rotation);
667  }
668  }
669 
670  DisplayMsg = prevMsg;
671  PickKey = prevPickKey;
672  RequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
673 
674  Attached = true;
675 #if CLIENT
676  item.DrawDepthOffset = 0.0f;
677 #endif
678  }
679 
680  public void DeattachFromWall()
681  {
682  if (!attachable) { return; }
683 
684  Attached = false;
685  attachTargetCell = null;
686 #if CLIENT
687  item.DrawDepthOffset = 0.0f;
688 #endif
689  //make the item pickable with the default pick key and with no specific tools/items when it's deattached
690  RequiredItems.Clear();
691  DisplayMsg = "";
692  PickKey = InputType.Select;
693 #if CLIENT
695 #endif
696  foreach (LightComponent light in item.GetComponents<LightComponent>())
697  {
698  light.CheckIfNeedsUpdate();
699  }
700  }
701 
702  public override void ParseMsg()
703  {
704  base.ParseMsg();
705  if (Attachable)
706  {
707  prevMsg = DisplayMsg;
708  }
709  }
710 
711  public override bool Use(float deltaTime, Character character = null)
712  {
713  if (UsageDisabledByRangedWeapon(character)) { return false; }
714  if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
715  if (character != null)
716  {
717  if (!characterUsable && !attachable) { return false; }
718  if (!character.IsKeyDown(InputType.Aim)) { return false; }
719  if (!CanBeAttached(character)) { return false; }
720 
721  if (LimitedAttachable)
722  {
723  if (character?.Info == null)
724  {
725  DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!");
726  return false;
727  }
728  Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
729  Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
730  int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
731 
732  int currentlyAttachedCount = Item.ItemList.Count(
733  i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
734  if (maxAttachableCount == 0)
735  {
736 #if CLIENT
737  if (character == Character.Controlled)
738  {
739  GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
740  }
741 #endif
742  return false;
743  }
744  else if (currentlyAttachedCount >= maxAttachableCount)
745  {
746 #if CLIENT
747  if (character == Character.Controlled)
748  {
749  GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
750  }
751 #endif
752  return false;
753  }
754  }
755 
756  if (GameMain.NetworkMember != null)
757  {
758  if (character != Character.Controlled)
759  {
760  return false;
761  }
762  else if (GameMain.NetworkMember.IsServer)
763  {
764  return false;
765  }
766  else
767  {
768 #if CLIENT
769  Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
770  item.CreateClientEvent(this, new EventData(attachPos));
771 #endif
772  }
773  return false;
774  }
775  else
776  {
777  item.Drop(character);
778  item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false);
779  //the light source won't get properly updated if lighting is disabled (even though the light sprite is still drawn when lighting is disabled)
780  //so let's ensure the light source is up-to-date
781  RefreshLightSources(item);
782  }
783  AttachToWall();
784  }
785  return true;
786 
787  static void RefreshLightSources(Item item)
788  {
790  foreach (var light in item.GetComponents<LightComponent>())
791  {
792  light.SetLightSourceTransform();
793  }
794  item.GetComponent<ItemContainer>()?.SetContainedItemPositions();
795  foreach (var containedItem in item.ContainedItems)
796  {
797  RefreshLightSources(containedItem);
798  }
799  }
800  }
801 
802 
803  public override bool SecondaryUse(float deltaTime, Character character = null)
804  {
805  return true;
806  }
807 
808  private Vector2 GetAttachPosition(Character user, bool useWorldCoordinates = false)
809  {
810  if (user == null) { return useWorldCoordinates ? item.WorldPosition : item.Position; }
811 
812  Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
813  mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
814 
815  Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
816  Vector2 attachPos = userPos + mouseDiff;
817 
818  //offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor
819  //in the sub editor, we align the top-left corner of the item with the grid
820  //but here the origin of the item is placed at the attach position, so we need to offset it
821  Vector2 offset = new Vector2(
822  -(item.Rect.Width / 2) % Submarine.GridSize.X,
823  (item.Rect.Height / 2) % Submarine.GridSize.Y);
824 
825  if (user.Submarine != null)
826  {
827  if (Submarine.PickBody(
828  ConvertUnits.ToSimUnits(user.Position),
829  ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
830  {
831  attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
832 
833  //round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
834  return
835  new Vector2(
836  (mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X,
837  (mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y)
838  - offset;
839  }
840  }
841  else if (Level.Loaded != null)
842  {
843  bool edgeFound = false;
844  foreach (var cell in Level.Loaded.GetCells(attachPos))
845  {
846  if (cell.CellType != Voronoi2.CellType.Solid) { continue; }
847  foreach (var edge in cell.Edges)
848  {
849  if (!edge.IsSolid) { continue; }
850  if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
851  {
852  attachPos = intersection;
853  edgeFound = true;
854  break;
855  }
856  }
857  if (edgeFound) { break; }
858  }
859  }
860 
861  return new Vector2(
862  MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X),
863  MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset;
864  }
865 
866  private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
867  {
868  if (Level.Loaded == null) { return null; }
869  foreach (var cell in Level.Loaded.GetCells(item.WorldPosition, searchDepth: 1))
870  {
871  if (cell.CellType != Voronoi2.CellType.Solid) { continue; }
872  Vector2 diff = cell.Center - item.WorldPosition;
873  if (diff.LengthSquared() > 0.0001f) { diff = Vector2.Normalize(diff); }
874  if (cell.IsPointInside(item.WorldPosition + diff * maxDist))
875  {
876  return cell;
877  }
878  }
879  return null;
880  }
881 
882  public override void UpdateBroken(float deltaTime, Camera cam)
883  {
884  Update(deltaTime, cam);
885  }
886 
887  public Rope GetRope()
888  {
889  var rangedWeapon = Item.GetComponent<RangedWeapon>();
890  if (rangedWeapon != null)
891  {
892  var lastProjectile = rangedWeapon.LastProjectile;
893  if (lastProjectile != null)
894  {
895  return lastProjectile.Item.GetComponent<Rope>();
896  }
897  }
898  return null;
899  }
900 
901  public override void Update(float deltaTime, Camera cam)
902  {
903  if (item.body == null || !item.body.Enabled) { return; }
904 
906 
907  if (owner != null)
908  {
909  ApplyStatusEffects(ActionType.OnActive, deltaTime, owner);
910  }
911 
912  if (picker == null || !picker.HasEquippedItem(item))
913  {
914  if (Pusher != null) { Pusher.Enabled = false; }
915  if (attachTargetCell == null && owner == null) { IsActive = false; }
916  return;
917  }
918 
920  {
921  Drawable = true;
922  }
923 
924  UpdateSwingPos(deltaTime, out Vector2 swingPos);
926  {
927  item.FlipX(relativeToSub: false);
928  }
929 
931 
932  if (picker.HeldItems.Contains(item))
933  {
934  scaledHandlePos[0] = handlePos[0] * item.Scale;
935  scaledHandlePos[1] = handlePos[1] * item.Scale;
936  bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker);
937  if (aim)
938  {
939  if (picker.AnimController.IsHoldingToRope && GetRope() is { Snapped: false } rope)
940  {
941  Vector2 targetPos = Submarine.GetRelativeSimPosition(picker, rope.Item);
942  picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos, aim: true, holdAngle, aimAngle, targetPos: targetPos);
943  }
944  else
945  {
946  picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos + swingPos, aim: true, holdAngle, aimAngle);
947  }
948  }
949  else
950  {
951  picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: holdPos + swingPos, aim: false, holdAngle);
952  if (GetRope() is { SnapWhenNotAimed: true } rope)
953  {
954  if (rope.Item.ParentInventory == null)
955  {
956  rope.Snap();
957  }
958  }
959  }
960  }
961  else
962  {
963  GetRope()?.Snap();
964  Limb equipLimb = null;
966  {
967  equipLimb = picker.AnimController.GetLimb(LimbType.Head);
968  }
969  else if (picker.Inventory.IsInLimbSlot(item, InvSlotType.InnerClothes) ||
971  {
972  equipLimb = picker.AnimController.GetLimb(LimbType.Torso);
973  }
974 
975  if (equipLimb != null && !equipLimb.Removed)
976  {
977  float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir);
978 
979  Matrix itemTransfrom = Matrix.CreateRotationZ(equipLimb.Rotation);
980  Vector2 transformedHandlePos = Vector2.Transform(handlePos[0] * item.Scale, itemTransfrom);
981 
983  item.SetTransform(equipLimb.SimPosition - transformedHandlePos, itemAngle);
984  }
985  }
986  }
987 
988  public void UpdateSwingPos(float deltaTime, out Vector2 swingPos)
989  {
990  swingPos = Vector2.Zero;
991  if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f)
992  {
993  swingState += deltaTime;
994  swingState %= 1.0f;
995  if (SwingWhenHolding ||
998  {
999  swingPos = swingAmount * new Vector2(
1000  PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
1001  PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
1002  }
1003  }
1004  }
1005 
1006  protected bool UsageDisabledByRangedWeapon(Character character)
1007  {
1008  if (DisableWhenRangedWeaponEquipped && character != null)
1009  {
1010  if (character.HeldItems.Any(it => it.GetComponent<RangedWeapon>() != null)) { return true; }
1011  }
1012  return false;
1013  }
1014 
1015  public override void ReceiveSignal(Signal signal, Connection connection)
1016  {
1017  //do nothing
1018  }
1019 
1020  public override void FlipX(bool relativeToSub)
1021  {
1022  handlePos[0].X = -handlePos[0].X;
1023  handlePos[1].X = -handlePos[1].X;
1024  if (item.body != null)
1025  {
1026  item.body.Dir = -item.body.Dir;
1027  }
1028  }
1029 
1030  public override void OnItemLoaded()
1031  {
1032  if (item.Submarine != null && item.Submarine.Loading) { return; }
1033  OnMapLoaded();
1035  }
1036 
1037  public override void OnMapLoaded()
1038  {
1039  if (!attachable) { return; }
1040 
1041  //the Holdable component didn't get loaded from an instance of the item, just the prefab xml = a mod or update must've made the item movable/detachable
1042  if (!loadedFromInstance)
1043  {
1044  if (attachedByDefault)
1045  {
1046  AttachToWall();
1047  return;
1048  }
1049  }
1050 
1051  if (Attached)
1052  {
1053  AttachToWall();
1054  }
1055  else
1056  {
1057  if (body != null)
1058  {
1060  item.body = body;
1061  body.Enabled = item.ParentInventory == null;
1062  }
1063  DeattachFromWall();
1064  }
1065  }
1066 
1067  protected override void RemoveComponentSpecific()
1068  {
1069  base.RemoveComponentSpecific();
1070  attachTargetCell = null;
1071  if (Pusher != null)
1072  {
1073  Pusher.Remove();
1074  Pusher = null;
1075  }
1076  body = null;
1077  }
1078 
1079  public override XElement Save(XElement parentElement)
1080  {
1081  if (!attachable)
1082  {
1083  return base.Save(parentElement);
1084  }
1085 
1086  var tempMsg = DisplayMsg;
1087  var tempRequiredItems = RequiredItems;
1088 
1089  DisplayMsg = prevMsg;
1090  RequiredItems = prevRequiredItems;
1091 
1092  XElement saveElement = base.Save(parentElement);
1093 
1094  DisplayMsg = tempMsg;
1095  RequiredItems = tempRequiredItems;
1096 
1097  return saveElement;
1098  }
1099 
1100  }
1101 }
static StatTypes ParseStatType(string statTypeString, string debugIdentifier)
void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 itemPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle=0.0f, bool aimMelee=false, Vector2? targetPos=null)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
void FlashAllowedSlots(Item item, Color color)
Flash the slots the item is allowed to go in (not taking into account whether there's already somethi...
bool GetAttributeBool(string key, bool def)
XAttribute? GetAttribute(string name)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static NetworkMember NetworkMember
Definition: GameMain.cs:190
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
IEnumerable< Item > GetItemsAt(int index)
Get all the item stored in the specified inventory slot. Can return more than one item if the slot co...
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
override void FlipX(bool relativeToSub)
Flip the entity horizontally
void CheckCleanable()
Recheck if the item needs to be included in the list of cleanable items
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
override bool SecondaryUse(float deltaTime, Character character=null)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
override bool Pick(Character picker)
a Character has picked the item
override void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
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)
Dictionary< RelatedItem.RelationType, List< RelatedItem > > RequiredItems
List< InvSlotType > allowedSlots
Definition: Pickable.cs:14
void DropConnectedWires(Character character)
Definition: Pickable.cs:248
bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform=true)
readonly Identifier Identifier
Definition: Prefab.cs:34
bool IsHoldingToRope
Is attached to something with a rope.
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.
static Structure GetAttachTarget(Vector2 worldPosition)
Checks if there's a structure items can be attached to at the given position and returns it.
static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? targetWorldPos=null)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
Interface for entities that the clients can send events to the server
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180