Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Map/Map/Map.cs
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Graphics;
3 using System;
4 using System.Collections.Generic;
5 using System.Collections.Immutable;
6 using System.Linq;
7 using Microsoft.Xna.Framework.Input;
9 
10 namespace Barotrauma
11 {
12  partial class Map
13  {
14  class MapAnim
15  {
16  public Location StartLocation;
17  public Location EndLocation;
18  public string StartMessage;
19  public string EndMessage;
20 
24  public float? StartZoom;
28  public float? EndZoom;
29 
30  private float startDelay;
31  public float StartDelay
32  {
33  get { return startDelay; }
34  set
35  {
36  startDelay = value;
37  Timer = -startDelay;
38  }
39  }
40 
41  public Vector2? StartPos;
42 
43  public float Duration;
44  public float Timer;
45 
46  public bool Finished;
47  }
48 
49  private readonly Queue<MapAnim> mapAnimQueue = new Queue<MapAnim>();
50 
51  public Location HighlightedLocation { get; private set; }
52 
53  private static Sprite noiseOverlay;
54 
55  public Vector2 DrawOffset;
56  private Vector2 drawOffsetNoise;
57 
58  private Vector2 currLocationIndicatorPos;
59 
60  private float zoom = 3.0f;
61  private float targetZoom;
62 
63  private Rectangle borders;
64 
65  private Sprite[,] mapTiles;
66  private bool[,] tileDiscovered;
67 
68  private float connectionHighlightState;
69 
70  private (Rectangle targetArea, RichString tip)? tooltip;
71 
72  private SubmarineInfo.PendingSubInfo pendingSubInfo;
73 
74  private RichString beaconStationActiveText, beaconStationInactiveText;
75 
76  private GUIComponent locationInfoOverlay;
77 
78  /*private (Rectangle targetArea, string tip)? connectionTooltip;
79  private string sanitizedConnectionTooltip;
80  private List<RichTextData> connectionTooltipRichTextData;
81  private string prevConnectionTooltip;*/
82 
83 #if DEBUG
84  private GUIComponent editor;
85 
86  private void CreateEditor()
87  {
88  editor = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f), GUI.Canvas, Anchor.TopRight, minSize: new Point(400, 0)));
89  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), editor.RectTransform, Anchor.Center))
90  {
91  Stretch = true,
92  RelativeSpacing = 0.02f,
93  CanBeFocused = false
94  };
95 
96  var listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), paddedFrame.RectTransform, Anchor.Center));
97  new SerializableEntityEditor(listBox.Content.RectTransform, generationParams, false, true);
98 
99  new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform), "Generate")
100  {
101  OnClicked = (btn, userData) =>
102  {
103  Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed));
104  Generate(GameMain.GameSession?.Campaign);
105  InitProjectSpecific();
106  return true;
107  }
108  };
109  }
110 #endif
111 
112  partial void InitProjectSpecific()
113  {
114  noiseOverlay ??= new Sprite("Content/UI/noise.png", Vector2.Zero);
115 
116  OnLocationChanged.RegisterOverwriteExisting(
117  "Map.InitProjSpecific".ToIdentifier(),
118  (locationChangeInfo) => LocationChanged(locationChangeInfo.PrevLocation, locationChangeInfo.NewLocation));
119 
120  borders = new Rectangle(
121  (int)Locations.Min(l => l.MapPosition.X),
122  (int)Locations.Min(l => l.MapPosition.Y),
123  (int)Locations.Max(l => l.MapPosition.X),
124  (int)Locations.Max(l => l.MapPosition.Y));
125  borders.Width -= borders.X;
126  borders.Height -= borders.Y;
127 
128  if (CurrentLocation != null)
129  {
131  }
132 
133  Vector2 tileSize = generationParams.MapTiles.Values.First().First().size * generationParams.MapTileScale;
134  int tilesX = (int)Math.Ceiling(Width / tileSize.X);
135  int tilesY = (int)Math.Ceiling(Height / tileSize.Y);
136  mapTiles = new Sprite[tilesX, tilesY];
137  tileDiscovered = new bool[tilesX, tilesY];
138  HashSet<Biome> missingBiomes = new HashSet<Biome>();
139  for (int x = 0; x < tilesX; x++)
140  {
141  for (int y = 0; y < tilesY; y++)
142  {
143  var biome = GetBiome(x * tileSize.X);
144  ImmutableArray<Sprite> tileList;
145  if (generationParams.MapTiles.ContainsKey(biome.Identifier))
146  {
147  tileList = generationParams.MapTiles[biome.Identifier];
148  }
149  else
150  {
151  tileList = generationParams.MapTiles.Values.First();
152  missingBiomes.Add(biome);
153  }
154  mapTiles[x, y] = tileList[x % tileList.Length];
155  }
156  }
157 
158  foreach (var missingBiome in missingBiomes)
159  {
160  DebugConsole.ThrowError($"Could not find campaign map sprites for the biome \"{missingBiome.Identifier}\". Using the sprites of the first biome instead...");
161  }
162 
163  beaconStationActiveText = RichString.Rich(TextManager.Get("BeaconStationActiveTooltip"));
164  beaconStationInactiveText = RichString.Rich(TextManager.Get("BeaconStationInactiveTooltip"));
165 
166  RemoveFogOfWar(StartLocation);
167 
168  GenerateAllLocationConnectionVisuals();
169  }
170 
171  partial void GenerateAllLocationConnectionVisuals()
172  {
173  foreach (LocationConnection connection in Connections)
174  {
175  GenerateLocationConnectionVisuals(connection);
176  }
177  }
178  partial void GenerateLocationConnectionVisuals(LocationConnection connection)
179  {
180  Vector2 connectionStart = connection.Locations[0].MapPosition;
181  Vector2 connectionEnd = connection.Locations[1].MapPosition;
182  float connectionLength = Vector2.Distance(connectionStart, connectionEnd);
183  int iterations = Math.Min((int)Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier), 5);
184  connection.CrackSegments.Clear();
185  connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine(
186  connectionStart, connectionEnd,
187  iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier,
188  rng: Rand.GetRNG(Rand.RandSync.ServerAndClient)));
189  }
190 
191  private void LocationChanged(Location prevLocation, Location newLocation)
192  {
193  if (prevLocation == newLocation) { return; }
194  //focus on starting location
195  if (prevLocation != null)
196  {
197  mapAnimQueue.Enqueue(new MapAnim()
198  {
199  EndZoom = 1.0f,
200  EndLocation = prevLocation,
201  Duration = MathHelper.Clamp(Vector2.Distance(-DrawOffset, prevLocation.MapPosition) / 1000.0f, 0.1f, 0.5f)
202  });
203  mapAnimQueue.Enqueue(new MapAnim()
204  {
205  EndZoom = 0.5f,
206  StartLocation = prevLocation,
207  EndLocation = newLocation,
208  Duration = 2.0f,
209  StartDelay = 0.5f
210  });
211  }
212  else
213  {
214  currLocationIndicatorPos = CurrentLocation.MapPosition;
215  }
216 
217  if (newLocation.Visited)
218  {
219  RemoveFogOfWar(newLocation);
220  }
221  }
222 
223  partial void RemoveFogOfWarProjSpecific(Location location) => RemoveFogOfWar(location);
224 
225  private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true)
226  {
227  if (mapTiles == null) { return; }
228  if (location == null) { return; }
229 
230  var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault();
231  if (mapTile == null) { return; }
232 
233  Vector2 mapTileSize = mapTile.size * generationParams.MapTileScale;
234  int startX = (int)Math.Max(Math.Floor(location.MapPosition.X / mapTileSize.X - 0.25f), 0);
235  int startY = (int)Math.Max(Math.Floor(location.MapPosition.Y / mapTileSize.Y - 0.25f), 0);
236  int endX = (int)Math.Min(Math.Floor(location.MapPosition.X / mapTileSize.X + 0.25f), mapTiles.GetLength(0) - 1);
237  int endY = (int)Math.Min(Math.Floor(location.MapPosition.Y / mapTileSize.Y + 0.25f), mapTiles.GetLength(1) - 1);
238  for (int x = startX; x <= endX; x++)
239  {
240  for (int y = startY; y <= endY; y++)
241  {
242  tileDiscovered[x, y] = true;
243  }
244  }
245  if (removeFromAdjacentLocations)
246  {
247  foreach (LocationConnection c in location.Connections)
248  {
249  var otherLocation = c.OtherLocation(location);
250  RemoveFogOfWar(otherLocation, removeFromAdjacentLocations: false);
251  }
252  }
253  }
254 
255  private bool IsInFogOfWar(Location location)
256  {
257  if (GameMain.DebugDraw) { return false; }
258  Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale;
259  int x = (int)Math.Floor(location.MapPosition.X / mapTileSize.X);
260  int y = (int)Math.Floor(location.MapPosition.Y / mapTileSize.Y);
261 
262  return !tileDiscovered[MathHelper.Clamp(x, 0, tileDiscovered.Length), MathHelper.Clamp(y, 0, tileDiscovered.Length)];
263  }
264 
265  private class MapNotification
266  {
267  public readonly RichString Text;
268  public readonly GUIFont Font;
269 
270  public readonly Vector2 TextSize;
271 
272  public int TimesShown;
273 
274  public float Offset;
275 
276  public readonly Location RelatedLocation;
277 
278  public bool IsCurrentlyVisible;
279 
280  public MapNotification(string text, GUIFont font, List<MapNotification> existingNotifications, Location relatedLocation)
281  {
282  Text = RichString.Rich(text);
283  Font = font;
284  TextSize = Font.MeasureString(Font.ForceUpperCase ? Text.SanitizedValue.ToUpper() : Text.SanitizedValue);
285  if (existingNotifications.Any())
286  {
287  Offset = existingNotifications.Max(n => n.Offset + n.TextSize.X + GUI.IntScale(60));
288  }
289  RelatedLocation = relatedLocation;
290  }
291  }
292 
293  private readonly List<MapNotification> mapNotifications = new List<MapNotification>();
294 
295  partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change)
296  {
297  var messages = change.GetMessages(location.Faction);
298  if (!messages.Any()) { return; }
299 
300  string msg = messages.GetRandom(Rand.RandSync.Unsynced)
301  .Replace("[previousname]", $"‖color:gui.yellow‖{prevName}‖end‖")
302  .Replace("[name]", $"‖color:gui.yellow‖{location.DisplayName}‖end‖");
303  location.LastTypeChangeMessage = msg;
304 
305  mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location));
306  }
307 
308  public void DrawNotifications(SpriteBatch spriteBatch, GUICustomComponent container)
309  {
310  Vector2 pos = new Vector2(container.Rect.Right, container.Rect.Center.Y);
311  foreach (var notification in mapNotifications)
312  {
313  Vector2 textPos = pos + new Vector2(notification.Offset, -notification.TextSize.Y / 2);
314 
315  notification.Font.DrawStringWithColors(
316  spriteBatch,
317  notification.Text.SanitizedValue,
318  textPos,
319  Color.White, 0.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0,
320  notification.Text.RichTextData);
321 
322  int margin = container.Rect.Width / 5;
323  notification.IsCurrentlyVisible =
324  textPos.X < container.Rect.Right - margin &&
325  textPos.X + notification.TextSize.X > container.Rect.X + margin;
326  }
327  }
328 
329  private void UpdateNotifications(float deltaTime, GUICustomComponent mapContainer)
330  {
331  if (mapNotifications.Count < 5)
332  {
333  int maxIndex = 1;
334  while (TextManager.ContainsTag("randomnews" + maxIndex))
335  {
336  maxIndex++;
337  }
338  string textTag = "randomnews" + Rand.Range(0, maxIndex);
339  if (TextManager.ContainsTag(textTag))
340  {
341  mapNotifications.Add(new MapNotification(TextManager.Get(textTag).Value, GUIStyle.SubHeadingFont, mapNotifications, relatedLocation: null));
342  }
343  }
344 
345  for (int i = mapNotifications.Count - 1; i >= 0; i--)
346  {
347  var notification = mapNotifications[i];
348  notification.Offset -= deltaTime * 75.0f;
349  if (notification.Offset < -notification.TextSize.X - mapContainer.Rect.Width)
350  {
351  notification.Offset = Math.Max(mapNotifications.Max(n => n.Offset + n.TextSize.X) + GUI.IntScale(60), 0);
352  notification.TimesShown++;
353  if (mapNotifications.Count > 5)
354  {
355  mapNotifications.RemoveAt(i);
356  }
357  else if (mapNotifications.Count > 3 && notification.TimesShown > 2)
358  {
359  mapNotifications.RemoveAt(i);
360  }
361  }
362  }
363  }
364 
365  private void CreateLocationInfoOverlay(Location location)
366  {
367  locationInfoOverlay = new GUIFrame(new RectTransform(new Point(GUI.IntScale(350), GUI.IntScale(350)), GUI.Canvas), style: "GUIToolTip")
368  {
369  UserData = location
370  };
371  locationInfoOverlay.Color *= 0.8f;
372 
373  var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), locationInfoOverlay.RectTransform, Anchor.Center))
374  {
375  Stretch = true,
376  RelativeSpacing = 0.02f
377  };
378 
379  bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null;
380 
381  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, font: GUIStyle.LargeFont) { Padding = Vector4.Zero };
382  if (!location.Type.Name.IsNullOrEmpty())
383  {
384  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero };
385  }
386 
387  CreateSpacing(10);
388 
389  if (!location.Type.Description.IsNullOrEmpty())
390  {
391  CreateTextWithIcon(location.Type.Description, location.Type.Sprite);
392  }
393 
394  int highestSubTier = location.HighestSubmarineTierAvailable();
395  List<(SubmarineClass subClass, int tier)> overrideTiers = null;
396  if (location.CanHaveSubsForSale())
397  {
398  overrideTiers = new List<(SubmarineClass subClass, int tier)>();
399  foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass)))
400  {
401  if (subClass == SubmarineClass.Undefined) { continue; }
402  int highestClassTier = location.HighestSubmarineTierAvailable(subClass);
403  if (highestClassTier > 0 && highestClassTier > highestSubTier)
404  {
405  overrideTiers.Add((subClass, highestClassTier));
406  }
407  }
408  }
409  if (highestSubTier > 0)
410  {
411  CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon");
412  }
413  if (overrideTiers != null)
414  {
415  foreach (var (subClass, tier) in overrideTiers)
416  {
417  CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon");
418  }
419  }
420 
421  CreateSpacing(10);
422 
423  void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null)
424  {
425  var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true)
426  {
427  Stretch = true,
428  CanBeFocused = true
429  };
430  var guiIcon =
431  style == null ?
432  new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) :
433  new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style);
434  var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text);
435  textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0);
436  textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0);
437  }
438 
439  void CreateSpacing(int height)
440  {
441  new GUIFrame(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(height)), content.RectTransform), style: null);
442  }
443 
444  if (location.Faction != null)
445  {
446  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
447  RichString.Rich(TextManager.GetWithVariables("reputationgainnotification",
448  ("[value]", string.Empty),
449  ("[reputationname]", $"‖color:{XMLExtensions.ToStringHex(location.Faction.Prefab.IconColor)}‖{location.Faction.Prefab.Name}‖end‖"))))
450  {
451  Padding = Vector4.Zero
452  };
453 
454  CreateSpacing(10);
455 
456  var repBarHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(25)), content.RectTransform), isHorizontal: true)
457  {
458  Stretch = true,
459  RelativeSpacing = 0.05f
460  };
461  new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) =>
462  {
463  if (location.Reputation == null) { return; }
464  RoundSummary.DrawReputationBar(sb, component.Rect,
465  location.Reputation.NormalizedValue,
466  location.Reputation.MinReputation, location.Reputation.MaxReputation);
467  });
468 
469  new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform),
470  location.Reputation.GetFormattedReputationText(), textAlignment: Alignment.CenterRight);
471 
472  new GUIImage(new RectTransform(new Vector2(0.25f, 0.5f), locationInfoOverlay.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.05f) },
473  location.Faction.Prefab.Icon, scaleToFit: true)
474  {
475  Color = location.Faction.Prefab.IconColor * 0.5f
476  };
477  CreateSpacing(20);
478  }
479 
480  locationInfoOverlay.RectTransform.NonScaledSize =
481  new Point(
482  Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)),
483  (int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y));
484  }
485 
486  partial void ClearAnimQueue()
487  {
488  mapAnimQueue.Clear();
489  }
490 
491  public void Update(CampaignMode campaign, float deltaTime, GUICustomComponent mapContainer)
492  {
493  Rectangle rect = mapContainer.Rect;
494 
495  UpdateNotifications(deltaTime, mapContainer);
496 
497  var currentDisplayLocation = campaign?.GetCurrentDisplayLocation();
498  if (currentDisplayLocation != null)
499  {
500  if (!currentDisplayLocation.Discovered)
501  {
502  RemoveFogOfWar(currentDisplayLocation);
503  Discover(currentDisplayLocation);
504  if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
505  {
506  furthestDiscoveredLocation = currentDisplayLocation;
507  }
508  }
509  }
510 
511  Vector2 currentPosition = currentDisplayLocation.MapPosition;
512  if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection && Level.Loaded.StartLocation != null && Level.Loaded.EndLocation != null)
513  {
514  Vector2 startPos = currentDisplayLocation == Level.Loaded.StartLocation ? Level.Loaded.StartLocation.MapPosition : Level.Loaded.EndLocation.MapPosition;
515  int moveDir = currentDisplayLocation == Level.Loaded.StartLocation ? 1 : -1;
516 
518  currentPosition = startPos +
519  Vector2.Normalize(diff) * Math.Min(100, diff.Length() * 0.2f) * moveDir;
520  }
521  else
522  {
523  currentPosition += Vector2.UnitY * 35;
524  }
525 
526  currLocationIndicatorPos = Vector2.Lerp(currLocationIndicatorPos, currentPosition, deltaTime);
527 #if DEBUG
528  if (GameMain.DebugDraw)
529  {
530  if (editor == null) CreateEditor();
531  editor.AddToGUIUpdateList(order: 1);
532  }
533 
534  if (PlayerInput.KeyHit(Keys.Space))
535  {
536  Radiation?.OnStep();
537  }
538 #endif
539 
540  Radiation?.MapUpdate(deltaTime);
541 
542  if (mapAnimQueue.Count > 0)
543  {
544  hudVisibility = Math.Max(hudVisibility - deltaTime, 0.0f);
545  UpdateMapAnim(mapAnimQueue.Peek(), deltaTime);
546  if (mapAnimQueue.Peek().Finished)
547  {
548  mapAnimQueue.Dequeue();
549  }
550  return;
551  }
552 
553  hudVisibility = Math.Min(hudVisibility + deltaTime, 0.75f + (float)Math.Sin(Timing.TotalTime * 3.0f) * 0.25f);
554 
555  Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y);
556  Vector2 viewOffset = DrawOffset + drawOffsetNoise;
557  if (HighlightedLocation != null)
558  {
559  Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom;
560  if (locationInfoOverlay == null || locationInfoOverlay.UserData != HighlightedLocation)
561  {
562  CreateLocationInfoOverlay(HighlightedLocation);
563  }
564 
565  Point offsetFromLocationIcon = new Point(GUI.IntScale(25));
566  var locationInfoRt = locationInfoOverlay.RectTransform;
567  if (locationInfoRt.Pivot == Pivot.BottomLeft || locationInfoRt.Pivot == Pivot.BottomRight)
568  {
569  offsetFromLocationIcon.Y = -offsetFromLocationIcon.Y;
570  }
571  if (locationInfoRt.Pivot == Pivot.TopRight || locationInfoRt.Pivot == Pivot.BottomRight)
572  {
573  offsetFromLocationIcon.X = -offsetFromLocationIcon.X;
574  }
575  locationInfoRt.ScreenSpaceOffset = highlightedLocationDrawPos.ToPoint() + offsetFromLocationIcon;
576  if (locationInfoOverlay.Rect.Bottom > rect.Bottom)
577  {
578  locationInfoRt.Pivot = Pivot.BottomLeft;
579  }
580  if (locationInfoOverlay.Rect.Right > rect.Right)
581  {
582  locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight;
583  }
584  locationInfoOverlay?.AddToGUIUpdateList(order: 1);
585  }
586 
587  float closestDist = 0.0f;
588  HighlightedLocation = null;
589  if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer))
590  {
591  for (int i = 0; i < Locations.Count; i++)
592  {
593  Location location = Locations[i];
594  if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; }
595 
596  Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom;
597  if (!rect.Contains(pos)) { continue; }
598 
599  Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite;
600  float iconScale = generationParams.LocationIconSize / locationSprite.size.X;
601  if (location == currentDisplayLocation) { iconScale *= 1.2f; }
602 
603  Rectangle drawRect = locationSprite.SourceRect;
604  drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f);
605  drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f);
606  drawRect.X = (int)pos.X - drawRect.Width / 2;
607  drawRect.Y = (int)pos.Y - drawRect.Width / 2;
608 
609  if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; }
610 
611  float dist = Vector2.Distance(PlayerInput.MousePosition, pos);
612  if (HighlightedLocation == null || dist < closestDist)
613  {
614  closestDist = dist;
615  HighlightedLocation = location;
616  }
617  }
618  }
619 
620  if (SelectedConnection != null)
621  {
622  connectionHighlightState = Math.Min(connectionHighlightState + deltaTime, 1.0f);
623  }
624  else
625  {
626  connectionHighlightState = 0.0f;
627  }
628 
629  if (GUI.KeyboardDispatcher.Subscriber == null)
630  {
631  float moveSpeed = 1000.0f;
632  Vector2 moveAmount = Vector2.Zero;
633  if (PlayerInput.KeyDown(InputType.Left)) { moveAmount += Vector2.UnitX; }
634  if (PlayerInput.KeyDown(InputType.Right)) { moveAmount -= Vector2.UnitX; }
635  if (PlayerInput.KeyDown(InputType.Up)) { moveAmount += Vector2.UnitY; }
636  if (PlayerInput.KeyDown(InputType.Down)) { moveAmount -= Vector2.UnitY; }
637  DrawOffset += moveAmount * moveSpeed / zoom * deltaTime;
638  }
639 
640  targetZoom = MathHelper.Clamp(targetZoom, generationParams.MinZoom, generationParams.MaxZoom);
641  zoom = MathHelper.Lerp(zoom, targetZoom * GUI.Scale, 0.1f);
642 
643  if (GUI.MouseOn == mapContainer)
644  {
645  foreach (LocationConnection connection in Connections)
646  {
647  if (HighlightedLocation != currentDisplayLocation &&
648  connection.Locations.Contains(HighlightedLocation) &&
649  connection.Locations.Contains(currentDisplayLocation))
650  {
652  SelectedLocation != HighlightedLocation && HighlightedLocation != null)
653  {
654  if (connection.Locked)
655  {
656  new GUIMessageBox(string.Empty, TextManager.Get("LockedPathTooltip"));
657  }
658  //clients aren't allowed to select the location without a permission
659  else if (CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap))
660  {
661  connectionHighlightState = 0.0f;
662  SelectedConnection = connection;
663  SelectedLocation = HighlightedLocation;
664 
665  OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
667  }
668  }
669  }
670  }
671 
672  targetZoom += PlayerInput.ScrollWheelSpeed / 500.0f;
673 
674  if (PlayerInput.MidButtonHeld() || (HighlightedLocation == null && PlayerInput.PrimaryMouseButtonHeld()))
675  {
676  DrawOffset += PlayerInput.MouseSpeed / zoom;
677  }
678  if (AllowDebugTeleport)
679  {
680  if (PlayerInput.DoubleClicked() && HighlightedLocation != null)
681  {
682  var passedConnection = currentDisplayLocation.Connections.Find(c => c.OtherLocation(currentDisplayLocation) == HighlightedLocation);
683  if (passedConnection != null)
684  {
685  passedConnection.Passed = true;
686  }
687 
688  Location prevLocation = currentDisplayLocation;
689  CurrentLocation = HighlightedLocation;
690  Level.Loaded.DebugSetStartLocation(CurrentLocation);
692 
693  Discover(CurrentLocation);
694  Visit(CurrentLocation);
695  OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
696  SelectLocation(-1);
697  if (GameMain.Client == null)
698  {
699  CurrentLocation.CreateStores();
700  ProgressWorld(campaign);
701  Radiation?.OnStep(1);
702  }
703  else
704  {
706  }
707  }
708 
709  if (PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation == null)
710  {
711  SelectLocation(-1);
712  }
713  }
714  }
715  }
716 
717  public void Draw(CampaignMode campaign, SpriteBatch spriteBatch, GUICustomComponent mapContainer)
718  {
719  tooltip = null;
720  var currentDisplayLocation = campaign?.GetCurrentDisplayLocation();
721 
722  Rectangle rect = mapContainer.Rect;
723 
724  Vector2 viewSize = new Vector2(rect.Width / zoom, rect.Height / zoom);
725  Vector2 edgeBuffer = new Vector2(rect.Width * 0.05f);
726  DrawOffset.X = MathHelper.Clamp(DrawOffset.X, -Width - edgeBuffer.X + viewSize.X / 2.0f, edgeBuffer.X - viewSize.X / 2.0f);
727  DrawOffset.Y = MathHelper.Clamp(DrawOffset.Y, -Height - edgeBuffer.Y + viewSize.Y / 2.0f, edgeBuffer.Y - viewSize.Y / 2.0f);
728 
729  drawOffsetNoise = new Vector2(
730  (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.1f % 255, Timing.TotalTime * 0.1f % 255, 0) - 0.5f,
731  (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.2f % 255, Timing.TotalTime * 0.2f % 255, 0.5f) - 0.5f) * 10.0f;
732 
733  Vector2 viewOffset = DrawOffset + drawOffsetNoise;
734 
735  Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y);
736 
737  float missionIconScale = generationParams.MissionIcon != null ? 18.0f / generationParams.MissionIcon.SourceRect.Width : 1.0f;
738 
739  Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle;
740  spriteBatch.End();
741  spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect);
742  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
743 
744  Vector2 topLeft = rectCenter + viewOffset - rect.Location.ToVector2();
745  Vector2 bottomRight = topLeft + new Vector2(Width, Height);
746  Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale;
747 
748  int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1;
749  int startY = (int)Math.Floor(-topLeft.Y / mapTileSize.Y) - 1;
750  int endX = (int)Math.Ceiling((-topLeft.X + rect.Width) / mapTileSize.X);
751  int endY = (int)Math.Ceiling((-topLeft.Y + rect.Height) / mapTileSize.Y);
752 
753  float noiseT = (float)(Timing.TotalTime * 0.01f);
754  cameraNoiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT * 0.5f, noiseT * 0.2f);
755  float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 5.0f;
756 
757  for (int x = startX; x <= endX; x++)
758  {
759  for (int y = startY; y <= endY; y++)
760  {
761  int tileX = Math.Abs(x) % mapTiles.GetLength(0);
762  int tileY = Math.Abs(y) % mapTiles.GetLength(1);
763  Vector2 tilePos = rectCenter + (viewOffset + new Vector2(x, y) * mapTileSize) * zoom;
764  mapTiles[tileX, tileY].Draw(spriteBatch, tilePos, Color.White, origin: Vector2.Zero, scale: generationParams.MapTileScale * zoom);
765 
766  if (GameMain.DebugDraw) { continue; }
767  if (!tileDiscovered[tileX, tileY] || x < 0 || y < 0 || x >= tileDiscovered.GetLength(0) || y >= tileDiscovered.GetLength(1))
768  {
769  generationParams.FogOfWarSprite?.Draw(spriteBatch, tilePos, Color.White * cameraNoiseStrength, origin: Vector2.Zero, scale: generationParams.MapTileScale * zoom);
770  noiseOverlay.DrawTiled(spriteBatch, tilePos, mapTileSize * zoom,
771  startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)),
772  color: Color.White * cameraNoiseStrength * 0.2f,
773  textureScale: Vector2.One * noiseScale);
774  }
775  }
776  }
777 
778  if (GameMain.DebugDraw)
779  {
780  if (topLeft.X > rect.X)
781  GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Y, (int)(topLeft.X - rect.X), rect.Height), Color.Black * 0.5f, true);
782  if (topLeft.Y > rect.Y)
783  GUI.DrawRectangle(spriteBatch, new Rectangle((int)topLeft.X, rect.Y, (int)(bottomRight.X - topLeft.X), (int)(topLeft.Y - rect.Y)), Color.Black * 0.5f, true);
784  if (bottomRight.X < rect.Right)
785  GUI.DrawRectangle(spriteBatch, new Rectangle((int)bottomRight.X, rect.Y, (int)(rect.Right - bottomRight.X), rect.Height), Color.Black * 0.5f, true);
786  if (bottomRight.Y < rect.Bottom)
787  GUI.DrawRectangle(spriteBatch, new Rectangle((int)topLeft.X, (int)bottomRight.Y, (int)(bottomRight.X - topLeft.X), (int)(rect.Bottom - bottomRight.Y)), Color.Black * 0.5f, true);
788  }
789 
790  float rawNoiseScale = 1.0f + PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1));
791  DrawNoise(spriteBatch, rect, rawNoiseScale);
792 
793  Radiation?.Draw(spriteBatch, rect, zoom);
794 
795  if (generationParams.ShowLocations)
796  {
797  foreach (LocationConnection connection in Connections)
798  {
799  if (IsInFogOfWar(connection.Locations[0]) && IsInFogOfWar(connection.Locations[1])) { continue; }
800  DrawConnection(spriteBatch, connection, rect, viewOffset, currentDisplayLocation);
801  }
802 
803  for (int i = 0; i < Locations.Count; i++)
804  {
805  Location location = Locations[i];
806  if (!location.Discovered && IsInFogOfWar(location)) { continue; }
807  bool isEndLocation = endLocations.Contains(location);
808  if (!GameMain.DebugDraw && isEndLocation && location != endLocations.First()) { continue; }
809  Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom;
810 
811  Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite;
812 
813  Rectangle drawRect = locationSprite.SourceRect;
814  drawRect.X = (int)pos.X - drawRect.Width / 2;
815  drawRect.Y = (int)pos.Y - drawRect.Width / 2;
816 
817  if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableMissions.Any())
818  {
819  Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y);
820  generationParams.MissionIcon.Draw(spriteBatch,
821  offScreenMissionIconPos,
822  generationParams.IndicatorColor, scale: missionIconScale * zoom);
823  GUI.Arrow.Draw(spriteBatch,
824  offScreenMissionIconPos + Vector2.UnitX * generationParams.MissionIcon.size.X * missionIconScale * zoom,
825  generationParams.IndicatorColor, MathHelper.PiOver2, scale: 0.5f);
826  }
827 
828 
829  if (!rect.Intersects(drawRect)) { continue; }
830 
831  Color color = location.Type.SpriteColor;
832  if (!location.Visited) { color = Color.White; }
833  if (location.Connections.Find(c => c.Locations.Contains(currentDisplayLocation)) == null)
834  {
835  color *= 0.5f;
836  }
837 
838  float iconScale = location == currentDisplayLocation ? 1.2f : 1.0f;
839  if (location == HighlightedLocation) { iconScale *= 1.2f; }
840  if (isEndLocation) { iconScale *= 2.0f; }
841 
842  float notificationPulseAmount = 1.0f;
843  float notificationColorLerp = 0.0f;
844  if (mapNotifications.Any(n => n.RelatedLocation == location && n.IsCurrentlyVisible))
845  {
846  float sin = MathF.Sin((float)Timing.TotalTime * 2.0f);
847  notificationPulseAmount = Math.Max(sin + 0.5f, 1.0f);
848  notificationColorLerp = (notificationPulseAmount - 1.0f) * 4.0f;
849  color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp);
850  iconScale *= notificationPulseAmount;
851  }
852 
853  locationSprite.Draw(spriteBatch, pos, color,
854  scale: generationParams.LocationIconSize / locationSprite.size.X * iconScale * zoom);
855 
856  if (location.Faction != null)
857  {
858  float factionIconScale = iconScale * 0.7f;
859  Sprite factionIcon = location.Faction.Prefab.IconSmall ?? location.Faction.Prefab.Icon;
860  Color factionIconColor = Color.Lerp(color, location.Faction.Prefab.IconColor, notificationColorLerp);
861  factionIcon.Draw(spriteBatch, pos + new Vector2(-15, 15) * zoom, factionIconColor,
862  scale: generationParams.LocationIconSize / factionIcon.size.X * factionIconScale * zoom);
863  }
864 
865  if (location == currentDisplayLocation)
866  {
867  if (SelectedLocation != null)
868  {
869  Vector2 dir = Vector2.Normalize(SelectedLocation.MapPosition - currLocationIndicatorPos);
870  GUI.Arrow.Draw(spriteBatch,
871  rectCenter + (currLocationIndicatorPos + viewOffset) * zoom + dir * generationParams.LocationIconSize * 0.6f * zoom,
872  generationParams.IndicatorColor,
873  GUI.Arrow.Origin,
874  rotate: MathUtils.VectorToAngle(dir) + MathHelper.PiOver2,
875  new Vector2(0.5f, 1.0f) * zoom);
876  }
877  generationParams.CurrentLocationIndicator.Draw(spriteBatch,
878  rectCenter + (currLocationIndicatorPos + viewOffset) * zoom,
879  generationParams.IndicatorColor,
880  generationParams.CurrentLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.CurrentLocationIndicator.size.X) * 0.8f * zoom);
881 
882  }
883 
884  if (location == SelectedLocation)
885  {
886  generationParams.SelectedLocationIndicator.Draw(spriteBatch,
887  rectCenter + (location.MapPosition + viewOffset) * zoom,
888  generationParams.IndicatorColor,
889  generationParams.SelectedLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.SelectedLocationIndicator.size.X) * 1.7f * zoom);
890  }
891 
892  if (location.TimeSinceLastTypeChange < 1 && !string.IsNullOrEmpty(location.LastTypeChangeMessage) && generationParams.TypeChangeIcon != null)
893  {
894  Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom;
895  float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width;
896  Color iconColor = GUIStyle.Red;
897  color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp);
898  iconScale *= notificationPulseAmount;
899  generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, iconColor, scale: typeChangeIconScale * zoom);
900  if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom &&
901  (tooltip == null || IsPreferredTooltip(typeChangeIconPos)))
902  {
903  tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), RichString.Rich(location.LastTypeChangeMessage));
904  }
905  }
906  if (location != CurrentLocation && generationParams.MissionIcon != null)
907  {
908  if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) ||
909  location.AvailableMissions.Any(m => m.Locations[0] == m.Locations[1]))
910  {
911  Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom;
912  generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom);
913  if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos))
914  {
915  var availableMissions = CurrentLocation.AvailableMissions
916  .Where(m => m.Locations.Contains(location))
917  .Concat(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1]))
918  .Distinct();
919  tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name)));
920  }
921  }
922  }
923 
924  if (GameMain.DebugDraw)
925  {
926  Vector2 dPos = pos;
927  if (location == HighlightedLocation)
928  {
929  dPos.Y -= 80;
930  GUI.DrawString(spriteBatch, dPos + new Vector2(15, 32), "Faction: " + (location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont);
931  GUI.DrawString(spriteBatch, dPos + new Vector2(15, 50), "Secondary Faction: " + (location.SecondaryFaction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont);
932  dPos.Y += 48;
933 
934  if (PlayerInput.KeyDown(Keys.LeftShift))
935  {
936  GUI.DrawString(spriteBatch, new Vector2(150,150), "Dist: " +
937  GetDistanceToClosestLocationOrConnection(CurrentLocation, int.MaxValue, loc => loc == location), Color.White, Color.Black, font: GUIStyle.SubHeadingFont);
938 
939  }
940  }
941  dPos.Y += 48;
942  GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont);
943  }
944  }
945  }
946 
947  DrawDecorativeHUD(spriteBatch, rect);
948 
949  bool drawRadiationTooltip = true;
950 
951  if (tooltip != null)
952  {
953  GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea);
954  drawRadiationTooltip = false;
955  }
956 
957  if (drawRadiationTooltip)
958  {
959  Radiation?.DrawFront(spriteBatch);
960  }
961 
962  spriteBatch.End();
963  GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect;
964  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
965  }
966 
967  public static void DrawNoise(SpriteBatch spriteBatch, Rectangle rect, float strength)
968  {
969  noiseOverlay ??= new Sprite("Content/UI/noise.png", Vector2.Zero);
970 
971  float noiseT = (float)(Timing.TotalTime * 0.01f);
972  float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 5.0f;
973 
974  float rawNoiseScale = 1.0f + GetPerlinNoise();
975 
976  noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(),
977  startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)),
978  color : Color.White * strength * 0.1f,
979  textureScale: Vector2.One * rawNoiseScale);
980 
981  noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(),
982  startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)),
983  color: new Color(20,20,20,50),
984  textureScale: Vector2.One * rawNoiseScale * 2);
985 
986  noiseOverlay.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight),
987  startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)),
988  color: Color.White * strength * 0.1f,
989  textureScale: Vector2.One * noiseScale);
990  }
991 
992  private static float GetPerlinNoise() => PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1));
993 
994  private void DrawConnection(SpriteBatch spriteBatch, LocationConnection connection, Rectangle viewArea, Vector2 viewOffset, Location currentDisplayLocation, Color? overrideColor = null)
995  {
996  Color connectionColor;
997  if (GameMain.DebugDraw)
998  {
999  float sizeFactor = MathUtils.InverseLerp(
1000  generationParams.SmallLevelConnectionLength,
1001  generationParams.LargeLevelConnectionLength,
1002  connection.Length);
1003  connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUIStyle.Orange, GUIStyle.Red);
1004  }
1005  else if (overrideColor.HasValue)
1006  {
1007  connectionColor = overrideColor.Value;
1008  }
1009  else
1010  {
1011  connectionColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor;
1012  }
1013 
1014  int width = (int)(generationParams.LocationConnectionWidth * zoom);
1015 
1016  //current level
1017  if (Level.Loaded?.LevelData == connection.LevelData)
1018  {
1019  connectionColor = generationParams.HighlightedConnectionColor;
1020  width = (int)(width * 1.5f);
1021  }
1022  //selected connection
1023  if (SelectedLocation != currentDisplayLocation &&
1024  connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(currentDisplayLocation))
1025  {
1026  connectionColor = generationParams.HighlightedConnectionColor;
1027  width *= 2;
1028  }
1029  //highlighted connection
1030  else if (HighlightedLocation != currentDisplayLocation &&
1031  connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(currentDisplayLocation))
1032  {
1033  connectionColor = generationParams.HighlightedConnectionColor;
1034  width *= 2;
1035  }
1036 
1037  Vector2 rectCenter = viewArea.Center.ToVector2();
1038 
1039  int startIndex = connection.CrackSegments.Count > 2 ? 1 : 0;
1040  int endIndex = connection.CrackSegments.Count > 2 ? connection.CrackSegments.Count - 1 : connection.CrackSegments.Count;
1041 
1042  Vector2? connectionStart = null;
1043  Vector2? connectionEnd = null;
1044  for (int i = startIndex; i < endIndex; i++)
1045  {
1046  var segment = connection.CrackSegments[i];
1047 
1048  Vector2 start = rectCenter + (segment[0] + viewOffset) * zoom;
1049  if (!connectionStart.HasValue) { connectionStart = start; }
1050  Vector2 end = rectCenter + (segment[1] + viewOffset) * zoom;
1051  connectionEnd = end;
1052 
1053  if (!viewArea.Contains(start) && !viewArea.Contains(end))
1054  {
1055  continue;
1056  }
1057  else
1058  {
1059  if (MathUtils.GetLineRectangleIntersection(start, end, new Rectangle(viewArea.X, viewArea.Y + viewArea.Height, viewArea.Width, viewArea.Height), out Vector2 intersection))
1060  {
1061  if (!viewArea.Contains(start))
1062  {
1063  start = intersection;
1064  }
1065  else
1066  {
1067  end = intersection;
1068  }
1069  }
1070  }
1071 
1072  float a = 1.0f;
1073  if (!connection.Locations[0].Visited && !connection.Locations[1].Visited)
1074  {
1075  if (IsInFogOfWar(connection.Locations[0]))
1076  {
1077  a = (float)i / connection.CrackSegments.Count;
1078  }
1079  else if (IsInFogOfWar(connection.Locations[1]))
1080  {
1081  a = 1.0f - (float)i / connection.CrackSegments.Count;
1082  }
1083  }
1084  float dist = Vector2.Distance(start, end);
1085  var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite;
1086  if (connectionSprite?.Texture == null) { continue; }
1087 
1088  Color segmentColor = connectionColor;
1089  int segmentWidth = width;
1090  if (connection == SelectedConnection)
1091  {
1092  float t = (i - startIndex) / (float)(endIndex - startIndex - 1);
1093  if (currentDisplayLocation == connection.Locations[1]) { t = 1.0f - t; }
1094  if (t > connectionHighlightState)
1095  {
1096  segmentWidth /= 2;
1097  segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor;
1098  }
1099  }
1100 
1101  spriteBatch.Draw(connectionSprite.Texture,
1102  new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), segmentWidth),
1103  connectionSprite.SourceRect, segmentColor * a,
1104  MathUtils.VectorToAngle(end - start),
1105  new Vector2(0, connectionSprite.size.Y / 2), SpriteEffects.None, 0.01f);
1106  }
1107 
1108  int iconCount = 0, iconIndex = 0;
1109  if (connectionStart.HasValue && connectionEnd.HasValue)
1110  {
1111  if (connection.LevelData.HasBeaconStation) { iconCount++; }
1112  if (connection.LevelData.HasHuntingGrounds) { iconCount++; }
1113  if (connection.Locked) { iconCount++; }
1114  string tooltip = null;
1115 
1116  float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo);
1117  string crushDepthWarningIconStyle = null;
1118 
1119  var levelData = connection.LevelData;
1120  float spawnDepth =
1121  levelData.InitialDepth +
1122  //base the warning on the start or end position of the level, whichever is deeper
1123  levelData.Size.Y * Math.Max(levelData.GenerationParams.StartPosition.Y, levelData.GenerationParams.EndPosition.Y);
1124 
1125  //"high warning" if the sub spawns at/below crush depth
1126  if (spawnDepth * Physics.DisplayToRealWorldRatio > subCrushDepth)
1127  {
1128  iconCount++;
1129  crushDepthWarningIconStyle = "CrushDepthWarningHighIcon";
1130  tooltip = "crushdepthwarninghigh";
1131  }
1132  //"low warning" if the spawn position is less than the level's height away from crush depth
1133  //(i.e. the crush depth is pretty close to the spawn pos, possibly inside the level or at least close enough that many parts of the abyss are unreachable)
1134  else if ((spawnDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth)
1135  {
1136  iconCount++;
1137  crushDepthWarningIconStyle = "CrushDepthWarningLowIcon";
1138  tooltip = "crushdepthwarninglow";
1139  }
1140 
1141  if (connection.LevelData.HasBeaconStation)
1142  {
1143  bool beaconActive =
1144  connection.LevelData.IsBeaconActive ||
1145  (Level.Loaded?.LevelData == connection.LevelData && Level.Loaded.CheckBeaconActive());
1146  var beaconStationIconStyle = beaconActive ? "BeaconStationActive" : "BeaconStationInactive";
1147  DrawIcon(beaconStationIconStyle, (int)(28 * zoom), beaconActive ? beaconStationActiveText : beaconStationInactiveText);
1148  }
1149 
1150  if (connection.Locked)
1151  {
1152  var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1];
1153  var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction);
1154 
1155  if (unlockEvent != null)
1156  {
1157  Reputation unlockReputation = CurrentLocation.Reputation;
1158  Faction unlockFaction = null;
1159  if (!unlockEvent.Faction.IsEmpty)
1160  {
1161  unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction);
1162  unlockReputation = unlockFaction?.Reputation;
1163  }
1164  if (unlockReputation != null)
1165  {
1166  DrawIcon(
1167  "LockedLocationConnection", (int)(28 * zoom),
1168  RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip",
1169  ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)),
1170  ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true)))));
1171  }
1172  }
1173  else
1174  {
1175  DrawIcon("LockedLocationConnection", (int)(28 * zoom), TextManager.Get("LockedPathTooltip"));
1176  }
1177 
1178  }
1179 
1180  if (connection.LevelData.HasHuntingGrounds)
1181  {
1182  DrawIcon("HuntingGrounds", (int)(28 * zoom), RichString.Rich(TextManager.Get("HuntingGroundsTooltip")));
1183  }
1184 
1185  if (crushDepthWarningIconStyle != null)
1186  {
1187  DrawIcon(crushDepthWarningIconStyle, (int)(32 * zoom),
1188  RichString.Rich(TextManager.GetWithVariables(tooltip,
1189  ("[initialdepth]", $"‖color:gui.orange‖{(int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)}‖end‖"),
1190  ("[submarinecrushdepth]", $"‖color:gui.orange‖{(int)subCrushDepth}‖end‖"))));
1191  }
1192  }
1193 
1194  if (GameMain.DebugDraw && zoom > (1.0f * GUI.Scale) && generationParams.ShowLevelTypeNames)
1195  {
1196  Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom;
1197  if (viewArea.Contains(center) && connection.Biome != null)
1198  {
1199  GUI.DrawString(spriteBatch, center, (connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier) + " (" + connection.Difficulty.FormatSingleDecimal() + ")", Color.White);
1200  }
1201  }
1202 
1203  void DrawIcon(string iconStyle, int iconSize, RichString tooltipText)
1204  {
1205  Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2;
1206  Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize;
1207 
1208  iconPos += (iconDiff * -(iconCount - 1) / 2.0f) + iconDiff * iconIndex;
1209 
1210  var style = GUIStyle.GetComponentStyle(iconStyle);
1211  bool mouseOn = Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize && IsPreferredTooltip(iconPos);
1212  Sprite iconSprite = style.GetDefaultSprite();
1213  iconSprite.Draw(spriteBatch, iconPos, (mouseOn ? style.HoverColor : style.Color) * 0.7f,
1214  scale: iconSize / iconSprite.size.X);
1215  if (mouseOn)
1216  {
1217  tooltip = (new Rectangle((iconPos - Vector2.One * iconSize / 2).ToPoint(), new Point(iconSize)), tooltipText);
1218  }
1219  iconIndex++;
1220  }
1221  }
1222 
1223  private bool IsPreferredTooltip(Vector2 tooltipPos)
1224  {
1225  return tooltip == null || Vector2.DistanceSquared(tooltipPos, PlayerInput.MousePosition) < Vector2.DistanceSquared(tooltip.Value.targetArea.Center.ToVector2(), PlayerInput.MousePosition);
1226  }
1227 
1228  private float hudVisibility;
1229  private float cameraNoiseStrength;
1230 
1231  private void DrawDecorativeHUD(SpriteBatch spriteBatch, Rectangle rect)
1232  {
1233  generationParams.DecorativeGraphSprite.Draw(spriteBatch, (int)((Timing.TotalTime * 5.0f) % generationParams.DecorativeGraphSprite.FrameCount),
1234  new Vector2(rect.X, rect.Bottom - (generationParams.DecorativeGraphSprite.FrameSize.Y + 30) * GUI.Scale),
1235  Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale, SpriteEffects.FlipVertically);
1236 
1237  GUI.DrawString(spriteBatch,
1238  new Vector2(rect.Right - GUI.IntScale(170), rect.Y + GUI.IntScale(5)),
1239  "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont);
1240  GUI.DrawString(spriteBatch,
1241  new Vector2(rect.X + GUI.IntScale(5), rect.Y + GUI.IntScale(5)),
1242  "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont);
1243  }
1244 
1245  private void UpdateMapAnim(MapAnim anim, float deltaTime)
1246  {
1247  //pause animation while there are messageboxes (other than hints) on screen
1248  if (GUIMessageBox.MessageBoxes.Count(c => !(c is GUIMessageBox mb) || mb.MessageBoxType != GUIMessageBox.Type.Hint) > 0) { return; }
1249 
1250  if (!string.IsNullOrEmpty(anim.StartMessage))
1251  {
1252  new GUIMessageBox("", anim.StartMessage);
1253  anim.StartMessage = null;
1254  return;
1255  }
1256 
1257  float unscaledZoom = zoom / GUI.Scale;
1258  if (anim.StartZoom == null) { anim.StartZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, unscaledZoom); }
1259  if (anim.EndZoom == null) { anim.EndZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, unscaledZoom); }
1260 
1261  anim.StartPos = (anim.StartLocation == null) ? -DrawOffset : anim.StartLocation.MapPosition;
1262 
1263  anim.Timer = Math.Min(anim.Timer + deltaTime, anim.Duration);
1264  float t = anim.Duration <= 0.0f ? 1.0f : Math.Max(anim.Timer / anim.Duration, 0.0f);
1265  DrawOffset = -Vector2.SmoothStep(anim.StartPos.Value, anim.EndLocation.MapPosition, t);
1266  DrawOffset += new Vector2(
1267  (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.3f % 255, Timing.TotalTime * 0.4f % 255, 0) - 0.5f,
1268  (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.4f % 255, Timing.TotalTime * 0.3f % 255, 0.5f) - 0.5f) * 50.0f * (float)Math.Sin(t * MathHelper.Pi);
1269 
1270  zoom =
1271  MathHelper.Lerp(generationParams.MinZoom, generationParams.MaxZoom,
1272  MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t))
1273  * GUI.Scale;
1274 
1275  if (anim.Timer >= anim.Duration)
1276  {
1277  if (!string.IsNullOrEmpty(anim.EndMessage))
1278  {
1279  new GUIMessageBox("", anim.EndMessage);
1280  anim.EndMessage = null;
1281  return;
1282  }
1283  anim.Finished = true;
1284  }
1285  }
1286 
1290  public void ResetPendingSub()
1291  {
1292  pendingSubInfo = new SubmarineInfo.PendingSubInfo();
1293  }
1294 
1295  partial void RemoveProjSpecific()
1296  {
1297  noiseOverlay?.Remove();
1298  noiseOverlay = null;
1299  }
1300  }
1301 }
static bool AllowedToManageCampaign(ClientPermissions permissions)
There is a server-side implementation of the method in MultiPlayerCampaign
Location GetCurrentDisplayLocation()
The location that's displayed as the "current one" in the map screen. Normally the current outpost or...
FactionPrefab Prefab
Definition: Factions.cs:18
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual Rectangle Rect
void DrawToolTip(SpriteBatch spriteBatch)
Creates and draws a tooltip.
RectTransform RectTransform
GUIComponent that can be used to render custom content on the UI
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
static int GraphicsHeight
Definition: GameMain.cs:168
static bool DebugDraw
Definition: GameMain.cs:29
static GameClient Client
Definition: GameMain.cs:188
static GameMain Instance
Definition: GameMain.cs:144
LevelGenerationParams GenerationParams
Definition: LevelData.cs:27
readonly Point Size
Definition: LevelData.cs:52
readonly int InitialDepth
The depth at which the level starts at, in in-game coordinates. E.g. if this was set to 100 000 (= 10...
Definition: LevelData.cs:57
void DebugSetEndLocation(Location newEndLocation)
void DebugSetStartLocation(Location newStartLocation)
readonly List< Vector2[]> CrackSegments
int TimeSinceLastTypeChange
Definition: Location.cs:510
readonly List< LocationConnection > Connections
Definition: Location.cs:56
LocationType Type
Definition: Location.cs:91
bool IsGateBetweenBiomes
Definition: Location.cs:512
bool IsCriticallyRadiated()
Definition: Location.cs:1069
IEnumerable< Mission > AvailableMissions
Definition: Location.cs:432
Reputation Reputation
Definition: Location.cs:103
string LastTypeChangeMessage
Definition: Location.cs:508
Vector2 MapPosition
Definition: Location.cs:89
Faction SecondaryFaction
Definition: Location.cs:101
readonly ImmutableDictionary< Identifier, ImmutableArray< Sprite > > MapTiles
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
void Update(CampaignMode campaign, float deltaTime, GUICustomComponent mapContainer)
void DrawNotifications(SpriteBatch spriteBatch, GUICustomComponent container)
List< LocationConnection > Connections
static void DrawNoise(SpriteBatch spriteBatch, Rectangle rect, float strength)
void Draw(CampaignMode campaign, SpriteBatch spriteBatch, GUICustomComponent mapContainer)
void ResetPendingSub()
Resets pendingSubInfo and forces crush depth to be calculated again for icon displaying purposes
static bool KeyDown(InputType inputType)
readonly Identifier Identifier
Definition: Prefab.cs:34
Point ScreenSpaceOffset
Screen space offset. From top left corner. In pixels.
Point NonScaledSize
Size before scale multiplications.
Reputation(CampaignMetadata metadata, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation)
Definition: Reputation.cs:128
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, float rotation=0f, Vector2? origin=null, Color? color=null, Vector2? startOffset=null, Vector2? textureScale=null, float? depth=null, SpriteEffects? spriteEffects=null)
readonly record struct PendingSubInfo(SubmarineInfo PendingSub=null, bool StructuresDefineRealWorldCrushDepth=false, float RealWorldCrushDepth=Level.DefaultRealWorldCrushDepth)