Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Items/Inventory.cs
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 
10 namespace Barotrauma
11 {
12  class VisualSlot
13  {
14  public Rectangle Rect;
15 
17 
18  public bool Disabled;
19 
21 
22  public Vector2 DrawOffset;
23 
24  public Color Color;
25 
26  public Color HighlightColor;
27  public float HighlightScaleUpAmount;
28  private CoroutineHandle highlightCoroutine;
29  public float HighlightTimer;
30 
32 
33  public int InventoryKeyIndex = -1;
34 
35  public int SubInventoryDir = -1;
36 
37  public bool IsHighlighted
38  {
39  get
40  {
41  return State == GUIComponent.ComponentState.Hover;
42  }
43  }
44 
45  public float QuickUseTimer;
47  public bool IsMoving = false;
48 
49  private static Rectangle offScreenRect = new Rectangle(new Point(-1000, 0), Point.Zero);
52  {
53  get
54  {
55  // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen
56  if (IsMoving) { return offScreenRect; }
57 
58  int buttonDir = Math.Sign(SubInventoryDir);
59 
61 
62  Vector2 equipIndicatorPos = new Vector2(Rect.Left, Rect.Center.Y + (Rect.Height / 2 + 15 * Inventory.UIScale) * buttonDir - sizeY / 2f);
63  equipIndicatorPos += DrawOffset;
64 
65  return new Rectangle((int)equipIndicatorPos.X, (int)equipIndicatorPos.Y, (int)Rect.Width, (int)sizeY);
66  }
67  }
68 
69  public VisualSlot(Rectangle rect)
70  {
71  Rect = rect;
72  InteractRect = rect;
73  InteractRect.Inflate(5, 5);
75  Color = Color.White * 0.4f;
76  }
77 
78  public bool MouseOn()
79  {
80  Rectangle rect = InteractRect;
81  rect.Location += DrawOffset.ToPoint();
82  return rect.Contains(PlayerInput.MousePosition);
83  }
84 
85  public void ShowBorderHighlight(Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount = 0.5f)
86  {
87  if (highlightCoroutine != null)
88  {
89  CoroutineManager.StopCoroutines(highlightCoroutine);
90  highlightCoroutine = null;
91  }
92 
93  HighlightScaleUpAmount = scaleUpAmount;
94  currentHighlightState = 0.0f;
95  this.fadeInDuration = fadeInDuration;
96  this.fadeOutDuration = fadeOutDuration;
97  currentHighlightColor = color;
98  HighlightTimer = 1.0f;
99  highlightCoroutine = CoroutineManager.StartCoroutine(UpdateBorderHighlight());
100  }
101 
102  private float currentHighlightState, fadeInDuration, fadeOutDuration;
103  private Color currentHighlightColor;
104  private IEnumerable<CoroutineStatus> UpdateBorderHighlight()
105  {
106  HighlightTimer = 1.0f;
107  while (currentHighlightState < fadeInDuration + fadeOutDuration)
108  {
109  HighlightColor = (currentHighlightState < fadeInDuration) ?
110  Color.Lerp(Color.Transparent, currentHighlightColor, currentHighlightState / fadeInDuration) :
111  Color.Lerp(currentHighlightColor, Color.Transparent, (currentHighlightState - fadeInDuration) / fadeOutDuration);
112 
113  currentHighlightState += CoroutineManager.DeltaTime;
114  HighlightTimer = 1.0f - currentHighlightState / (fadeInDuration + fadeOutDuration);
115 
116  yield return CoroutineStatus.Running;
117  }
118 
119  HighlightTimer = 0.0f;
120  HighlightColor = Color.Transparent;
121 
122  yield return CoroutineStatus.Success;
123  }
124 
128  public void MoveBorderHighlight(VisualSlot newSlot)
129  {
130  if (highlightCoroutine == null) { return; }
131  CoroutineManager.StopCoroutines(highlightCoroutine);
132  highlightCoroutine = null;
133 
135  newSlot.currentHighlightState = currentHighlightState;
136  newSlot.fadeInDuration = fadeInDuration;
137  newSlot.fadeOutDuration = fadeOutDuration;
138  newSlot.currentHighlightColor = currentHighlightColor;
139  newSlot.highlightCoroutine = CoroutineManager.StartCoroutine(newSlot.UpdateBorderHighlight());
140  }
141  }
142 
143  partial class Inventory
144  {
145  public static float UIScale
146  {
147  get { return (GameMain.GraphicsWidth / 1920.0f + GameMain.GraphicsHeight / 1080.0f) / 2.5f * GameSettings.CurrentConfig.Graphics.InventoryScale; }
148  }
149 
150  public static int ContainedIndicatorHeight
151  {
152  get { return (int)(15 * UIScale); }
153  }
154 
155  protected float prevUIScale = UIScale;
156  protected float prevHUDScale = GUI.Scale;
157  protected Point prevScreenResolution;
158 
159  protected static Sprite slotHotkeySprite;
160 
161  private static Sprite slotSpriteSmall;
162  public static Sprite SlotSpriteSmall
163  {
164  get
165  {
166  if (slotSpriteSmall == null)
167  {
168  //TODO: define this in xml
169  slotSpriteSmall = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(10, 6, 119, 120), null, 0);
170  // Adjustment to match the old size of 75,71
172  }
173  return slotSpriteSmall;
174  }
175  }
176 
177  public const float SlotSpriteSmallScale = 0.575f;
178 
179  public static Sprite DraggableIndicator;
180  public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator;
181 
183 
185 
186  public Rectangle BackgroundFrame { get; protected set; }
187 
188  private List<ushort>[] partialReceivedItemIDs;
189  private List<ushort>[] receivedItemIDs;
190  private CoroutineHandle syncItemsCoroutine;
191 
192  public float HideTimer;
193 
194  private bool isSubInventory;
195 
196  private const float movableFrameRectHeight = 40f;
197  private Color movableFrameRectColor = new Color(60, 60, 60);
198  private Rectangle movableFrameRect;
199  private Point savedPosition, originalPos;
200  private bool canMove = false;
201  private bool positionUpdateQueued = false;
202  private Vector2 draggableIndicatorOffset;
203  private float draggableIndicatorScale;
204 
205  public class SlotReference
206  {
207  public readonly Inventory ParentInventory;
208  public readonly int SlotIndex;
209  public VisualSlot Slot;
210 
212  public readonly Item Item;
213  public readonly bool IsSubSlot;
214  public RichString Tooltip { get; private set; }
215 
218 
219  public bool ForceTooltipRefresh;
220 
221  public SlotReference(Inventory parentInventory, VisualSlot slot, int slotIndex, bool isSubSlot, Inventory subInventory = null)
222  {
223  ParentInventory = parentInventory;
224  Slot = slot;
225  SlotIndex = slotIndex;
226  Inventory = subInventory;
227  IsSubSlot = isSubSlot;
228  Item = ParentInventory.GetItemAt(slotIndex);
229 
230  RefreshTooltip();
231  }
232 
233  public bool TooltipNeedsRefresh()
234  {
235  if (ForceTooltipRefresh) { return true; }
236  if (Item == null) { return false; }
237  if (PlayerInput.KeyDown(InputType.ContextualCommand) != tooltipShowedContextualOptions) { return true; }
239  }
240 
241  public void RefreshTooltip()
242  {
243  ForceTooltipRefresh = false;
244  if (Item == null) { return; }
245  IEnumerable<Item> itemsInSlot = null;
246  if (ParentInventory != null && Item != null)
247  {
248  itemsInSlot = ParentInventory.GetItemsAt(SlotIndex);
249  }
250  Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled);
253  }
254 
255  private static RichString GetTooltip(Item item, IEnumerable<Item> itemsInSlot, Character character)
256  {
257  if (item == null) { return null; }
258 
259  LocalizedString toolTip = "";
260  if (GameMain.DebugDraw)
261  {
262  toolTip = item.ToString();
263  }
264  else
265  {
266  LocalizedString description = item.Description;
267  if (item.HasTag(Tags.IdCardTag) || item.HasTag(Tags.DespawnContainer))
268  {
269  string[] readTags = item.Tags.Split(',');
270  string idName = null;
271  string idJob = null;
272  foreach (string tag in readTags)
273  {
274  string[] s = tag.Split(':');
275  switch (s[0])
276  {
277  case "name":
278  idName = s[1];
279  break;
280  case "job":
281  case "jobid":
282  idJob = s[1];
283  break;
284  }
285  }
286  if (idName != null)
287  {
288  if (idJob == null)
289  {
290  description = TextManager.GetWithVariable("IDCardName", "[name]", idName);
291  }
292  else
293  {
294  description = TextManager.GetWithVariables("IDCardNameJob",
295  ("[name]", idName, FormatCapitals.No),
296  ("[job]", TextManager.Get("jobname." + idJob).Fallback(idJob), FormatCapitals.Yes));
297  }
298  if (!string.IsNullOrEmpty(item.Description))
299  {
300  description = description + " " + item.Description;
301  }
302  }
303  }
304 
305  LocalizedString name = item.Name;
306  foreach (ItemComponent component in item.Components)
307  {
308  component.AddTooltipInfo(ref name, ref description);
309  }
310 
311  if (item.Prefab.ShowContentsInTooltip && item.OwnInventory != null)
312  {
313  foreach (string itemName in item.OwnInventory.AllItems.Select(it => it.Name).Distinct())
314  {
315  int itemCount = item.OwnInventory.AllItems.Count(it => it != null && it.Name == itemName);
316  description += itemCount == 1 ?
317  "\n " + itemName :
318  "\n " + itemName + " x" + itemCount;
319  }
320  }
321 
322  string colorStr = (item.Illegitimate ? GUIStyle.Red : Color.White).ToStringHex();
323 
324  toolTip = $"‖color:{colorStr}‖{name}‖color:end‖";
325  if (item.GetComponent<Quality>() != null)
326  {
327  toolTip += "\n" + TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "")
328  .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", ""))
329  .TrimStart();
330  }
331 
332  if (itemsInSlot.All(it => !it.IsInteractable(Character.Controlled)))
333  {
334  toolTip += " " + TextManager.Get("connectionlocked");
335  }
336  if (!item.IsFullCondition && !item.Prefab.HideConditionInTooltip)
337  {
338  string conditionColorStr = XMLExtensions.ToStringHex(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull));
339  toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖";
340  }
341  if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; }
342  if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null)
343  {
344  colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
345  toolTip += $"\n‖color:{colorStr}‖{item.Prefab.ContentPackage.Name}‖color:end‖";
346  }
347  }
348  if (itemsInSlot.Count() > 1)
349  {
350  toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖";
351  toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖";
352  }
353  if (item.Prefab.SkillRequirementHints != null && item.Prefab.SkillRequirementHints.Any())
354  {
355  toolTip += item.Prefab.GetSkillRequirementHints(character);
356  }
357 #if DEBUG
358  toolTip += $" ({item.Prefab.Identifier})";
359 #endif
360  if (PlayerInput.KeyDown(InputType.ContextualCommand))
361  {
362  toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖";
363  }
364  else
365  {
366  var colorStr = XMLExtensions.ToStringHex(Color.LightGray * 0.7f);
367  toolTip += $"\n‖color:{colorStr}‖{TextManager.Get("itemmsg.morreoptionsavailable")}‖color:end‖";
368  }
369 
370  return RichString.Rich(toolTip);
371  }
372  }
373 
374  public static VisualSlot DraggingSlot;
375  public static readonly List<Item> DraggingItems = new List<Item>();
376  public static bool DraggingItemToWorld
377  {
378  get
379  {
380  return Character.Controlled != null &&
383  DraggingItems.Any();
384  }
385  }
386 
387  public static readonly List<Item> doubleClickedItems = new List<Item>();
388 
389  protected Vector4 padding;
390 
391  private int slotsPerRow;
392  public int SlotsPerRow
393  {
394  set { slotsPerRow = Math.Max(1, value); }
395  }
396 
397  protected static HashSet<SlotReference> highlightedSubInventorySlots = new HashSet<SlotReference>();
398  private static readonly List<SlotReference> subInventorySlotsToDraw = new List<SlotReference>();
399 
400  protected static SlotReference selectedSlot;
401 
403 
404  private Rectangle prevRect;
409 
414  public bool DrawWhenEquipped;
415 
417  {
418  get
419  {
420  if (selectedSlot?.ParentInventory?.Owner == null || selectedSlot.ParentInventory.Owner.Removed)
421  {
422  return null;
423  }
424  return selectedSlot;
425  }
426  }
427 
429  {
430  return ReplacedBy?.GetReplacementOrThiS() ?? this;
431  }
432 
433  public virtual void CreateSlots()
434  {
436 
437  int rows = (int)Math.Ceiling((double)capacity / slotsPerRow);
438  int columns = Math.Min(slotsPerRow, capacity);
439 
440  Vector2 spacing = new Vector2(5.0f * UIScale);
442  Vector2 rectSize = new Vector2(60.0f * UIScale);
443 
444  padding = new Vector4(spacing.X, spacing.Y, spacing.X, spacing.X);
445 
446 
447  Vector2 slotAreaSize = new Vector2(
448  columns * rectSize.X + (columns - 1) * spacing.X,
449  rows * rectSize.Y + (rows - 1) * spacing.Y);
450  slotAreaSize.X += padding.X + padding.Z;
451  slotAreaSize.Y += padding.Y + padding.W;
452 
453  Vector2 topLeft = new Vector2(
454  GameMain.GraphicsWidth / 2 - slotAreaSize.X / 2,
455  GameMain.GraphicsHeight / 2 - slotAreaSize.Y / 2);
456 
457  Vector2 center = topLeft + slotAreaSize / 2;
458 
459  if (RectTransform != null)
460  {
461  Vector2 scale = new Vector2(
462  RectTransform.Rect.Width / slotAreaSize.X,
463  RectTransform.Rect.Height / slotAreaSize.Y);
464 
465  spacing *= scale;
466  rectSize *= scale;
467  padding.X *= scale.X; padding.Z *= scale.X;
468  padding.Y *= scale.Y; padding.W *= scale.Y;
469 
470  center = RectTransform.Rect.Center.ToVector2();
471 
472  topLeft = RectTransform.TopLeft.ToVector2() + new Vector2(padding.X, padding.Y);
473  prevRect = RectTransform.Rect;
474  }
475 
476  Rectangle slotRect = new Rectangle((int)topLeft.X, (int)topLeft.Y, (int)rectSize.X, (int)rectSize.Y);
477  for (int i = 0; i < capacity; i++)
478  {
479  int row = (int)Math.Floor((double)i / slotsPerRow);
480  int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow);
481  int slotNumberOnThisRow = i - row * slotsPerRow;
482 
483  int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1));
484  slotRect.X = (int)(center.X) - rowWidth / 2;
485  slotRect.X += (int)((rectSize.X + spacing.X) * (slotNumberOnThisRow % slotsPerThisRow));
486 
487  slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * row);
488  visualSlots[i] = new VisualSlot(slotRect);
490  (int)(visualSlots[i].Rect.X - spacing.X / 2 - 1), (int)(visualSlots[i].Rect.Y - spacing.Y / 2 - 1),
491  (int)(visualSlots[i].Rect.Width + spacing.X + 2), (int)(visualSlots[i].Rect.Height + spacing.Y + 2));
492 
493  if (visualSlots[i].Rect.Width > visualSlots[i].Rect.Height)
494  {
495  visualSlots[i].Rect.Inflate((visualSlots[i].Rect.Height - visualSlots[i].Rect.Width) / 2, 0);
496  }
497  else
498  {
499  visualSlots[i].Rect.Inflate(0, (visualSlots[i].Rect.Width - visualSlots[i].Rect.Height) / 2);
500  }
501  }
502 
503  if (selectedSlot != null && selectedSlot.ParentInventory == this)
504  {
506  }
508  }
509 
510  protected virtual void CalculateBackgroundFrame()
511  {
512  }
513 
514  public bool Movable()
515  {
516  return movableFrameRect.Size != Point.Zero;
517  }
518 
519  public bool IsInventoryHoverAvailable(Character owner, ItemContainer container)
520  {
521  if (container == null && this is ItemInventory)
522  {
523  container = (this as ItemInventory).Container;
524  }
525 
526  if (container == null) { return false; }
527  return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item);
528  }
529 
530  public virtual bool HideSlot(int i)
531  {
532  return visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty());
533  }
534 
535  public virtual void Update(float deltaTime, Camera cam, bool subInventory = false)
536  {
537  if (visualSlots == null || isSubInventory != subInventory ||
538  (RectTransform != null && RectTransform.Rect != prevRect))
539  {
540  CreateSlots();
541  isSubInventory = subInventory;
542  }
543 
544  if (!subInventory || (OpenState >= 0.99f || OpenState < 0.01f))
545  {
546  for (int i = 0; i < capacity; i++)
547  {
548  if (HideSlot(i)) { continue; }
549  UpdateSlot(visualSlots[i], i, slots[i].Items.FirstOrDefault(), subInventory);
550  }
551  if (!isSubInventory)
552  {
553  ControlInput(cam);
554  }
555  }
556  }
557 
558  protected virtual void ControlInput(Camera cam)
559  {
560  // Note that these targets are static. Therefore the outcome is the same if this method is called multiple times or only once.
561  if (selectedSlot != null && !DraggingItemToWorld && cam.GetZoomAmountFromPrevious() <= 0.25f)
562  {
563  cam.Freeze = true;
564  }
565  }
566 
567  protected void UpdateSlot(VisualSlot slot, int slotIndex, Item item, bool isSubSlot)
568  {
569  Rectangle interactRect = slot.InteractRect;
570  interactRect.Location += slot.DrawOffset.ToPoint();
571 
572  bool mouseOnGUI = false;
573  /*if (GUI.MouseOn != null)
574  {
575  //block usage if the mouse is on a GUIComponent that's not related to this inventory
576  if (RectTransform == null || (RectTransform != GUI.MouseOn.RectTransform && !GUI.MouseOn.IsParentOf(RectTransform.GUIComponent)))
577  {
578  mouseOnGUI = true;
579  }
580  }*/
581 
582  bool mouseOn = interactRect.Contains(PlayerInput.MousePosition) && !Locked && !mouseOnGUI && !slot.Disabled && IsMouseOnInventory;
583 
584  // Delete item from container in sub editor
586  {
587  DraggingItems.Clear();
588  var mouseDrag = SubEditorScreen.MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, SubEditorScreen.MouseDragStart) >= GUI.Scale * 20;
589  if (mouseOn && (PlayerInput.PrimaryMouseButtonClicked() || mouseDrag))
590  {
591  if (item != null)
592  {
593  slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f);
594  if (!mouseDrag)
595  {
596  SoundPlayer.PlayUISound(GUISoundType.PickItem);
597  }
598 
599  if (!item.Removed)
600  {
602  SubEditorScreen.BulkItemBuffer.Add(new AddOrDeleteCommand(new List<MapEntity> { item }, true));
603  }
604 
606  item.Remove();
607  }
608  }
609  }
610 
612  {
613  mouseOn = false;
614  }
615 
616  if (selectedSlot != null && selectedSlot.Slot != slot)
617  {
618  //subinventory slot highlighted -> don't allow highlighting this one
619  if (selectedSlot.IsSubSlot && !isSubSlot)
620  {
621  mouseOn = false;
622  }
623  else if (!selectedSlot.IsSubSlot && isSubSlot && mouseOn)
624  {
625  selectedSlot = null;
626  }
627  }
628 
629 
630  slot.State = GUIComponent.ComponentState.None;
631 
632  if (mouseOn && (DraggingItems.Any() || selectedSlot == null || selectedSlot.Slot == slot) && DraggingInventory == null)
633  {
634  slot.State = GUIComponent.ComponentState.Hover;
635 
636  if (selectedSlot == null || (!selectedSlot.IsSubSlot && isSubSlot))
637  {
638  var slotRef = new SlotReference(this, slot, slotIndex, isSubSlot, slots[slotIndex].FirstOrDefault()?.GetComponent<ItemContainer>()?.Inventory);
639  if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && slotRef.ParentInventory is CharacterInventory) { return; }
640  if (CanSelectSlot(slotRef))
641  {
642  selectedSlot = slotRef;
643  }
644  }
645 
646  if (!DraggingItems.Any())
647  {
648  var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => it.IsInteractable(Character.Controlled)) : slots[slotIndex].Items;
649  if (interactableItems.Any())
650  {
651  if (availableContextualOrder.target != null)
652  {
654  {
656  new Order(OrderPrefab.Prefabs[availableContextualOrder.orderIdentifier], availableContextualOrder.target, targetItem: null, orderGiver: Character.Controlled));
657  }
658  availableContextualOrder = default;
659  }
660  else if (PlayerInput.KeyDown(InputType.Command) &&
661  PlayerInput.KeyDown(InputType.ContextualCommand) &&
663  {
664  GameMain.GameSession.CrewManager.OpenCommandUI(interactableItems.FirstOrDefault(), forceContextual: true);
665  }
667  {
668  if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot))
669  {
670  DraggingItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2));
671  }
672  else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot))
673  {
674  DraggingItems.Add(interactableItems.First());
675  }
676  else
677  {
678  DraggingItems.AddRange(interactableItems);
679  }
680  DraggingSlot = slot;
681  }
682  }
683  }
685  {
686  var interactableItems = Screen.Selected == GameMain.GameScreen ?
687  slots[slotIndex].Items.Where(it => it.IsInteractable(Character.Controlled)) :
688  slots[slotIndex].Items;
689  if (PlayerInput.DoubleClicked() && interactableItems.Any())
690  {
691  doubleClickedItems.Clear();
692  if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot))
693  {
694  doubleClickedItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2));
695  }
696  else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot))
697  {
698  doubleClickedItems.Add(interactableItems.First());
699  }
700  else
701  {
702  doubleClickedItems.AddRange(interactableItems);
703  }
704  }
705  }
706  }
707  }
708 
709  protected Inventory GetSubInventory(int slotIndex)
710  {
711  var container = slots[slotIndex].FirstOrDefault()?.GetComponent<ItemContainer>();
712  if (container == null) { return null; }
713 
714  return container.Inventory;
715  }
716 
717  protected virtual ItemInventory GetActiveEquippedSubInventory(int slotIndex)
718  {
719  return null;
720  }
721 
722  public float OpenState;
723 
724  public void UpdateSubInventory(float deltaTime, int slotIndex, Camera cam)
725  {
726  var item = slots[slotIndex].FirstOrDefault();
727  if (item == null) { return; }
728 
729  var container = item.GetComponent<ItemContainer>();
730  if (container == null || !container.DrawInventory) { return; }
731  if (container.Inventory.DrawWhenEquipped) { return; }
732 
733  var subInventory = container.Inventory;
734  if (subInventory.visualSlots == null) { subInventory.CreateSlots(); }
735 
736  canMove = container.MovableFrame && !subInventory.IsInventoryHoverAvailable(Owner as Character, container) && subInventory.originalPos != Point.Zero;
737  if (this is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default)
738  {
739  canMove = false;
740  }
741 
742  if (canMove)
743  {
744  subInventory.HideTimer = 1.0f;
745  subInventory.OpenState = 1.0f;
746  if (subInventory.movableFrameRect.Contains(PlayerInput.MousePosition) && PlayerInput.SecondaryMouseButtonClicked())
747  {
748  container.Inventory.savedPosition = container.Inventory.originalPos;
749  }
750  if (subInventory.movableFrameRect.Contains(PlayerInput.MousePosition) || (DraggingInventory != null && DraggingInventory == subInventory))
751  {
752  if (DraggingInventory == null)
753  {
755  {
756  // Prevent us from dragging an item
757  DraggingItems.Clear();
758  DraggingSlot = null;
759  DraggingInventory = subInventory;
760  }
761  }
763  {
764  DraggingInventory = null;
765  subInventory.savedPosition = PlayerInput.MousePosition.ToPoint();
766  }
767  else if (DraggingInventory == subInventory)
768  {
769  subInventory.savedPosition = PlayerInput.MousePosition.ToPoint();
770  }
771  }
772  }
773 
774  int itemCapacity = subInventory.slots.Length;
775  var slot = visualSlots[slotIndex];
776  int dir = slot.SubInventoryDir;
777  Rectangle subRect = slot.Rect;
778  Vector2 spacing;
779 
780  spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale * GUI.AspectRatioAdjustment);
781 
782  int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow);
783  while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f)
784  {
785  columns++;
786  }
787 
788  int width = (int)(subRect.Width * columns + spacing.X * (columns - 1));
789  int startX = slot.Rect.Center.X - (int)(width / 2.0f);
790  int startY = dir < 0 ?
791  slot.EquipButtonRect.Y - subRect.Height - (int)(35 * UIScale) :
792  slot.EquipButtonRect.Bottom + (int)(10 * UIScale);
793 
794  if (canMove)
795  {
796  startX += subInventory.savedPosition.X - subInventory.originalPos.X;
797  startY += subInventory.savedPosition.Y - subInventory.originalPos.Y;
798  }
799 
800  float totalHeight = itemCapacity / columns * (subRect.Height + spacing.Y);
801  int padding = (int)(20 * UIScale);
802 
803  //prevent the inventory from extending outside the left side of the screen
804  startX = Math.Max(startX, padding);
805  //same for the right side of the screen
806  startX -= Math.Max(startX + width - GameMain.GraphicsWidth + padding, 0);
807 
808  //prevent the inventory from extending outside the top of the screen
809  startY = Math.Max(startY, (int)totalHeight - padding / 2);
810  //same for the bottom side of the screen
811  startY -= Math.Max(startY - GameMain.GraphicsHeight + padding * 2 + (canMove ? (int)(movableFrameRectHeight * UIScale) : 0), 0);
812 
813  subRect.X = startX;
814  subRect.Y = startY;
815 
816  subInventory.OpenState = subInventory.HideTimer >= 0.5f ?
817  Math.Min(subInventory.OpenState + deltaTime * 8.0f, 1.0f) :
818  Math.Max(subInventory.OpenState - deltaTime * 5.0f, 0.0f);
819 
820  for (int i = 0; i < itemCapacity; i++)
821  {
822  subInventory.visualSlots[i].Rect = subRect;
823  subInventory.visualSlots[i].Rect.Location += new Point(0, (int)totalHeight * -dir);
824 
825  subInventory.visualSlots[i].DrawOffset = Vector2.SmoothStep(new Vector2(0, -50 * dir), new Vector2(0, totalHeight * dir), subInventory.OpenState);
826 
827  subInventory.visualSlots[i].InteractRect = new Rectangle(
828  (int)(subInventory.visualSlots[i].Rect.X - spacing.X / 2 - 1), (int)(subInventory.visualSlots[i].Rect.Y - spacing.Y / 2 - 1),
829  (int)(subInventory.visualSlots[i].Rect.Width + spacing.X + 2), (int)(subInventory.visualSlots[i].Rect.Height + spacing.Y + 2));
830 
831  if ((i + 1) % columns == 0)
832  {
833  subRect.X = startX;
834  subRect.Y += subRect.Height * dir;
835  subRect.Y += (int)(spacing.Y * dir);
836  }
837  else
838  {
839  subRect.X = (int)(subInventory.visualSlots[i].Rect.Right + spacing.X);
840  }
841  }
842 
843  if (canMove)
844  {
845  subInventory.movableFrameRect.X = subRect.X - (int)spacing.X;
846  subInventory.movableFrameRect.Y = subRect.Y + (int)(spacing.Y);
847  }
848  visualSlots[slotIndex].State = GUIComponent.ComponentState.Hover;
849 
850  subInventory.isSubInventory = true;
851  subInventory.Update(deltaTime, cam, true);
852  }
853 
854  public void ClearSubInventories()
855  {
856  if (highlightedSubInventorySlots.Count == 0) { return; }
857 
858  foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots)
859  {
860  highlightedSubInventorySlot.Inventory.HideTimer = 0.0f;
861  }
862 
864  }
865 
866  public virtual void Draw(SpriteBatch spriteBatch, bool subInventory = false)
867  {
868  if (visualSlots == null || isSubInventory != subInventory) { return; }
869 
870  for (int i = 0; i < capacity; i++)
871  {
872  if (HideSlot(i)) { continue; }
873 
874  //don't draw the item if it's being dragged out of the slot
875  bool drawItem = !DraggingItems.Any() || !slots[i].Items.All(it => DraggingItems.Contains(it)) || visualSlots[i].MouseOn();
876 
877  DrawSlot(spriteBatch, this, visualSlots[i], slots[i].FirstOrDefault(), i, drawItem);
878  }
879  }
880 
886  public static bool IsMouseOnSlot(VisualSlot slot)
887  {
888  var rect = new Rectangle(slot.InteractRect.X, slot.InteractRect.Y, slot.InteractRect.Width, slot.InteractRect.Height);
889  rect.Offset(slot.DrawOffset);
890  return rect.Contains(PlayerInput.MousePosition);
891  }
892 
893  public static bool IsMouseOnInventory
894  {
895  get; private set;
896  }
897 
901  public static void RefreshMouseOnInventory()
902  {
903  IsMouseOnInventory = DetermineMouseOnInventory();
904  }
905 
909  private static bool DetermineMouseOnInventory(bool ignoreDraggedItem = false)
910  {
911  if (GameMain.GameSession?.Campaign != null &&
913  {
914  return false;
915  }
916  if (GameSession.IsTabMenuOpen) { return false; }
917  if (CrewManager.IsCommandInterfaceOpen) { return false; }
918 
919  if (Character.Controlled == null) { return false; }
920 
921  if (!ignoreDraggedItem)
922  {
923  if (DraggingItems.Any() || DraggingInventory != null) { return true; }
924  }
925 
926  var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode;
927 
928  if (Character.Controlled.Inventory != null && !isSubEditor)
929  {
930  if (IsOnInventorySlot(Character.Controlled.Inventory)) { return true; }
931  }
932 
933  if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor)
934  {
935  if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; }
936  }
937 
938  static bool IsOnInventorySlot(Inventory inventory)
939  {
940  for (var i = 0; i < inventory.visualSlots.Length; i++)
941  {
942  if (inventory.HideSlot(i)) { continue; }
943  var slot = inventory.visualSlots[i];
944  if (slot.InteractRect.Contains(PlayerInput.MousePosition))
945  {
946  return true;
947  }
948 
949  // check if the equip button actually exists
950  if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) &&
951  i >= 0 && inventory.slots.Length > i &&
952  !inventory.slots[i].Empty())
953  {
954  return true;
955  }
956  }
957  return false;
958  }
959 
960  if (Character.Controlled.SelectedItem != null)
961  {
962  foreach (var ic in Character.Controlled.SelectedItem.ActiveHUDs)
963  {
964  var itemContainer = ic as ItemContainer;
965  if (itemContainer?.Inventory?.visualSlots == null) { continue; }
966 
967  foreach (VisualSlot slot in itemContainer.Inventory.visualSlots)
968  {
969  if (slot.InteractRect.Contains(PlayerInput.MousePosition) ||
970  slot.EquipButtonRect.Contains(PlayerInput.MousePosition))
971  {
972  return true;
973  }
974  }
975  }
976  }
977 
978  foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots)
979  {
980  if (GetSubInventoryHoverArea(highlightedSubInventorySlot).Contains(PlayerInput.MousePosition)) { return true; }
981  }
982 
983  return false;
984  }
985 
987  {
988  var character = Character.Controlled;
989  if (character == null) { return CursorState.Default; }
990  if (DraggingItems.Any() || DraggingInventory != null) { return CursorState.Dragging; }
991 
992  var inv = character.Inventory;
993  var selInv = character.SelectedCharacter?.Inventory;
994 
995  if (inv == null) { return CursorState.Default; }
996 
997  foreach (var item in inv.AllItems)
998  {
999  var container = item?.GetComponent<ItemContainer>();
1000  if (container == null) { continue; }
1001 
1002  if (container.Inventory.visualSlots != null)
1003  {
1004  if (container.Inventory.visualSlots.Any(slot => slot.IsHighlighted))
1005  {
1006  return CursorState.Hand;
1007  }
1008  }
1009 
1010  if (container.Inventory.movableFrameRect.Contains(PlayerInput.MousePosition))
1011  {
1012  return CursorState.Move;
1013  }
1014  }
1015 
1016  if (selInv != null)
1017  {
1018  for (int i = 0; i < selInv.visualSlots.Length; i++)
1019  {
1020  VisualSlot slot = selInv.visualSlots[i];
1021  Item item = selInv.slots[i].FirstOrDefault();
1022  if (slot.InteractRect.Contains(PlayerInput.MousePosition) ||
1023  (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && item != null && item.AllowedSlots.Contains(InvSlotType.Any)))
1024  {
1025  return CursorState.Hand;
1026  }
1027  var container = item?.GetComponent<ItemContainer>();
1028  if (container == null) { continue; }
1029  if (container.Inventory.visualSlots != null)
1030  {
1031  if (container.Inventory.visualSlots.Any(slot => slot.IsHighlighted))
1032  {
1033  return CursorState.Hand;
1034  }
1035  }
1036  }
1037  }
1038 
1039  if (character.SelectedItem != null)
1040  {
1041  foreach (var ic in character.SelectedItem.ActiveHUDs)
1042  {
1043  var itemContainer = ic as ItemContainer;
1044  if (itemContainer?.Inventory?.visualSlots == null) { continue; }
1045  if (!ic.Item.IsInteractable(character)) { continue; }
1046 
1047  foreach (var slot in itemContainer.Inventory.visualSlots)
1048  {
1049  if (slot.InteractRect.Contains(PlayerInput.MousePosition) ||
1050  slot.EquipButtonRect.Contains(PlayerInput.MousePosition))
1051  {
1052  return CursorState.Hand;
1053  }
1054  }
1055  }
1056  }
1057 
1058  for (int i = 0; i < inv.visualSlots.Length; i++)
1059  {
1060  VisualSlot slot = inv.visualSlots[i];
1061  Item item = inv.slots[i].FirstOrDefault();
1062  if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && item != null && item.AllowedSlots.Contains(InvSlotType.Any))
1063  {
1064  return CursorState.Hand;
1065  }
1066 
1067  // This is the only place we double check this because if we have a inventory container
1068  // highlighting any area within that container registers as highlighting the
1069  // original slot the item is in thus giving us a false hand cursor.
1070  if (slot.InteractRect.Contains(PlayerInput.MousePosition))
1071  {
1072  if (slot.IsHighlighted)
1073  {
1074  return CursorState.Hand;
1075  }
1076  }
1077  }
1078  return CursorState.Default;
1079  }
1080 
1081  protected static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle highlightedSlot)
1082  {
1083  GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, Anchor.BottomRight);
1084  }
1085 
1086  public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex)
1087  {
1088  var item = slots[slotIndex].FirstOrDefault();
1089  if (item == null) { return; }
1090 
1091  var container = item.GetComponent<ItemContainer>();
1092  if (container == null || !container.DrawInventory) { return; }
1093 
1094  if (container.Inventory.visualSlots == null || !container.Inventory.isSubInventory) { return; }
1095  if (container.Inventory.DrawWhenEquipped) { return; }
1096 
1097  int itemCapacity = container.Capacity;
1098 
1099 #if DEBUG
1100  System.Diagnostics.Debug.Assert(slotIndex >= 0 && slotIndex < slots.Length);
1101 #else
1102  if (slotIndex < 0 || slotIndex >= capacity) { return; }
1103 #endif
1104 
1105  if (!canMove)
1106  {
1107  Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
1108  spriteBatch.End();
1109  spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable);
1110  if (visualSlots[slotIndex].SubInventoryDir > 0)
1111  {
1112  spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(
1113  new Point(0, visualSlots[slotIndex].Rect.Bottom),
1114  new Point(GameMain.GraphicsWidth, (int)Math.Max(GameMain.GraphicsHeight - visualSlots[slotIndex].Rect.Bottom, 0)));
1115  }
1116  else
1117  {
1118  spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(
1119  new Point(0, 0),
1120  new Point(GameMain.GraphicsWidth, visualSlots[slotIndex].Rect.Y));
1121  }
1122  container.Inventory.Draw(spriteBatch, true);
1123  spriteBatch.End();
1124  spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
1125  spriteBatch.Begin(SpriteSortMode.Deferred);
1126  }
1127  else
1128  {
1129  container.Inventory.Draw(spriteBatch, true);
1130  }
1131 
1132  container.InventoryBottomSprite?.Draw(spriteBatch,
1133  new Vector2(visualSlots[slotIndex].Rect.Center.X, visualSlots[slotIndex].Rect.Y) + visualSlots[slotIndex].DrawOffset,
1134  0.0f, UIScale);
1135 
1136  container.InventoryTopSprite?.Draw(spriteBatch,
1137  new Vector2(
1138  visualSlots[slotIndex].Rect.Center.X,
1139  container.Inventory.visualSlots[container.Inventory.visualSlots.Length - 1].Rect.Y) + container.Inventory.visualSlots[container.Inventory.visualSlots.Length - 1].DrawOffset,
1140  0.0f, UIScale);
1141 
1142  if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container))
1143  {
1144  if (container.Inventory.positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working
1145  {
1146  int height = (int)(movableFrameRectHeight * UIScale);
1147  CreateSlots();
1148  container.Inventory.movableFrameRect = new Rectangle(container.Inventory.BackgroundFrame.X, container.Inventory.BackgroundFrame.Y - height, container.Inventory.BackgroundFrame.Width, height);
1149  draggableIndicatorScale = 1.25f * UIScale;
1150  draggableIndicatorOffset = DraggableIndicator.size * draggableIndicatorScale / 2f;
1151  draggableIndicatorOffset += new Vector2(height / 2f - draggableIndicatorOffset.Y);
1152  container.Inventory.originalPos = container.Inventory.savedPosition = container.Inventory.movableFrameRect.Center;
1153  container.Inventory.positionUpdateQueued = false;
1154  }
1155 
1156  if (container.Inventory.movableFrameRect.Size == Point.Zero || GUI.HasSizeChanged(prevScreenResolution, prevUIScale, prevHUDScale))
1157  {
1158  // Reset position
1159  container.Inventory.savedPosition = container.Inventory.originalPos;
1160 
1162  prevUIScale = UIScale;
1163  prevHUDScale = GUI.Scale;
1164  container.Inventory.positionUpdateQueued = true;
1165  }
1166  else
1167  {
1168  Color color = movableFrameRectColor;
1169  if (DraggingInventory != null && DraggingInventory != container.Inventory)
1170  {
1171  color *= 0.7f;
1172  }
1173  else if (container.Inventory.movableFrameRect.Contains(PlayerInput.MousePosition))
1174  {
1175  color = Color.Lerp(color, PlayerInput.PrimaryMouseButtonHeld() ? Color.Black : Color.White, 0.25f);
1176  }
1177  GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, color, true);
1178  DraggableIndicator.Draw(spriteBatch, container.Inventory.movableFrameRect.Location.ToVector2() + draggableIndicatorOffset, 0, draggableIndicatorScale);
1179  }
1180  }
1181  }
1182 
1183  public static void UpdateDragging()
1184  {
1186  {
1187  DraggingItems.RemoveAll(it => !Character.Controlled.CanInteractWith(it));
1188  }
1189 
1191  {
1193 
1194  bool mouseOnPortrait = CharacterHUD.MouseOnCharacterPortrait();
1195  if (!DetermineMouseOnInventory(ignoreDraggedItem: true) &&
1196  (CharacterHealth.OpenHealthWindow != null || mouseOnPortrait))
1197  {
1198  if (TryPortraitAndHealthDrop(mouseOnPortrait))
1199  {
1200  return;
1201  }
1202  }
1203 
1204  if (selectedSlot == null)
1205  {
1206  HandleOutsideInventoryDrop();
1207  }
1209  {
1210  HandleInventorySlotDrop();
1211  }
1212 
1213  DraggingItems.Clear();
1214  }
1215 
1216  if (selectedSlot != null && !CanSelectSlot(selectedSlot))
1217  {
1218  selectedSlot = null;
1219  }
1220 
1221  bool TryPortraitAndHealthDrop(bool mouseOnPortrait)
1222  {
1223  bool dropSuccessful = false;
1224  foreach (Item item in DraggingItems)
1225  {
1226  var inventory = item.ParentInventory;
1227  var indices = inventory?.FindIndices(item);
1228  dropSuccessful |= (CharacterHealth.OpenHealthWindow ?? Character.Controlled.CharacterHealth).OnItemDropped(item, ignoreMousePos: mouseOnPortrait);
1229  if (dropSuccessful)
1230  {
1231  if (indices != null && inventory.visualSlots != null)
1232  {
1233  foreach (int i in indices)
1234  {
1235  inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f);
1236  }
1237  }
1238  break;
1239  }
1240  }
1241  if (dropSuccessful)
1242  {
1243  DraggingItems.Clear();
1244  return true;
1245  }
1246 
1247  return false;
1248  }
1249 
1250  void HandleOutsideInventoryDrop()
1251  {
1252  bool isTargetingValidContainer = Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item &&
1253  item.GetComponent<ItemContainer>() is { } container &&
1254  container.HasRequiredItems(Character.Controlled, addMessage: false) &&
1255  container.AllowDragAndDrop &&
1256  inventory.CanBePut(DraggingItems.FirstOrDefault());
1257 
1258  bool isTargetingValidCharacter = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter);
1259 
1260  if (DraggingItemToWorld && (isTargetingValidContainer || isTargetingValidCharacter))
1261  {
1262  bool anySuccess = false;
1263  foreach (Item it in DraggingItems)
1264  {
1265  bool success = false;
1266  if (isTargetingValidContainer)
1267  {
1269  }
1270  if (!success && isTargetingValidCharacter)
1271  {
1273  }
1274 
1275  if (!success) { break; }
1276  anySuccess = true;
1277  }
1278 
1279  if (anySuccess) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
1280  }
1281  else
1282  {
1284  {
1285  if (DraggingItems.First()?.ParentInventory != null)
1286  {
1287  SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List<Item>(DraggingItems), true));
1288  }
1289  }
1290 
1291  SoundPlayer.PlayUISound(GUISoundType.DropItem);
1292  bool removed = false;
1293  if (Screen.Selected is SubEditorScreen editor)
1294  {
1295  if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition))
1296  {
1297  DraggingItems.ForEachMod(it => it.Remove());
1298  removed = true;
1299  }
1300  else
1301  {
1302  if (editor.WiringMode)
1303  {
1304  DraggingItems.ForEachMod(it => it.Remove());
1305  removed = true;
1306  }
1307  else
1308  {
1309  DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
1310  }
1311  }
1312  }
1313  else
1314  {
1315  DraggingItems.ForEachMod(it => it.Drop(Character.Controlled));
1316  DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false);
1317  }
1318  SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem);
1319  }
1320  }
1321 
1322  void HandleInventorySlotDrop()
1323  {
1324  Inventory oldInventory = DraggingItems.First().ParentInventory;
1325  Inventory selectedInventory = selectedSlot.ParentInventory;
1326  int slotIndex = selectedSlot.SlotIndex;
1327  int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.slots, DraggingItems);
1328 
1329  //if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot
1330  if (selectedInventory.slots[slotIndex].Empty() &&
1331  selectedInventory == Character.Controlled.Inventory &&
1332  !DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) &&
1333  DraggingItems.Any(it => selectedInventory.TryPutItem(it, Character.Controlled, it.AllowedSlots)))
1334  {
1335  if (selectedInventory.visualSlots != null)
1336  {
1337  for (int i = 0; i < selectedInventory.visualSlots.Length; i++)
1338  {
1339  if (DraggingItems.Any(it => selectedInventory.slots[i].Contains(it)))
1340  {
1341  selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
1342  }
1343  }
1344  selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f);
1345  }
1346  SoundPlayer.PlayUISound(GUISoundType.PickItem);
1347  }
1348  else
1349  {
1350  bool anySuccess = false;
1351  //if we're dragging a stack of partial items or trying to drag to a stack of partial items
1352  //(which should not normally exist, but can happen when e.g. fire damages a stack of items)
1353  //don't allow combining because it leads to weird behavior (stack of items of mixed quality)
1354  bool allowCombine = !(DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 ||
1355  selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1);
1356  int itemCount = 0;
1357  foreach (Item item in DraggingItems)
1358  {
1359  if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container &&
1360  container.Inventory.CanBePut(item))
1361  {
1362  if (!container.AllowDragAndDrop || !container.AllowAccess)
1363  {
1364  allowCombine = false;
1365  }
1366  }
1367  bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled);
1368  if (success)
1369  {
1370  anySuccess = true;
1371  itemCount++;
1372  }
1373  if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory))
1374  {
1375  break;
1376  }
1377  }
1378 
1379  if (anySuccess)
1380  {
1381  highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == oldInventory || s.ParentInventory == selectedInventory);
1383  {
1384  foreach (Item draggingItem in DraggingItems)
1385  {
1386  if (selectedInventory.slots[slotIndex].Contains(draggingItem))
1387  {
1388  SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex));
1389  }
1390  }
1391  }
1392  if (selectedInventory.visualSlots != null) { selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); }
1393  SoundPlayer.PlayUISound(GUISoundType.PickItem);
1394  }
1395  else
1396  {
1397  if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); }
1398  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
1399  }
1400  }
1401 
1402  selectedInventory.HideTimer = 2.0f;
1403  if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null)
1404  {
1405  for (int i = 0; i < parentItem.ParentInventory.capacity; i++)
1406  {
1407  if (parentItem.ParentInventory.HideSlot(i)) { continue; }
1408  if (parentItem.ParentInventory.slots[i].FirstOrDefault() != parentItem) { continue; }
1409 
1411  parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i],
1412  i, false, selectedSlot.ParentInventory));
1413  break;
1414  }
1415  }
1416  DraggingItems.Clear();
1417  DraggingSlot = null;
1418  }
1419  }
1420 
1421  private static bool IsValidTargetForDragDropGive(Character giver, Character receiver)
1422  {
1423  if (giver == null || receiver == null) { return false; }
1424  if (receiver == giver) { return false; }
1425  return receiver.IsInventoryAccessibleTo(giver, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited);
1426  }
1427 
1428  private static bool CanSelectSlot(SlotReference selectedSlot)
1429  {
1430  if (!IsMouseOnInventory)
1431  {
1432  return false;
1433  }
1434  if (!selectedSlot.Slot.MouseOn())
1435  {
1436  return false;
1437  }
1438  else
1439  {
1440  static bool OwnerInaccessible(Entity owner) =>
1441  owner != Character.Controlled &&
1442  owner != Character.Controlled.SelectedCharacter &&
1443  owner != Character.Controlled.SelectedItem &&
1444  (Character.Controlled.SelectedItem == null || !Character.Controlled.SelectedItem.linkedTo.Contains(owner));
1445 
1446  Entity owner = selectedSlot.ParentInventory?.Owner;
1447  Entity rootOwner = (owner as Item)?.GetRootInventoryOwner();
1448  if (OwnerInaccessible(owner) && (rootOwner == owner || OwnerInaccessible(rootOwner)))
1449  {
1450  return false;
1451  }
1452  Item parentItem = (owner as Item) ?? selectedSlot?.Item;
1453  if (parentItem?.GetRootInventoryOwner() is Character ownerCharacter)
1454  {
1455  if (ownerCharacter == Character.Controlled &&
1456  CharacterHealth.OpenHealthWindow?.Character != ownerCharacter &&
1457  ownerCharacter.Inventory.IsInLimbSlot(parentItem, InvSlotType.HealthInterface) &&
1458  Screen.Selected != GameMain.SubEditorScreen)
1459  {
1460  highlightedSubInventorySlots.RemoveWhere(s => s.Item == parentItem);
1461  return false;
1462  }
1463  }
1464  }
1465  return true;
1466  }
1467 
1468 
1470  {
1471  if (Character.Controlled == null)
1472  {
1473  return Rectangle.Empty;
1474  }
1475 
1476  Rectangle hoverArea;
1477  bool isMovable = subSlot.Inventory.Movable() && !subSlot.ParentInventory.IsInventoryHoverAvailable(Character.Controlled, subSlot.Item?.GetComponent<ItemContainer>());
1478  bool unEquipped = Character.Controlled.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item);
1479  bool isDefaultLayout = subSlot.ParentInventory is not CharacterInventory characterInventory || characterInventory.CurrentLayout == CharacterInventory.Layout.Default;
1480  bool subEditorCharacterInventoryHidden = Screen.Selected == GameMain.SubEditorScreen && !GameMain.SubEditorScreen.DrawCharacterInventory;
1481  if (subEditorCharacterInventoryHidden || (isMovable && !unEquipped && isDefaultLayout))
1482  {
1483  hoverArea = subSlot.Inventory.BackgroundFrame;
1484  hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint();
1485  if (subSlot.Inventory.movableFrameRect != Rectangle.Empty)
1486  {
1487  hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect);
1488  }
1489  }
1490  else
1491  {
1492  //slot not visible as a separate, movable panel -> just use the area of the slot directly
1493  hoverArea = subSlot.Slot.Rect;
1494  hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint();
1495  hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect);
1496  }
1497 
1498  if (subSlot.Inventory?.visualSlots != null)
1499  {
1500  foreach (VisualSlot slot in subSlot.Inventory.visualSlots)
1501  {
1502  Rectangle subSlotRect = slot.InteractRect;
1503  subSlotRect.Location += slot.DrawOffset.ToPoint();
1504  hoverArea = Rectangle.Union(hoverArea, subSlotRect);
1505  }
1506  if (subSlot.Slot.SubInventoryDir < 0)
1507  {
1508  // 24/2/2020 - the below statement makes the sub inventory extend all the way to the bottom of the screen because of a double negative
1509  // Not sure if it's intentional or not but it was causing hover issues and disabling it seems to have no detrimental effects.
1510  // hoverArea.Height -= hoverArea.Bottom - subSlot.Slot.Rect.Bottom;
1511  }
1512  else
1513  {
1514  int over = subSlot.Slot.Rect.Y - hoverArea.Y;
1515  hoverArea.Y += over;
1516  hoverArea.Height -= over;
1517  }
1518  }
1519 
1520  float inflateAmount = 10 * UIScale;
1521 
1522  hoverArea.Inflate(inflateAmount, inflateAmount);
1523  return hoverArea;
1524  }
1525 
1526  public static void DrawFront(SpriteBatch spriteBatch)
1527  {
1528  if (GUI.PauseMenuOpen || GUI.SettingsMenuOpen) { return; }
1529  if (GameMain.GameSession?.Campaign != null &&
1531 
1532  subInventorySlotsToDraw.Clear();
1533  subInventorySlotsToDraw.AddRange(highlightedSubInventorySlots);
1534  foreach (var slot in subInventorySlotsToDraw)
1535  {
1536  int slotIndex = Array.IndexOf(slot.ParentInventory.visualSlots, slot.Slot);
1537  if (slotIndex > -1 && slotIndex < slot.ParentInventory.visualSlots.Length &&
1538  (slot.Item?.GetComponent<ItemContainer>()?.HasRequiredItems(Character.Controlled, addMessage: false) ?? true))
1539  {
1540  slot.ParentInventory.DrawSubInventory(spriteBatch, slotIndex);
1541  }
1542  }
1543 
1544  if (DraggingItems.Any())
1545  {
1546  DrawDragRelated();
1547  }
1548 
1549  if (selectedSlot != null && selectedSlot.Item != null)
1550  {
1551  Rectangle slotRect = selectedSlot.Slot.Rect;
1552  slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint();
1554  {
1556  }
1557 
1558  if (!slotIconTooltip.IsNullOrEmpty())
1559  {
1560  DrawToolTip(spriteBatch, slotIconTooltip, slotRect);
1561  }
1562  else
1563  {
1564  DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect);
1565  }
1566  slotIconTooltip = string.Empty;
1567  }
1568 
1569  void DrawDragRelated()
1570  {
1571  if (DraggingSlot == null || (!DraggingSlot.MouseOn()))
1572  {
1573  Sprite sprite = DraggingItems.First().Prefab.InventoryIcon ?? DraggingItems.First().Sprite;
1574 
1575  int iconSize = (int)(64 * GUI.Scale);
1576  float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f);
1577  Vector2 itemPos = PlayerInput.MousePosition;
1578 
1579  bool mouseOnHealthInterface =
1582  mouseOnHealthInterface = mouseOnHealthInterface && DraggingItems.Any(it => it.UseInHealthInterface);
1583 
1584  if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null)
1585  {
1586  var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0];
1587 
1588  (LocalizedString toolTip, Color toolTipColor) = GetDragLabelTextAndColor(mouseOnHealthInterface);
1589 
1590  Vector2 nameSize = GUIStyle.Font.MeasureString(DraggingItems.First().Name);
1591  Vector2 toolTipSize = GUIStyle.SmallFont.MeasureString(toolTip);
1592  int textWidth = (int)Math.Max(nameSize.X, toolTipSize.X);
1593  int textSpacing = (int)(15 * GUI.Scale);
1594 
1595  Vector2 textPos = itemPos;
1596  int textDir = textPos.X + textWidth * 1.5f > GameMain.GraphicsWidth ? -1 : 1;
1597  int textOffset = textDir == 1 ? 0 : -1;
1598  textPos += new Vector2((iconSize / 2 + textSpacing) * textDir, 0);
1599 
1600  Point shadowPadding = new Point(40, 20).Multiply(GUI.Scale);
1601  Point shadowSize = new Point(iconSize + textWidth + textSpacing, iconSize) + shadowPadding.Multiply(2);
1602 
1603  shadowSprite.Draw(spriteBatch,
1604  new Rectangle(itemPos.ToPoint() - new Point((iconSize / 2 - shadowPadding.X) * textDir - shadowSize.X * textOffset, iconSize / 2 + shadowPadding.Y), shadowSize), Color.Black * 0.8f);
1605 
1606  GUI.DrawString(spriteBatch, textPos + new Vector2(nameSize.X * textOffset, -iconSize / 2), DraggingItems.First().Name, Color.White);
1607  GUI.DrawString(spriteBatch, textPos + new Vector2(toolTipSize.X * textOffset, 0), toolTip,
1608  color: toolTipColor,
1609  font: GUIStyle.SmallFont);
1610  }
1611 
1612  Item draggedItem = DraggingItems.First();
1613 
1614  sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black, scale: scale);
1615  sprite.Draw(spriteBatch,
1616  itemPos,
1617  sprite == draggedItem.Sprite ? draggedItem.GetSpriteColor() : draggedItem.GetInventoryIconColor(),
1618  scale: scale);
1619 
1620  if (draggedItem.Prefab.GetMaxStackSize(null) > 1)
1621  {
1622  int stackAmount = DraggingItems.Count;
1623  if (selectedSlot?.ParentInventory != null)
1624  {
1625  if (selectedSlot.Item?.OwnInventory != null)
1626  {
1627  int maxAmountPerSlot = 0;
1628  for (int i = 0; i < SelectedSlot.Item.OwnInventory.Capacity; i++)
1629  {
1630  maxAmountPerSlot = Math.Max(
1631  maxAmountPerSlot,
1632  selectedSlot.Item.OwnInventory.HowManyCanBePut(draggedItem.Prefab, i, draggedItem.Condition, ignoreItemsInSlot: true));
1633  }
1634  stackAmount = Math.Min(stackAmount, maxAmountPerSlot);
1635  }
1636  else
1637  {
1638  stackAmount = Math.Min(
1639  stackAmount,
1640  selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition, ignoreItemsInSlot: true));
1641  }
1642  }
1643  Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f;
1644  string stackCountText = "x" + stackAmount;
1645  GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black);
1646  GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright);
1647  }
1648  }
1649  }
1650 
1651  (LocalizedString, Color) GetDragLabelTextAndColor(bool mouseOnHealthInterface)
1652  {
1653  bool useDragDropGive = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter);
1654 
1655  Color toolTipColor = Color.LightGreen;
1656 
1657  LocalizedString toolTip;
1658  if (mouseOnHealthInterface)
1659  {
1660  toolTip = TextManager.Get("QuickUseAction.UseTreatment");
1661  }
1662  else if (Character.Controlled.FocusedItem != null)
1663  {
1664  toolTip = TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes);
1665  }
1666  else if (useDragDropGive)
1667  {
1668  toolTip = TextManager.GetWithVariable("GiveItemTo", "[character]", Character.Controlled.FocusedCharacter.Name, FormatCapitals.Yes);
1669  }
1670  else
1671  {
1672  toolTipColor = GUIStyle.Red;
1673  toolTip = TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem");
1674  }
1675  return (toolTip, toolTipColor);
1676  }
1677  }
1678 
1679  private static (Item target, Identifier orderIdentifier) availableContextualOrder;
1680  private static LocalizedString slotIconTooltip;
1681 
1682  public static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, VisualSlot slot, Item item, int slotIndex, bool drawItem = true, InvSlotType type = InvSlotType.Any)
1683  {
1684  Rectangle rect = slot.Rect;
1685  rect.Location += slot.DrawOffset.ToPoint();
1686 
1687  if (slot.HighlightColor.A > 0)
1688  {
1689  float inflateAmount = (slot.HighlightColor.A / 255.0f) * slot.HighlightScaleUpAmount * 0.5f;
1690  rect.Inflate(rect.Width * inflateAmount, rect.Height * inflateAmount);
1691  }
1692 
1693  Color slotColor = Color.White;
1694  Item parentItem = inventory?.Owner as Item;
1695  if (parentItem != null && !parentItem.IsPlayerTeamInteractable) { slotColor = Color.Gray; }
1696  var itemContainer = item?.GetComponent<ItemContainer>();
1697  if (itemContainer != null && (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null))
1698  {
1699  if (!highlightedSubInventorySlots.Any(s => s.Slot == slot))
1700  {
1701  itemContainer.InventoryBottomSprite?.Draw(spriteBatch, new Vector2(rect.Center.X, rect.Y), 0, UIScale);
1702  itemContainer.InventoryTopSprite?.Draw(spriteBatch, new Vector2(rect.Center.X, rect.Y), 0, UIScale);
1703  }
1704 
1705  drawItem = false;
1706  }
1707  else
1708  {
1709  Sprite slotSprite = slot.SlotSprite ?? SlotSpriteSmall;
1710 
1711  if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; }
1712  spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor);
1713 
1715  {
1716  GUI.DrawRectangle(spriteBatch, rect, GUIStyle.Red * 0.3f, isFilled: true);
1717  }
1718 
1719  bool canBePut = false;
1720 
1721  if (DraggingItems.Any() && inventory != null && slotIndex > -1 && slotIndex < inventory.visualSlots.Length)
1722  {
1723  var itemInSlot = inventory.slots[slotIndex].FirstOrDefault();
1724  if (inventory.CanBePutInSlot(DraggingItems.First(), slotIndex))
1725  {
1726  canBePut = true;
1727  }
1728  else if
1729  (itemInSlot?.OwnInventory != null &&
1730  itemInSlot.OwnInventory.CanBePut(DraggingItems.First()) &&
1731  itemInSlot.OwnInventory.Container.AllowDragAndDrop &&
1732  itemInSlot.OwnInventory.Container.DrawInventory)
1733  {
1734  canBePut = true;
1735  }
1736  else if (inventory.slots[slotIndex] == null && inventory == Character.Controlled.Inventory &&
1737  !DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) &&
1739  {
1740  canBePut = true;
1741  }
1742  }
1743  if (slot.MouseOn() && canBePut && selectedSlot?.Slot == slot)
1744  {
1745  GUIStyle.UIGlow.Draw(spriteBatch, rect, GUIStyle.Green);
1746  }
1747 
1748  if (item != null && drawItem)
1749  {
1750  if (!item.IsFullCondition && !item.Prefab.HideConditionBar && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator))
1751  {
1752  int dir = slot.SubInventoryDir;
1753  Rectangle conditionIndicatorArea;
1754  if (itemContainer != null && itemContainer.ShowContainedStateIndicator)
1755  {
1756  conditionIndicatorArea = new Rectangle(rect.X, rect.Bottom - (int)(10 * GUI.Scale), rect.Width, (int)(10 * GUI.Scale));
1757  }
1758  else
1759  {
1760  conditionIndicatorArea = new Rectangle(
1761  rect.X, dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight,
1762  rect.Width, ContainedIndicatorHeight);
1763  conditionIndicatorArea.Inflate(-4, 0);
1764  }
1765 
1766  var indicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator.Default");
1767  Sprite indicatorSprite = indicatorStyle?.GetDefaultSprite();
1768  Sprite emptyIndicatorSprite = indicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover);
1769  DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, conditionIndicatorArea, item.Condition / item.MaxCondition);
1770  }
1771 
1772  if (itemContainer != null && itemContainer.ShowContainedStateIndicator && itemContainer.Capacity > 0)
1773  {
1774  float containedState = itemContainer.GetContainedIndicatorState();
1775  int dir = slot.SubInventoryDir;
1776  Rectangle containedIndicatorArea = new Rectangle(rect.X,
1777  dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight);
1778  containedIndicatorArea.Inflate(-4, 0);
1779 
1780  Sprite indicatorSprite =
1781  itemContainer.ContainedStateIndicator ??
1782  itemContainer.IndicatorStyle?.GetDefaultSprite();
1783  Sprite emptyIndicatorSprite =
1784  itemContainer.ContainedStateIndicatorEmpty ??
1785  itemContainer.IndicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover);
1786 
1787  bool usingDefaultSprite = itemContainer.IndicatorStyle?.Name == "ContainedStateIndicator.Default";
1788 
1789  DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, containedIndicatorArea, containedState,
1790  pulsate: !usingDefaultSprite && containedState >= 0.0f && containedState < 0.25f && inventory == Character.Controlled?.Inventory && Character.Controlled.HasEquippedItem(item));
1791  }
1792 
1793  if (item.Quality != 0)
1794  {
1795  var style = GUIStyle.GetComponentStyle("InnerGlowSmall");
1796  if (style == null)
1797  {
1798  GUI.DrawRectangle(spriteBatch, rect, GUIStyle.GetQualityColor(item.Quality) * 0.7f);
1799  }
1800  else
1801  {
1802  style.Sprites[GUIComponent.ComponentState.None].FirstOrDefault()?.Draw(spriteBatch, rect, GUIStyle.GetQualityColor(item.Quality) * 0.5f);
1803  }
1804  }
1805  }
1806  else
1807  {
1808  var slotIcon = parentItem?.GetComponent<ItemContainer>()?.GetSlotIcon(slotIndex);
1809  if (slotIcon != null)
1810  {
1811  slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUIStyle.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f);
1812  }
1813  }
1814  }
1815 
1816  if (GameMain.DebugDraw)
1817  {
1818  GUI.DrawRectangle(spriteBatch, rect, Color.White, false, 0, 1);
1819  GUI.DrawRectangle(spriteBatch, slot.EquipButtonRect, Color.White, false, 0, 1);
1820  }
1821 
1822  if (slot.HighlightColor != Color.Transparent)
1823  {
1824  GUIStyle.UIGlow.Draw(spriteBatch, rect, slot.HighlightColor);
1825  }
1826 
1827  if (item != null && drawItem)
1828  {
1829  Sprite sprite = item.Prefab.InventoryIcon ?? item.Sprite;
1830  float scale = Math.Min(Math.Min((rect.Width - 10) / sprite.size.X, (rect.Height - 10) / sprite.size.Y), 2.0f);
1831  Vector2 itemPos = rect.Center.ToVector2();
1832  if (itemPos.Y > GameMain.GraphicsHeight)
1833  {
1834  itemPos.Y -= Math.Min(
1835  (itemPos.Y + sprite.size.Y / 2 * scale) - GameMain.GraphicsHeight,
1836  (itemPos.Y - sprite.size.Y / 2 * scale) - rect.Y);
1837  }
1838 
1839  float rotation = 0.0f;
1840  if (slot.HighlightColor.A > 0)
1841  {
1842  rotation = (float)Math.Sin(slot.HighlightTimer * MathHelper.TwoPi) * slot.HighlightTimer * 0.3f;
1843  }
1844 
1845  Color spriteColor = sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor();
1846  if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => !it.IsInteractable(Character.Controlled)))) { spriteColor *= 0.5f; }
1847  if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface && !item.AllowedSlots.Contains(InvSlotType.HealthInterface) && item.GetComponent<GeneticMaterial>() == null)
1848  {
1849  spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f);
1850  }
1851  else
1852  {
1853  sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black * 0.6f, rotate: rotation, scale: scale);
1854  }
1855  sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale);
1856 
1857  if (item.OrderedToBeIgnored)
1858  {
1859  if (OrderPrefab.Prefabs.TryGet(Tags.IgnoreThis, out OrderPrefab ignoreOrder))
1860  {
1861  DrawSideIcon(ignoreOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.ignored"), ignoreOrder.Color, out bool mouseOn);
1862  if (mouseOn) { availableContextualOrder = (item, Tags.UnignoreThis); }
1863 
1864  }
1865  }
1866  else if (Item.DeconstructItems.Contains(item) &&
1867  OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder))
1868  {
1869  DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
1870  if (mouseOn) { availableContextualOrder = (item, Tags.DontDeconstructThis); }
1871  }
1872  else if ((item.Illegitimate || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.Illegitimate))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand))
1873  {
1874  DrawSideIcon(CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand], Direction.Left, TextManager.Get("tooltip.stolenitem"), GUIStyle.Red, out _);
1875  }
1876  int maxStackSize = item.Prefab.GetMaxStackSize(inventory);
1877  if (inventory is ItemInventory itemInventory)
1878  {
1879  maxStackSize = Math.Min(maxStackSize, itemInventory.Container.GetMaxStackSize(slotIndex));
1880  }
1881  if (maxStackSize > 1 && inventory != null)
1882  {
1883  int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].Items.Count : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count();
1884  if (item.IsFullCondition || MathUtils.NearlyEqual(item.Condition, 0.0f) || itemCount > 1)
1885  {
1886  Vector2 stackCountPos = new Vector2(rect.Right, rect.Bottom);
1887  string stackCountText = "x" + itemCount;
1888  stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2);
1889  GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black);
1890  GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White);
1891  }
1892  }
1893 
1894  if (HealingCooldown.IsOnCooldown && item.HasTag(Tags.MedicalItem))
1895  {
1896  RectangleF cdRect = rect;
1897  // shrink the rect from top to bottom depending on HealingCooldown.NormalizedCooldown
1898  cdRect.Height *= HealingCooldown.NormalizedCooldown;
1899  cdRect.Y += rect.Height;
1900  GUI.DrawFilledRectangle(spriteBatch, cdRect, Color.White * 0.5f);
1901  }
1902  }
1903 
1904  if (inventory != null &&
1905  !inventory.Locked &&
1906  Character.Controlled?.Inventory == inventory &&
1907  slot.InventoryKeyIndex != -1 &&
1908  slot.InventoryKeyIndex < GameSettings.CurrentConfig.InventoryKeyMap.Bindings.Length)
1909  {
1910  spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor);
1911 
1912  GUIStyle.HotkeyFont.DrawString(
1913  spriteBatch,
1914  GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name,
1915  rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)),
1916  Color.Black,
1917  rotation: 0.0f,
1918  origin: Vector2.Zero,
1919  scale: Vector2.One * GUI.AspectRatioAdjustment,
1920  SpriteEffects.None,
1921  layerDepth: 0.0f);
1922  }
1923 
1924  void DrawSideIcon(Sprite icon, Direction side, LocalizedString tooltip, Color color, out bool mouseOn)
1925  {
1926  Vector2 iconSize = new Vector2(25 * GUI.Scale);
1927  float margin = 0.2f;
1928  Vector2 pos = new Vector2(
1929  side == Direction.Left ? rect.X + iconSize.X * margin : rect.Right - iconSize.X * margin,
1930  rect.Bottom - iconSize.Y * 1.2f);
1931  mouseOn = Vector2.Distance(PlayerInput.MousePosition, pos) < iconSize.X / 2;
1932  if (mouseOn)
1933  {
1934  slotIconTooltip = tooltip;
1935  color = Color.Lerp(color, Color.White, 0.5f);
1936  }
1937  icon.Draw(spriteBatch, pos, color: color, scale: iconSize.X / icon.size.X);
1938  }
1939  }
1940 
1941 
1942  private static void DrawItemStateIndicator(
1943  SpriteBatch spriteBatch, Inventory inventory,
1944  Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState,
1945  bool pulsate = false)
1946  {
1947  Color backgroundColor = GUIStyle.ColorInventoryBackground;
1948 
1949  if (indicatorSprite == null)
1950  {
1951  containedIndicatorArea.Inflate(0, -2);
1952  GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true);
1953  GUI.DrawRectangle(spriteBatch,
1954  new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height),
1955  ToolBox.GradientLerp(containedState, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull) * 0.8f, true);
1956  GUI.DrawLine(spriteBatch,
1957  new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y),
1958  new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom),
1959  Color.Black * 0.8f);
1960  }
1961  else
1962  {
1963  float indicatorScale = Math.Min(
1964  containedIndicatorArea.Width / (float)indicatorSprite.SourceRect.Width,
1965  containedIndicatorArea.Height / (float)indicatorSprite.SourceRect.Height);
1966 
1967  if (pulsate)
1968  {
1969  indicatorScale += ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) * 0.2f;
1970  }
1971 
1972  indicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(),
1973  (inventory != null && inventory.Locked) ? backgroundColor * 0.5f : backgroundColor,
1974  origin: indicatorSprite.size / 2,
1975  rotate: 0.0f,
1976  scale: indicatorScale);
1977 
1978  if (containedState > 0.0f)
1979  {
1980  Color indicatorColor = ToolBox.GradientLerp(containedState, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull);
1981  if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; }
1982 
1983  spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(),
1984  sourceRectangle: new Rectangle(indicatorSprite.SourceRect.Location, new Point((int)(indicatorSprite.SourceRect.Width * containedState), indicatorSprite.SourceRect.Height)),
1985  color: indicatorColor,
1986  rotation: 0.0f,
1987  origin: indicatorSprite.size / 2,
1988  scale: indicatorScale,
1989  effects: SpriteEffects.None, layerDepth: 0.0f);
1990 
1991  spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(),
1992  sourceRectangle: new Rectangle(indicatorSprite.SourceRect.X - 1 + (int)(indicatorSprite.SourceRect.Width * containedState), indicatorSprite.SourceRect.Y, Math.Max((int)Math.Ceiling(1 / indicatorScale), 2), indicatorSprite.SourceRect.Height),
1993  color: Color.Black,
1994  rotation: 0.0f,
1995  origin: new Vector2(indicatorSprite.size.X * (0.5f - containedState), indicatorSprite.size.Y * 0.5f),
1996  scale: indicatorScale,
1997  effects: SpriteEffects.None, layerDepth: 0.0f);
1998  }
1999  else if (emptyIndicatorSprite != null)
2000  {
2001  Color indicatorColor = GUIStyle.ColorInventoryEmptyOverlay;
2002  if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; }
2003 
2004  emptyIndicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(),
2005  indicatorColor,
2006  origin: emptyIndicatorSprite.size / 2,
2007  rotate: 0.0f,
2008  scale: indicatorScale);
2009  }
2010  }
2011  }
2012 
2014  {
2015  UInt16 lastEventID = msg.ReadUInt16();
2016  partialReceivedItemIDs ??= new List<ushort>[capacity];
2017  SharedRead(msg, partialReceivedItemIDs, out bool readyToApply);
2018  if (!readyToApply) { return; }
2019 
2020  receivedItemIDs = partialReceivedItemIDs.ToArray();
2021  partialReceivedItemIDs = null;
2022 
2023  //delay applying the new state if less than 1 second has passed since this client last sent a state to the server
2024  //prevents the inventory from briefly reverting to an old state if items are moved around in quick succession
2025 
2026  //also delay if we're still midround syncing, some of the items in the inventory may not exist yet
2027  if (syncItemsDelay > 0.0f || GameMain.Client.MidRoundSyncing || NetIdUtils.IdMoreRecent(lastEventID, GameMain.Client.EntityEventManager.LastReceivedID))
2028  {
2029  if (syncItemsCoroutine != null) CoroutineManager.StopCoroutines(syncItemsCoroutine);
2030  syncItemsCoroutine = CoroutineManager.StartCoroutine(SyncItemsAfterDelay(lastEventID));
2031  }
2032  else
2033  {
2034  if (syncItemsCoroutine != null)
2035  {
2036  CoroutineManager.StopCoroutines(syncItemsCoroutine);
2037  syncItemsCoroutine = null;
2038  }
2040  }
2041  }
2042 
2043  private IEnumerable<CoroutineStatus> SyncItemsAfterDelay(UInt16 lastEventID)
2044  {
2045  while (syncItemsDelay > 0.0f ||
2046  //don't apply inventory updates until
2047  // 1. MidRound syncing is done AND
2048  // 2. We've received all the events created before the update was written (otherwise we may not yet know about some items the server has spawned in the inventory)
2049  (GameMain.Client != null && (GameMain.Client.MidRoundSyncing || NetIdUtils.IdMoreRecent(lastEventID, GameMain.Client.EntityEventManager.LastReceivedID))))
2050  {
2051  if (GameMain.GameSession == null || Level.Loaded == null)
2052  {
2053  yield return CoroutineStatus.Success;
2054  }
2055  syncItemsDelay = Math.Max((float)(syncItemsDelay - Timing.Step), 0.0f);
2056  yield return CoroutineStatus.Running;
2057  }
2058 
2059  if (Owner.Removed || GameMain.Client == null)
2060  {
2061  yield return CoroutineStatus.Success;
2062  }
2063 
2065 
2066  yield return CoroutineStatus.Success;
2067  }
2068 
2069  public void ApplyReceivedState()
2070  {
2071  if (receivedItemIDs == null || (Owner != null && Owner.Removed)) { return; }
2072 
2073  for (int i = 0; i < capacity; i++)
2074  {
2075  foreach (Item item in slots[i].Items.ToList())
2076  {
2077  if (!receivedItemIDs[i].Contains(item.ID))
2078  {
2079  item.Drop(null);
2080  }
2081  }
2082  }
2083 
2084  //iterate backwards to get the item to the Any slots first
2085  for (int i = capacity - 1; i >= 0; i--)
2086  {
2087  if (!receivedItemIDs[i].Any()) { continue; }
2088  foreach (UInt16 id in receivedItemIDs[i])
2089  {
2090  if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; }
2091 
2092  if (Owner is Item thisItem && thisItem.Container == item)
2093  {
2094  //if this item is inside the item we're trying to contain inside it, we need to drop it (both items can't be inside each other!)
2095  //can happen when a player swaps the items to be "the other way around", and we receive a message about the contained item
2096  //before the message about the "parent item" being placed in some other inventory (like the player's inventory)
2097  thisItem.Drop(null);
2098  }
2099 
2100  if (!TryPutItem(item, i, false, false, null, false))
2101  {
2102  try
2103  {
2104  ForceToSlot(item, i);
2105  }
2106  catch (InvalidOperationException e)
2107  {
2108  DebugConsole.AddSafeError(e.Message + "\n" + e.StackTrace.CleanupStackTrace());
2109  }
2110  }
2111  for (int j = 0; j < capacity; j++)
2112  {
2113  if (slots[j].Contains(item) && !receivedItemIDs[j].Contains(item.ID))
2114  {
2115  slots[j].RemoveItem(item);
2116  }
2117  }
2118  }
2119  }
2120 
2121  receivedItemIDs = null;
2122  }
2123  }
2124 }
bool Freeze
Resets to false each time the MoveCamera method is called.
Definition: Camera.cs:253
float GetZoomAmountFromPrevious()
Definition: Camera.cs:428
bool HasSelectedAnyItem
Has the characters selected a primary or a secondary item?
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel=CharacterInventory.AccessLevel.Limited)
Is the inventory accessible to the character? Doesn't check if the character can actually interact wi...
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
static CoroutineStatus Running
static CoroutineStatus Success
void OpenCommandUI(Entity entityContext=null, bool forceContextual=false)
void SetCharacterOrder(Character character, Order order, bool isNewOrder=true)
Sets the character's current order (if it's close enough to receive messages from orderGiver) and dis...
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
virtual Rectangle Rect
void DrawToolTip(SpriteBatch spriteBatch)
Creates and draws a tooltip.
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static int GraphicsHeight
Definition: GameMain.cs:168
static GameScreen GameScreen
Definition: GameMain.cs:52
static bool DebugDraw
Definition: GameMain.cs:29
static GameClient Client
Definition: GameMain.cs:188
SlotReference(Inventory parentInventory, VisualSlot slot, int slotIndex, bool isSubSlot, Inventory subInventory=null)
bool IsInventoryHoverAvailable(Character owner, ItemContainer container)
readonly int capacity
Capacity, or the number of slots in the inventory.
virtual ItemInventory GetActiveEquippedSubInventory(int slotIndex)
static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle highlightedSlot)
Item FirstOrDefault()
Return the first item in the inventory, or null if the inventory is empty.
virtual bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
virtual void Update(float deltaTime, Camera cam, bool subInventory=false)
void SharedRead(IReadMessage msg, List< ushort >[] receivedItemIds, out bool readyToApply)
static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, VisualSlot slot, Item item, int slotIndex, bool drawItem=true, InvSlotType type=InvSlotType.Any)
bool DrawWhenEquipped
Normally false - we don't draw the UI because it's drawn when the player hovers the cursor over the i...
RectTransform RectTransform
If set, the inventory is automatically positioned inside the rect
bool Contains(Item item)
Is the item contained in this inventory. Does not recursively check items inside items.
Inventory(Entity owner, int capacity, int slotsPerRow=5)
void UpdateSubInventory(float deltaTime, int slotIndex, Camera cam)
List< int > FindIndices(Item item)
Find the indices of all the slots the item is contained in (two-hand items for example can be in mult...
virtual void Draw(SpriteBatch spriteBatch, bool subInventory=false)
static bool IsMouseOnSlot(VisualSlot slot)
Check if the mouse is hovering on top of the slot
static void RefreshMouseOnInventory()
Refresh the value of IsMouseOnInventory
void DeleteAllItems()
Deletes all items inside the inventory (and also recursively all items inside the items)
int HowManyCanBePut(ItemPrefab itemPrefab, float? condition=null)
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
static HashSet< SlotReference > highlightedSubInventorySlots
void ForceToSlot(Item item, int index)
Forces an item to a specific slot. Doesn't remove the item from existing slots/inventories or do any ...
static Rectangle GetSubInventoryHoverArea(SlotReference subSlot)
void UpdateSlot(VisualSlot slot, int slotIndex, Item item, bool isSubSlot)
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 DrawSubInventory(SpriteBatch spriteBatch, int slotIndex)
Item GetItemAt(int index)
Get the item stored in the specified inventory slot. If the slot contains a stack of items,...
static void DrawFront(SpriteBatch spriteBatch)
bool IsPlayerTeamInteractable
Checks both NonInteractable and NonPlayerTeamInteractable
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
IEnumerable< InvSlotType > AllowedSlots
IEnumerable< ItemComponent > ActiveHUDs
Color GetSpriteColor(Color? defaultColor=null, bool withHighlight=false)
bool Illegitimate
Item shouldn't be in the player's inventory. If the guards find it, they will consider it as a theft.
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
static HashSet< Item > DeconstructItems
Items that have been marked for deconstruction
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot=false)
ImmutableArray< SkillRequirementHint > SkillRequirementHints
LocalizedString GetSkillRequirementHints(Character character)
The base class for components holding the different functionalities of the item
virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description)
override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
readonly ClientEntityEventManager EntityEventManager
Definition: GameClient.cs:163
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
static bool KeyDown(InputType inputType)
ContentPackage? ContentPackage
Definition: Prefab.cs:37
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
static List< AddOrDeleteCommand > BulkItemBuffer
static void StoreCommand(Command command)
void MoveBorderHighlight(VisualSlot newSlot)
Moves the current border highlight animation (if one is running) to the new slot
void ShowBorderHighlight(Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount=0.5f)
GUISoundType
Definition: GUI.cs:21
CursorState
Definition: GUI.cs:40