Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/EventManager.cs
3 using FarseerPhysics;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma
11 {
12  partial class EventManager
13  {
14  public enum NetworkEventType
15  {
16  CONVERSATION,
17  CONVERSATION_SELECTED_OPTION,
18  STATUSEFFECT,
19  MISSION,
20  UNLOCKPATH,
21  EVENTLOG,
22  EVENTOBJECTIVE,
23  }
24 
26  public readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text) : INetSerializableStruct;
27 
29  public readonly record struct NetEventObjective(
30  EventObjectiveAction.SegmentActionType Type,
31  Identifier Identifier,
32  Identifier ObjectiveTag,
33  Identifier TextTag,
34  Identifier ParentObjectiveId,
35  bool CanBeCompleted) : INetSerializableStruct;
36 
37 
38  const float IntensityUpdateInterval = 5.0f;
39 
40  const float CalculateDistanceTraveledInterval = 5.0f;
41 
42  const int MaxEventHistory = 20;
43 
44  private Level level;
45 
46  private readonly List<Sprite> preloadedSprites = new List<Sprite>();
47 
48  //The "intensity" of the current situation (a value between 0.0 - 1.0).
49  //High when a disaster has struck, low when nothing special is going on.
50  private float currentIntensity;
51  //The exact intensity of the current situation, current intensity is lerped towards this value
52  private float targetIntensity;
53  //follows targetIntensity a bit faster than currentIntensity to prevent e.g. combat musing staying on very long after the monsters are dead
54  private float musicIntensity;
55 
56  //How low the intensity has to be for an event to be triggered.
57  //Gradually increases with time, so additional problems can still appear eventually even if
58  //the sub is laying broken on the ocean floor or if the players are trying to abuse the system
59  //by intentionally keeping the intensity high by causing breaches, damaging themselves or such
60  private float eventThreshold = 0.2f;
61 
62  //New events can't be triggered when the cooldown is active.
63  private float eventCoolDown;
64 
65  private float intensityUpdateTimer;
66 
67  private PathFinder pathFinder;
68  private float totalPathLength;
69  private float calculateDistanceTraveledTimer;
70  private float distanceTraveled;
71 
72  private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterStrength;
77 
78  private float roundDuration;
79 
80  private bool isCrewAway;
81  //how long it takes after the crew returns for the event manager to resume normal operation
82  const float CrewAwayResetDelay = 60.0f;
83  private float crewAwayResetTimer;
84  private float crewAwayDuration;
85 
86  private readonly List<EventSet> pendingEventSets = new List<EventSet>();
87 
88  private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
89 
90  private readonly List<Event> activeEvents = new List<Event>();
91 
92  private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
93  private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
94 
95 
96 #if DEBUG && SERVER
97  private DateTime nextIntensityLogTime;
98 #endif
99 
100  private EventManagerSettings settings;
101 
102  private readonly bool isClient;
103 
104  public float CurrentIntensity
105  {
106  get { return currentIntensity; }
107  }
108  public float MusicIntensity
109  {
110  get { return musicIntensity; }
111  }
112 
113  public IEnumerable<Event> ActiveEvents
114  {
115  get { return activeEvents; }
116  }
117 
118  public readonly Queue<Event> QueuedEvents = new Queue<Event>();
119 
120  public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
121 
122  private readonly struct TimeStamp
123  {
124  public readonly double Time;
125  public readonly Event Event;
126 
127  public TimeStamp(Event e)
128  {
129  Event = e;
130  Time = Timing.TotalTime;
131  }
132  }
133 
134  private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
135  public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
136 
137  public readonly EventLog EventLog = new EventLog();
138 
139  public EventManager()
140  {
141  isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
142  }
143 
144  public bool Enabled = true;
145 
146  private MTRandom random;
147  public int RandomSeed { get; private set; }
148 
149  public void StartRound(Level level)
150  {
151  this.level = level;
152 
153  if (isClient) { return; }
154 
155  timeStamps.Clear();
156  pendingEventSets.Clear();
157  selectedEvents.Clear();
158  activeEvents.Clear();
159 #if SERVER
160  MissionAction.ResetMissionsUnlockedThisRound();
161 #endif
162  pathFinder = new PathFinder(WayPoint.WayPointList, false);
163  totalPathLength = 0.0f;
164  if (level != null)
165  {
166  var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(level.StartPosition), ConvertUnits.ToSimUnits(level.EndPosition));
167  totalPathLength = steeringPath.TotalLength;
168  }
169 
170  SelectSettings();
171 
172  if (level != null)
173  {
174  RandomSeed = ToolBox.StringToInt(level.Seed);
175  foreach (var previousEvent in level.LevelData.EventHistory)
176  {
177  RandomSeed ^= ToolBox.IdentifierToInt(previousEvent);
178  }
179  }
180  random = new MTRandom(RandomSeed);
181 
182  bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
183  EventSet initialEventSet = SelectRandomEvents(
184  EventSet.Prefabs.ToList(),
185  requireCampaignSet: playingCampaign,
186  random: random);
187  EventSet additiveSet = null;
188  if (initialEventSet != null && initialEventSet.Additive)
189  {
190  additiveSet = initialEventSet;
191  initialEventSet = SelectRandomEvents(
192  EventSet.Prefabs.Where(e => !e.Additive).ToList(),
193  requireCampaignSet: playingCampaign,
194  random: random);
195  }
196  if (initialEventSet != null)
197  {
198  pendingEventSets.Add(initialEventSet);
199  CreateEvents(initialEventSet);
200  }
201  if (additiveSet != null)
202  {
203  pendingEventSets.Add(additiveSet);
204  CreateEvents(additiveSet);
205  }
206 
207  if (level?.LevelData != null)
208  {
209  if (level.LevelData.Type == LevelData.LevelType.Outpost)
210  {
211  //if the outpost is connected to a locked connection, create an event to unlock it
212  if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
213  {
214  var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
215  if (unlockPathEventPrefab != null)
216  {
217  var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
218  activeEvents.Add(newEvent);
219  }
220  else
221  {
222  //if no event that unlocks the path can be found, unlock it automatically
223  level.StartLocation.Connections.ForEach(c => c.Locked = false);
224  }
225  }
226  }
227  RegisterNonRepeatableChildEvents(initialEventSet);
228  void RegisterNonRepeatableChildEvents(EventSet eventSet)
229  {
230  if (eventSet == null) { return; }
231  if (eventSet.OncePerLevel)
232  {
233  foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
234  {
235  nonRepeatableEvents.Add(ep.Identifier);
236  }
237  }
238  foreach (EventSet childSet in eventSet.ChildSets)
239  {
240  RegisterNonRepeatableChildEvents(childSet);
241  }
242  }
243  }
244 
245  while (QueuedEventsForNextRound.TryDequeue(out var id))
246  {
247  var eventPrefab = EventSet.GetEventPrefab(id);
248  if (eventPrefab == null)
249  {
250  DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}.");
251  continue;
252  }
253  var ev = eventPrefab.CreateInstance(RandomSeed);
254  if (ev != null)
255  {
256  QueuedEvents.Enqueue(ev);
257  }
258  }
259 
261 
262  roundDuration = 0.0f;
263  eventsInitialized = false;
264  isCrewAway = false;
265  crewAwayDuration = 0.0f;
266  crewAwayResetTimer = 0.0f;
267  intensityUpdateTimer = 0.0f;
268  CalculateCurrentIntensity(0.0f);
269  currentIntensity = musicIntensity = targetIntensity;
270  eventCoolDown = 0.0f;
275  distanceTraveled = 0;
276  }
277 
278  public void ActivateEvent(Event newEvent)
279  {
280  activeEvents.Add(newEvent);
281  newEvent.Init();
282  }
283 
284  public void ClearEvents()
285  {
286  activeEvents.Clear();
287  }
288 
289  private void SelectSettings()
290  {
291  if (!EventManagerSettings.Prefabs.Any())
292  {
293  throw new InvalidOperationException("Could not select EventManager settings (no settings loaded).");
294  }
295  var orderedByDifficulty = EventManagerSettings.OrderedByDifficulty.ToArray();
296  if (level == null)
297  {
298 #if CLIENT
299  if (GameMain.GameSession.GameMode is TestGameMode)
300  {
301  settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient);
302  if (settings != null)
303  {
304  eventThreshold = settings.DefaultEventThreshold;
305  }
306  return;
307  }
308 #endif
309  throw new InvalidOperationException("Could not select EventManager settings (level not set).");
310  }
311 
312  float extraDifficulty = 0;
313  if (GameMain.GameSession.Campaign?.Settings != null)
314  {
315  extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty;
316  }
317  float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100);
318  var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s =>
319  modifiedDifficulty >= s.MinLevelDifficulty &&
320  modifiedDifficulty <= s.MaxLevelDifficulty).ToArray();
321 
322  if (suitableSettings.Length == 0)
323  {
324  DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")");
325  settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient);
326  }
327  else
328  {
329  settings = suitableSettings.GetRandom(Rand.RandSync.ServerAndClient);
330  }
331  if (settings != null)
332  {
333  eventThreshold = settings.DefaultEventThreshold;
334  }
335  }
336 
337  public IEnumerable<ContentFile> GetFilesToPreload()
338  {
339  foreach (List<Event> eventList in selectedEvents.Values)
340  {
341  foreach (Event ev in eventList)
342  {
343  foreach (ContentFile contentFile in ev.GetFilesToPreload())
344  {
345  yield return contentFile;
346  }
347  }
348  }
349  }
350 
351  public void PreloadContent(IEnumerable<ContentFile> contentFiles)
352  {
353  var filesToPreload = contentFiles.ToList();
354  foreach (Submarine sub in Submarine.Loaded)
355  {
356  if (sub.WreckAI == null) { continue; }
357 
358  if (!sub.WreckAI.Config.DefensiveAgent.IsEmpty)
359  {
361  if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath))
362  {
363  filesToPreload.Add(prefab.ContentFile);
364  }
365  }
366  foreach (Item item in Item.ItemList)
367  {
368  if (item.Submarine != sub) { continue; }
369  foreach (Items.Components.ItemComponent component in item.Components)
370  {
371  if (component.statusEffectLists == null) { continue; }
372  foreach (var statusEffectList in component.statusEffectLists.Values)
373  {
374  foreach (StatusEffect statusEffect in statusEffectList)
375  {
376  foreach (var spawnInfo in statusEffect.SpawnCharacters)
377  {
378  var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName);
379  if (prefab != null && !filesToPreload.Contains(prefab.ContentFile))
380  {
381  filesToPreload.Add(prefab.ContentFile);
382  }
383  }
384  }
385  }
386  }
387  }
388  }
389 
390  foreach (ContentFile file in filesToPreload)
391  {
392  file.Preload(preloadedSprites.Add);
393  }
394  }
395 
397  {
398  foreach (var ev in activeEvents)
399  {
400  (ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
401  }
402  }
403 
404  public void EndRound()
405  {
406  pendingEventSets.Clear();
407  selectedEvents.Clear();
408  activeEvents.Clear();
409  QueuedEvents.Clear();
410  finishedEvents.Clear();
411  nonRepeatableEvents.Clear();
412 
413  preloadedSprites.ForEach(s => s.Remove());
414  preloadedSprites.Clear();
415 
416  timeStamps.Clear();
417 
418  pathFinder = null;
419  }
420 
424  public void RegisterEventHistory(bool registerFinishedOnly = false)
425  {
426  if (level?.LevelData == null) { return; }
427 
428  level.LevelData.EventsExhausted = !registerFinishedOnly;
429 
430  if (level.LevelData.Type == LevelData.LevelType.Outpost)
431  {
432  if (registerFinishedOnly)
433  {
434  foreach (var finishedEvent in finishedEvents)
435  {
436  EventSet parentSet = finishedEvent.ParentSet;
437  if (parentSet == null) { continue; }
438  if (parentSet.Exhaustible)
439  {
440  level.LevelData.EventsExhausted = true;
441  }
442  if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1))
443  {
444  level.LevelData.FinishedEvents[parentSet] += 1;
445  }
446  }
447  }
448 
449  level.LevelData.EventHistory.AddRange(selectedEvents.Values
450  .SelectMany(v => v)
451  .Select(e => e.Prefab.Identifier)
452  .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
453 
454  if (level.LevelData.EventHistory.Count > MaxEventHistory)
455  {
456  level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
457  }
458  }
459  level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
460 
461  if (!registerFinishedOnly)
462  {
463  level.LevelData.FinishedEvents.Clear();
464  }
465 
466  bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
467  }
468 
469  public void SkipEventCooldown()
470  {
471  eventCoolDown = 0.0f;
472  }
473 
474  private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness)
475  {
476  if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; }
477  float retVal = baseCommonness;
478  if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; }
479  return retVal;
480  }
481 
482  private void CreateEvents(EventSet eventSet)
483  {
484  selectedEvents.Remove(eventSet);
485  if (level == null) { return; }
486  if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
487  if (eventSet.Exhaustible && level.LevelData.EventsExhausted) { return; }
488 
489  DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
490 
491  int applyCount = 1;
492  List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
493  if (eventSet.PerRuin)
494  {
495  applyCount = level.Ruins.Count;
496  foreach (var ruin in level.Ruins)
497  {
498  spawnPosFilter.Add(pos => pos.Ruin == ruin);
499  }
500  }
501  else if (eventSet.PerCave)
502  {
503  applyCount = level.Caves.Count;
504  foreach (var cave in level.Caves)
505  {
506  spawnPosFilter.Add(pos => pos.Cave == cave);
507  }
508  }
509  else if (eventSet.PerWreck)
510  {
511  var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
512  applyCount = wrecks.Count();
513  foreach (var wreck in wrecks)
514  {
515  spawnPosFilter.Add(pos => pos.Submarine == wreck);
516  }
517  }
518 
519  foreach (var subEventPrefab in eventSet.EventPrefabs)
520  {
521  foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers())
522  {
523  DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".",
524  contentPackage: eventSet.ContentPackage);
525  }
526  }
527 
528  var suitablePrefabSubsets = eventSet.EventPrefabs.Where(
529  e => IsFactionSuitable(e.Faction, level) && e.EventPrefabs.Any(ep => IsSuitable(ep, level))).ToArray();
530 
531  for (int i = 0; i < applyCount; i++)
532  {
533  if (eventSet.ChooseRandom)
534  {
535  if (suitablePrefabSubsets.Any())
536  {
537  var unusedEvents = suitablePrefabSubsets.ToList();
538  int eventCount = eventSet.GetEventCount(level);
539  for (int j = 0; j < eventCount; j++)
540  {
541  if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; }
542  EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random);
543  (IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) = subEventPrefab;
544  if (eventPrefabs != null && random.NextDouble() <= probability)
545  {
546  var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random);
547  var newEvent = eventPrefab.CreateInstance(RandomSeed);
548  if (newEvent == null) { continue; }
549  if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
550  DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
551  if (!selectedEvents.ContainsKey(eventSet))
552  {
553  selectedEvents.Add(eventSet, new List<Event>());
554  }
555  selectedEvents[eventSet].Add(newEvent);
556  unusedEvents.Remove(subEventPrefab);
557  }
558  }
559  }
560  if (eventSet.ChildSets.Any())
561  {
562  int setCount = eventSet.SubSetCount;
563  if (setCount > 1)
564  {
565  var unusedSets = eventSet.ChildSets.ToList();
566  for (int j = 0; j < setCount; j++)
567  {
568  var newEventSet = SelectRandomEvents(unusedSets, random: random);
569  if (newEventSet == null) { break; }
570  unusedSets.Remove(newEventSet);
571  CreateEvents(newEventSet);
572  }
573  }
574  else
575  {
576  var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
577  if (newEventSet != null)
578  {
579  CreateEvents(newEventSet);
580  }
581  }
582  }
583  }
584  else
585  {
586  foreach ((IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) in suitablePrefabSubsets)
587  {
588  if (random.NextDouble() > probability) { continue; }
589 
590  var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random);
591  var newEvent = eventPrefab.CreateInstance(RandomSeed);
592  if (newEvent == null) { continue; }
593  if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
594  if (!selectedEvents.ContainsKey(eventSet))
595  {
596  selectedEvents.Add(eventSet, new List<Event>());
597  }
598  selectedEvents[eventSet].Add(newEvent);
599  }
600 
601  var location = GetEventLocation();
602  foreach (EventSet childEventSet in eventSet.ChildSets)
603  {
604  if (!IsValidForLevel(childEventSet, level)) { continue; }
605  if (!IsValidForLocation(childEventSet, location)) { continue; }
606  CreateEvents(childEventSet);
607  }
608  }
609  }
610  }
611 
612  private EventSet SelectRandomEvents(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
613  {
614  if (level == null) { return null; }
615  Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
616 
617  var allowedEventSets =
618  eventSets.Where(set => IsValidForLevel(set, level));
619 
620  if (requireCampaignSet.HasValue)
621  {
622  if (requireCampaignSet.Value)
623  {
624  if (allowedEventSets.Any(es => es.IsCampaignSet))
625  {
626  allowedEventSets =
627  allowedEventSets.Where(es => es.IsCampaignSet);
628  }
629  else
630  {
631  DebugConsole.AddWarning("No campaign event sets available. Using a non-campaign-specific set instead.");
632  }
633  }
634  else
635  {
636  allowedEventSets =
637  allowedEventSets.Where(es => !es.IsCampaignSet);
638  }
639  }
640 
641  var location = GetEventLocation();
642  allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
643 
644  allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly ||
645  (GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true }));
646 
647  int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location);
648  int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location);
649  if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex))
650  {
651  allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex);
652  }
653  else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex))
654  {
655  allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex);
656  }
657  else
658  {
659  // When there are no forced sets, only allow sets that aren't forced at any specific location
660  allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0);
661  }
662 
663  if (allowedEventSets.Count() == 1)
664  {
665  // When there's only a single set available, just select it directly
666  return allowedEventSets.First();
667  }
668 
669  float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
670  float randomNumber = (float)rand.NextDouble();
671  randomNumber *= totalCommonness;
672  foreach (EventSet eventSet in allowedEventSets)
673  {
674  float commonness = eventSet.GetCommonness(level);
675  if (randomNumber <= commonness)
676  {
677  return eventSet;
678  }
679  randomNumber -= commonness;
680  }
681 
682  return null;
683  }
684 
685  public static bool IsSuitable(EventPrefab e, Level level)
686  {
687  return IsLevelSuitable(e, level) && IsFactionSuitable(e.Faction, level);
688  }
689 
690  public static bool IsLevelSuitable(EventPrefab e, Level level)
691  {
692  return
693  (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
695  !level.LevelData.NonRepeatableEvents.Contains(e.Identifier);
696  }
697 
698  private static bool IsFactionSuitable(Identifier factionId, Level level)
699  {
700  return factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier;
701  }
702 
703  private static bool IsValidForLevel(EventSet eventSet, Level level)
704  {
705  return
706  level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
707  level.LevelData.Type == eventSet.LevelType &&
708  (eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
709  (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
710  }
711 
712  private bool IsValidForLocation(EventSet eventSet, Location location)
713  {
714  if (location is null) { return true; }
715  if (!eventSet.Faction.IsEmpty)
716  {
717  if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; }
718  }
719  var locationType = location.GetLocationType();
720  bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
721  if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
722  return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier);
723  }
724 
725  private Location GetEventLocation()
726  {
727  return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation;
728  }
729 
730  private bool CanStartEventSet(EventSet eventSet)
731  {
732  if (!eventSet.AllowAtStart)
733  {
734  ISpatialEntity refEntity = GetRefEntity();
735  float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint()));
736  float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint()));
737  if (distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f)
738  {
739  return false;
740  }
741  }
742 
743  if (eventSet.DelayWhenCrewAway)
744  {
745  if ((isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) || crewAwayResetTimer > 0.0f)
746  {
747  return false;
748  }
749  }
750 
751  if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
752  roundDuration < eventSet.MinMissionTime)
753  {
754  return false;
755  }
756 
757  if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
758  {
759  return false;
760  }
761 
762  return true;
763  }
764 
765  private bool eventsInitialized;
766 
767  public void Update(float deltaTime)
768  {
769  if (!Enabled) { return; }
770  if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; }
771 
772  if (!eventsInitialized)
773  {
774  foreach (var eventSet in selectedEvents.Keys)
775  {
776  foreach (var ev in selectedEvents[eventSet])
777  {
778  ev.Init(eventSet);
779  }
780  }
781  eventsInitialized = true;
782  }
783 
784  //clients only calculate the intensity but don't create any events
785  //(the intensity is used for controlling the background music)
786  CalculateCurrentIntensity(deltaTime);
787 
788 #if DEBUG && SERVER
789  if (DateTime.Now > nextIntensityLogTime)
790  {
791  DebugConsole.NewMessage("EventManager intensity: " + (int)Math.Round(currentIntensity * 100) + " %");
792  nextIntensityLogTime = DateTime.Now + new TimeSpan(0, minutes: 1, seconds: 0);
793  }
794 #endif
795 
796  if (isClient) { return; }
797 
798  roundDuration += deltaTime;
799 
800  if (settings == null)
801  {
802  DebugConsole.ThrowError("Event settings not set before updating EventManager. Attempting to select...");
803  SelectSettings();
804  if (settings == null)
805  {
806  DebugConsole.ThrowError("Could not select EventManager settings. Disabling EventManager for the round...");
807 #if SERVER
808  GameMain.Server?.SendChatMessage("Could not select EventManager settings. Disabling EventManager for the round...", Networking.ChatMessageType.Error);
809 #endif
810  Enabled = false;
811  return;
812  }
813  }
814 
815  if (IsCrewAway())
816  {
817  isCrewAway = true;
818  crewAwayResetTimer = CrewAwayResetDelay;
819  crewAwayDuration += deltaTime;
820  }
821  else if (crewAwayResetTimer > 0.0f)
822  {
823  isCrewAway = false;
824  crewAwayResetTimer -= deltaTime;
825  }
826  else
827  {
828  isCrewAway = false;
829  crewAwayDuration = 0.0f;
830  eventThreshold += settings.EventThresholdIncrease * deltaTime;
831  eventThreshold = Math.Min(eventThreshold, 1.0f);
832  eventCoolDown -= deltaTime;
833  }
834 
835  calculateDistanceTraveledTimer -= deltaTime;
836  if (calculateDistanceTraveledTimer <= 0.0f)
837  {
838  distanceTraveled = CalculateDistanceTraveled();
839  calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
840  }
841 
842  bool recheck = false;
843  do
844  {
845  recheck = false;
846  //activate pending event sets that can be activated
847  for (int i = pendingEventSets.Count - 1; i >= 0; i--)
848  {
849  var eventSet = pendingEventSets[i];
850  if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
851  if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
852  if (!CanStartEventSet(eventSet)) { continue; }
853 
854  pendingEventSets.RemoveAt(i);
855 
856  if (selectedEvents.ContainsKey(eventSet))
857  {
858  //start events in this set
859  foreach (Event ev in selectedEvents[eventSet])
860  {
861  activeEvents.Add(ev);
862  eventThreshold = settings.DefaultEventThreshold;
863  if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
864  {
865  eventCoolDown = settings.EventCooldown;
866  }
867  if (eventSet.ResetTime > 0)
868  {
869  ev.Finished += () =>
870  {
871  pendingEventSets.Add(eventSet);
872  CreateEvents(eventSet);
873  foreach (Event newEvent in selectedEvents[eventSet])
874  {
875  if (!newEvent.Initialized) { newEvent.Init(eventSet); }
876  }
877  };
878  }
879  }
880  }
881 
882  //add child event sets to pending
883  foreach (EventSet childEventSet in eventSet.ChildSets)
884  {
885  pendingEventSets.Add(childEventSet);
886  recheck = true;
887  }
888  }
889  } while (recheck);
890 
891  foreach (Event ev in activeEvents)
892  {
893  if (!ev.IsFinished)
894  {
895  ev.Update(deltaTime);
896  }
897  else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
898  {
899  if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
900  {
901  if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
902  }
903  finishedEvents.Add(ev);
904  }
905  }
906 
907  if (QueuedEvents.Count > 0)
908  {
909  activeEvents.Add(QueuedEvents.Dequeue());
910  }
911  }
912 
913  public void EntitySpawned(Entity entity)
914  {
915  foreach (var ev in activeEvents)
916  {
917  if (ev is ScriptedEvent scriptedEvent)
918  {
919  scriptedEvent.EntitySpawned(entity);
920  }
921  }
922  }
923 
924  private void CalculateCurrentIntensity(float deltaTime)
925  {
926  intensityUpdateTimer -= deltaTime;
927  if (intensityUpdateTimer > 0.0f) { return; }
928  intensityUpdateTimer = IntensityUpdateInterval;
929 
930  // crew health --------------------------------------------------------
931 
932  avgCrewHealth = 0.0f;
933  int characterCount = 0;
934  foreach (Character character in Character.CharacterList)
935  {
936  if (character.IsDead || character.TeamID == CharacterTeamType.FriendlyNPC) { continue; }
937  if (character.AIController is HumanAIController || character.IsRemotePlayer)
938  {
939  avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f);
940  characterCount++;
941  }
942  }
943  if (characterCount > 0)
944  {
945  avgCrewHealth /= characterCount;
946  }
947  else
948  {
949  avgCrewHealth = 0.5f;
950  }
951 
952  // enemy amount --------------------------------------------------------
953 
954  enemyDanger = 0.0f;
955  monsterStrength = 0;
956  foreach (Character character in Character.CharacterList)
957  {
958  if (character.IsIncapacitated || character.IsHandcuffed || !character.Enabled || character.IsPet) { continue; }
959 
960  if (character.AIController is EnemyAIController enemyAI)
961  {
962  if (!enemyAI.AIParams.StayInAbyss)
963  {
964  // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
965  monsterStrength += enemyAI.CombatStrength;
966  }
967 
968  if (character.CurrentHull?.Submarine?.Info != null &&
969  (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
970  character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
971  {
972  // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
973  enemyDanger += enemyAI.CombatStrength / 500.0f;
974  }
975  else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
976  {
977  // Enemy outside targeting the sub or something in it
978  // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
979  enemyDanger += enemyAI.CombatStrength / 5000.0f;
980  }
981  }
982  else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
983  {
984  if (character.Submarine != null &&
985  character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
986  Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
987  {
988  //we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
989  //so let's just go with a fixed value.
990  //5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max
991  enemyDanger += 0.2f;
992  }
993  }
994  }
995 
996  // Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
997  // On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
998  // And if they get inside, we add 0.1 per crawler on that.
999  // So, in practice the danger per enemy that is attacking the sub is half of what it would be when the enemy is not targeting the sub.
1000  // 10 Crawlers -> +0.2 (0.4 in total if all target the sub from outside).
1001  // 5 Mudraptors -> +0.21 (0.42 in total, before they get inside).
1002  // 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub).
1003  // 2 Molochs -> +0.5 (1.0 in total, if both target the sub).
1004  enemyDanger += monsterStrength / 5000f;
1005  enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
1006 
1007  // The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle.
1008  // Some examples that result in the max intensity even when the creatures would just idle around.
1009  // The values are theoretical, because in practice many of the monsters are targeting the sub, which will double the danger of those monster and effectively halve the max monster count.
1010  // In practice we don't use the max intensity. For example on level 50 we use max intensity 50, which would mean that we'd halve the numbers below.
1011  // There's no hard cap for the monster count, but if the amount of monsters is higher than this, we don't spawn more monsters from the events:
1012  // 50 Crawlers (We shouldn't actually ever spawn that many. 12 is the max per event, but theoretically 25 crawlers would result in max intensity).
1013  // 25 Tigerthreshers (Max 9 per event. 12 targeting the sub at the same time results in max intensity).
1014  // 10 Hammerheads (Max 3 per event. 5 targeting the sub at the same time results in max intensity).
1015  // 4 Molochs (Max 2 per event and 2 targeting the sub at the same time results in max intensity).
1016 
1017  // hull status (gaps, flooding, fire) --------------------------------------------------------
1018 
1019  float holeCount = 0.0f;
1020  float waterAmount = 0.0f;
1021  float dryHullVolume = 0.0f;
1022  foreach (Hull hull in Hull.HullList)
1023  {
1024  if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
1025  if (GameMain.GameSession?.GameMode is PvPMode)
1026  {
1027  if (hull.Submarine.TeamID != CharacterTeamType.Team1 && hull.Submarine.TeamID != CharacterTeamType.Team2) { continue; }
1028  }
1029  else
1030  {
1031  if (hull.Submarine.TeamID != CharacterTeamType.Team1) { continue; }
1032  }
1033  fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
1034  if (hull.IsWetRoom) { continue; }
1035  foreach (Gap gap in hull.ConnectedGaps)
1036  {
1037  if (!gap.IsRoomToRoom)
1038  {
1039  holeCount += gap.Open;
1040  }
1041  }
1042  waterAmount += hull.WaterVolume;
1043  dryHullVolume += hull.Volume;
1044  }
1045  if (dryHullVolume > 0)
1046  {
1047  floodingAmount = waterAmount / dryHullVolume;
1048  }
1049 
1050  //hull integrity at 0.0 if there are 10 or more wide-open holes
1051  avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f);
1052 
1053  //a fire of any size bumps up the fire amount to 20%
1054  //if the total width of the fires is 1000 or more, the fire amount is considered to be at 100%
1055  fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f);
1056 
1057  //flooding less than 10% of the sub is ignored
1058  //to prevent ballast tanks from affecting the intensity
1059  if (floodingAmount < 0.1f)
1060  {
1061  floodingAmount = 0.0f;
1062  }
1063  else
1064  {
1065  floodingAmount *= 1.5f;
1066  }
1067 
1068  // calculate final intensity --------------------------------------------------------
1069 
1070  targetIntensity =
1071  ((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f;
1072  targetIntensity += fireAmount * 0.5f;
1073  targetIntensity += enemyDanger;
1074  targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f);
1075 
1076  if (targetIntensity > currentIntensity)
1077  {
1078  //25 seconds for intensity to go from 0.0 to 1.0
1079  currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
1080  //20 seconds for intensity to go from 0.0 to 1.0
1081  musicIntensity = Math.Min(musicIntensity + 0.05f * IntensityUpdateInterval, targetIntensity);
1082  }
1083  else
1084  {
1085  //400 seconds for intensity to go from 1.0 to 0.0
1086  currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity);
1087  //20 seconds for intensity to go from 1.0 to 0.0
1088  musicIntensity = Math.Max(musicIntensity - 0.05f * IntensityUpdateInterval, targetIntensity);
1089  }
1090  }
1091 
1092  private float CalculateDistanceTraveled()
1093  {
1094  if (level == null || pathFinder == null) { return 0.0f; }
1095  var refEntity = GetRefEntity();
1096  if (refEntity == null) { return 0.0f; }
1097  Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition);
1098  var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target);
1099  if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength))
1100  {
1101  //use horizontal position in the level as a fallback if a path can't be found
1102  return MathHelper.Clamp((refEntity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
1103  }
1104  else
1105  {
1106  return MathHelper.Clamp(1.0f - steeringPath.TotalLength / totalPathLength, 0.0f, 1.0f);
1107  }
1108  }
1109 
1115  {
1116  ISpatialEntity refEntity = Submarine.MainSub;
1117 #if CLIENT
1118  if (Character.Controlled != null)
1119  {
1120  if (Character.Controlled.Submarine != null &&
1122  {
1123  refEntity = Character.Controlled.Submarine;
1124  }
1125  else
1126  {
1127  refEntity = Character.Controlled;
1128  }
1129  }
1130 #else
1131  foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
1132  {
1133  if (client.Character == null) { continue; }
1134  //only take the players inside a player sub into account.
1135  //Otherwise the system could be abused by for example making a respawned player wait
1136  //close to the destination outpost
1137  if (client.Character.Submarine != null &&
1138  client.Character.Submarine.Info.Type == SubmarineType.Player)
1139  {
1140  if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X)
1141  {
1142  refEntity = client.Character.Submarine;
1143  }
1144  }
1145  }
1146 #endif
1147  return refEntity;
1148  }
1149 
1150  private bool IsCrewAway()
1151  {
1152 #if CLIENT
1153  return Character.Controlled != null && IsCharacterAway(Character.Controlled);
1154 #else
1155  int playerCount = 0;
1156  int awayPlayerCount = 0;
1157  foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
1158  {
1159  if (client.Character == null || client.Character.IsDead || client.Character.IsIncapacitated) { continue; }
1160 
1161  playerCount++;
1162  if (IsCharacterAway(client.Character)) { awayPlayerCount++; }
1163  }
1164  return playerCount > 0 && awayPlayerCount / (float)playerCount > 0.5f;
1165 #endif
1166  }
1167 
1168  private bool IsCharacterAway(Character character)
1169  {
1170  if (character.Submarine != null)
1171  {
1172  switch (character.Submarine.Info.Type)
1173  {
1174  case SubmarineType.Player:
1175  case SubmarineType.Outpost:
1176  case SubmarineType.OutpostModule:
1177  return false;
1178  case SubmarineType.Wreck:
1179  case SubmarineType.BeaconStation:
1180  case SubmarineType.Ruin:
1181  return true;
1182  }
1183  }
1184 
1185  const int maxDist = 1000;
1186 
1187  if (level != null && !level.Removed)
1188  {
1189  foreach (var ruin in level.Ruins)
1190  {
1191  Rectangle area = ruin.Area;
1192  area.Inflate(maxDist, maxDist);
1193  if (area.Contains(character.WorldPosition)) { return true; }
1194  }
1195  foreach (var cave in level.Caves)
1196  {
1197  Rectangle area = cave.Area;
1198  area.Inflate(maxDist, maxDist);
1199  if (area.Contains(character.WorldPosition)) { return true; }
1200  }
1201  }
1202 
1203  foreach (Submarine sub in Submarine.Loaded)
1204  {
1205  if (sub.Info.Type != SubmarineType.BeaconStation && sub.Info.Type != SubmarineType.Wreck) { continue; }
1206  Rectangle worldBorders = new Rectangle(
1207  sub.Borders.X + (int)sub.WorldPosition.X - maxDist,
1208  sub.Borders.Y + (int)sub.WorldPosition.Y + maxDist,
1209  sub.Borders.Width + maxDist * 2,
1210  sub.Borders.Height + maxDist * 2);
1211  if (Submarine.RectContains(worldBorders, character.WorldPosition))
1212  {
1213  return true;
1214  }
1215  }
1216 
1217  return false;
1218  }
1219 
1220  public void Load(XElement element)
1221  {
1222  foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty<Identifier>()))
1223  {
1224  QueuedEventsForNextRound.Enqueue(id);
1225  }
1226  }
1227 
1228  public XElement Save()
1229  {
1230  return new XElement("eventmanager",
1231  new XAttribute(nameof(QueuedEventsForNextRound),
1232  string.Join(',', QueuedEventsForNextRound)));
1233  }
1234  }
1235 }
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
Base class for content file types, which are loaded from filelist.xml via reflection....
Definition: ContentFile.cs:23
virtual void Preload(Action< Sprite > addPreloadedSprite)
Definition: ContentFile.cs:143
Submarine Submarine
Definition: Entity.cs:53
bool Initialized
Definition: Event.cs:20
Action Finished
Definition: Event.cs:9
virtual void Update(float deltaTime)
Definition: Event.cs:69
bool IsFinished
Definition: Event.cs:25
Func< Level.InterestingPosition, bool > SpawnPosFilter
Definition: Event.cs:22
EventPrefab Prefab
Definition: Event.cs:16
virtual IEnumerable< ContentFile > GetFilesToPreload()
Definition: Event.cs:48
void Init(EventSet parentSet=null)
Definition: Event.cs:53
Used to store logs of scripted events (a sort of "quest log")
static bool IsLevelSuitable(EventPrefab e, Level level)
static ISpatialEntity GetRefEntity()
Get the entity that should be used in determining how far the player has progressed in the level....
static bool IsSuitable(EventPrefab e, Level level)
void AddTimeStamp(Event e)
void PreloadContent(IEnumerable< ContentFile > contentFiles)
readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text)[NetworkSerialize] readonly record struct const NetEventObjective(EventObjectiveAction.SegmentActionType Type, Identifier Identifier, Identifier ObjectiveTag, Identifier TextTag, Identifier ParentObjectiveId, bool CanBeCompleted) float IntensityUpdateInterval
void RegisterEventHistory(bool registerFinishedOnly=false)
Registers the exhaustible events in the level as exhausted, and adds the current events to the event ...
static readonly PrefabCollection< EventManagerSettings > Prefabs
readonly Identifier BiomeIdentifier
If set, the event set can only be chosen in this biome.
Definition: EventPrefab.cs:33
readonly Identifier RequiredLayer
If set, this layer must be present somewhere in the level.
Definition: EventPrefab.cs:38
Event CreateInstance(int seed)
Definition: EventPrefab.cs:115
static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction)
Definition: EventPrefab.cs:138
readonly Identifier Faction
If set, the event set can only be chosen in locations that belong to this faction.
Definition: EventPrefab.cs:43
Event sets are sets of random events that occur within a level (most commonly, monster spawns and scr...
Definition: EventSet.cs:31
static EventPrefab GetEventPrefab(Identifier identifier)
Definition: EventSet.cs:80
readonly ImmutableArray< SubEventPrefab > EventPrefabs
Definition: EventSet.cs:298
static readonly PrefabCollection< EventSet > Prefabs
Definition: EventSet.cs:44
readonly ImmutableArray< EventSet > ChildSets
Definition: EventSet.cs:300
readonly bool Additive
Additive sets are important to be aware of when creating custom event sets! If an additive set gets c...
Definition: EventSet.cs:209
readonly bool Exhaustible
'Exhaustible' sets won't appear in the same level until after one world step (~10 min,...
Definition: EventSet.cs:139
readonly bool OncePerLevel
If enabled, events from this set can only occur once in the level.
Definition: EventSet.cs:197
FactionPrefab Prefab
Definition: Factions.cs:18
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static readonly List< Item > ItemList
bool EventsExhausted
'Exhaustible' sets won't appear in the same level until after one world step (~10 min,...
Definition: LevelData.cs:79
readonly Dictionary< EventSet, int > FinishedEvents
Definition: LevelData.cs:74
readonly List< Identifier > EventHistory
Events that have previously triggered in this level. Used for making events the player hasn't seen ye...
Definition: LevelData.cs:67
readonly LevelType Type
Definition: LevelData.cs:19
LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome)
Definition: LevelData.cs:108
readonly List< Identifier > NonRepeatableEvents
Events that have already triggered in this level and can never trigger again. EventSet....
Definition: LevelData.cs:72
readonly Biome Biome
Definition: LevelData.cs:25
readonly List< LocationConnection > Connections
Definition: Location.cs:56
Vector2 MapPosition
Definition: Location.cs:89
Faction SecondaryFaction
Definition: Location.cs:101
Mersenne Twister based random
Definition: MTRandom.cs:9
override double NextDouble()
Returns random value larger or equal to 0.0 and less than 1.0
Definition: MTRandom.cs:122
Unlocks a mission in a nearby level or location.
Marks fields and properties as to be serialized and deserialized by INetSerializableStruct....
Executes all the child actions when the round ends.
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
Definition: PathFinder.cs:173
readonly Identifier Identifier
Definition: Prefab.cs:34
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static bool LayerExistsInAnySub(Identifier layer)