using Barotrauma.Extensions; using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma { partial class EventManager { public enum NetworkEventType { CONVERSATION, CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, UNLOCKPATH, EVENTLOG, EVENTOBJECTIVE, } [NetworkSerialize] public readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text) : INetSerializableStruct; [NetworkSerialize] public readonly record struct NetEventObjective( EventObjectiveAction.SegmentActionType Type, Identifier Identifier, Identifier ObjectiveTag, Identifier TextTag, Identifier ParentObjectiveId, bool CanBeCompleted) : INetSerializableStruct; const float IntensityUpdateInterval = 5.0f; const float CalculateDistanceTraveledInterval = 5.0f; const int MaxEventHistory = 20; private Level level; private readonly List preloadedSprites = new List(); //The "intensity" of the current situation (a value between 0.0 - 1.0). //High when a disaster has struck, low when nothing special is going on. private float currentIntensity; //The exact intensity of the current situation, current intensity is lerped towards this value private float targetIntensity; //follows targetIntensity a bit faster than currentIntensity to prevent e.g. combat musing staying on very long after the monsters are dead private float musicIntensity; //How low the intensity has to be for an event to be triggered. //Gradually increases with time, so additional problems can still appear eventually even if //the sub is laying broken on the ocean floor or if the players are trying to abuse the system //by intentionally keeping the intensity high by causing breaches, damaging themselves or such private float eventThreshold = 0.2f; //New events can't be triggered when the cooldown is active. private float eventCoolDown; private float intensityUpdateTimer; private PathFinder pathFinder; private float totalPathLength; private float calculateDistanceTraveledTimer; private float distanceTraveled; private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterStrength; public float CumulativeMonsterStrengthMain; public float CumulativeMonsterStrengthRuins; public float CumulativeMonsterStrengthWrecks; public float CumulativeMonsterStrengthCaves; private float roundDuration; private bool isCrewAway; //how long it takes after the crew returns for the event manager to resume normal operation const float CrewAwayResetDelay = 60.0f; private float crewAwayResetTimer; private float crewAwayDuration; private readonly List pendingEventSets = new List(); private readonly Dictionary> selectedEvents = new Dictionary>(); private readonly List activeEvents = new List(); private readonly HashSet finishedEvents = new HashSet(); private readonly HashSet nonRepeatableEvents = new HashSet(); #if DEBUG && SERVER private DateTime nextIntensityLogTime; #endif private EventManagerSettings settings; private readonly bool isClient; public float CurrentIntensity { get { return currentIntensity; } } public float MusicIntensity { get { return musicIntensity; } } public IEnumerable ActiveEvents { get { return activeEvents; } } public readonly Queue QueuedEvents = new Queue(); public readonly Queue QueuedEventsForNextRound = new Queue(); private readonly struct TimeStamp { public readonly double Time; public readonly Event Event; public TimeStamp(Event e) { Event = e; Time = Timing.TotalTime; } } private readonly List timeStamps = new List(); public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e)); public readonly EventLog EventLog = new EventLog(); public EventManager() { isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; } public bool Enabled = true; private MTRandom random; public int RandomSeed { get; private set; } public void StartRound(Level level) { this.level = level; if (isClient) { return; } timeStamps.Clear(); pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); #if SERVER MissionAction.ResetMissionsUnlockedThisRound(); #endif pathFinder = new PathFinder(WayPoint.WayPointList, false); totalPathLength = 0.0f; if (level != null) { var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(level.StartPosition), ConvertUnits.ToSimUnits(level.EndPosition)); totalPathLength = steeringPath.TotalLength; } SelectSettings(); if (level != null) { RandomSeed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { RandomSeed ^= ToolBox.IdentifierToInt(previousEvent); } } random = new MTRandom(RandomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; EventSet initialEventSet = SelectRandomEvents( EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign, random: random); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; initialEventSet = SelectRandomEvents( EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: playingCampaign, random: random); } if (initialEventSet != null) { pendingEventSets.Add(initialEventSet); CreateEvents(initialEventSet); } if (additiveSet != null) { pendingEventSets.Add(additiveSet); CreateEvents(additiveSet); } if (level?.LevelData != null) { if (level.LevelData.Type == LevelData.LevelType.Outpost) { //if the outpost is connected to a locked connection, create an event to unlock it if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); activeEvents.Add(newEvent); } else { //if no event that unlocks the path can be found, unlock it automatically level.StartLocation.Connections.ForEach(c => c.Locked = false); } } } RegisterNonRepeatableChildEvents(initialEventSet); void RegisterNonRepeatableChildEvents(EventSet eventSet) { if (eventSet == null) { return; } if (eventSet.OncePerLevel) { foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { nonRepeatableEvents.Add(ep.Identifier); } } foreach (EventSet childSet in eventSet.ChildSets) { RegisterNonRepeatableChildEvents(childSet); } } } while (QueuedEventsForNextRound.TryDequeue(out var id)) { var eventPrefab = EventSet.GetEventPrefab(id); if (eventPrefab == null) { DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); continue; } var ev = eventPrefab.CreateInstance(RandomSeed); if (ev != null) { QueuedEvents.Enqueue(ev); } } PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; eventsInitialized = false; isCrewAway = false; crewAwayDuration = 0.0f; crewAwayResetTimer = 0.0f; intensityUpdateTimer = 0.0f; CalculateCurrentIntensity(0.0f); currentIntensity = musicIntensity = targetIntensity; eventCoolDown = 0.0f; CumulativeMonsterStrengthMain = 0; CumulativeMonsterStrengthRuins = 0; CumulativeMonsterStrengthWrecks = 0; CumulativeMonsterStrengthCaves = 0; distanceTraveled = 0; } public void ActivateEvent(Event newEvent) { activeEvents.Add(newEvent); newEvent.Init(); } public void ClearEvents() { activeEvents.Clear(); } private void SelectSettings() { if (!EventManagerSettings.Prefabs.Any()) { throw new InvalidOperationException("Could not select EventManager settings (no settings loaded)."); } var orderedByDifficulty = EventManagerSettings.OrderedByDifficulty.ToArray(); if (level == null) { #if CLIENT if (GameMain.GameSession.GameMode is TestGameMode) { settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient); if (settings != null) { eventThreshold = settings.DefaultEventThreshold; } return; } #endif throw new InvalidOperationException("Could not select EventManager settings (level not set)."); } float extraDifficulty = 0; if (GameMain.GameSession.Campaign?.Settings != null) { extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty; } float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100); var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s => modifiedDifficulty >= s.MinLevelDifficulty && modifiedDifficulty <= s.MaxLevelDifficulty).ToArray(); if (suitableSettings.Length == 0) { DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")"); settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient); } else { settings = suitableSettings.GetRandom(Rand.RandSync.ServerAndClient); } if (settings != null) { eventThreshold = settings.DefaultEventThreshold; } } public IEnumerable GetFilesToPreload() { foreach (List eventList in selectedEvents.Values) { foreach (Event ev in eventList) { foreach (ContentFile contentFile in ev.GetFilesToPreload()) { yield return contentFile; } } } } public void PreloadContent(IEnumerable contentFiles) { var filesToPreload = contentFiles.ToList(); foreach (Submarine sub in Submarine.Loaded) { if (sub.WreckAI == null) { continue; } if (!sub.WreckAI.Config.DefensiveAgent.IsEmpty) { var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent); if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) { filesToPreload.Add(prefab.ContentFile); } } foreach (Item item in Item.ItemList) { if (item.Submarine != sub) { continue; } foreach (Items.Components.ItemComponent component in item.Components) { if (component.statusEffectLists == null) { continue; } foreach (var statusEffectList in component.statusEffectLists.Values) { foreach (StatusEffect statusEffect in statusEffectList) { foreach (var spawnInfo in statusEffect.SpawnCharacters) { var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName); if (prefab != null && !filesToPreload.Contains(prefab.ContentFile)) { filesToPreload.Add(prefab.ContentFile); } } } } } } } foreach (ContentFile file in filesToPreload) { file.Preload(preloadedSprites.Add); } } public void TriggerOnEndRoundActions() { foreach (var ev in activeEvents) { (ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f); } } public void EndRound() { pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); timeStamps.Clear(); pathFinder = null; } /// /// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history /// public void RegisterEventHistory(bool registerFinishedOnly = false) { if (level?.LevelData == null) { return; } level.LevelData.EventsExhausted = !registerFinishedOnly; if (level.LevelData.Type == LevelData.LevelType.Outpost) { if (registerFinishedOnly) { foreach (var finishedEvent in finishedEvents) { EventSet parentSet = finishedEvent.ParentSet; if (parentSet == null) { continue; } if (parentSet.Exhaustible) { level.LevelData.EventsExhausted = true; } if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1)) { level.LevelData.FinishedEvents[parentSet] += 1; } } } level.LevelData.EventHistory.AddRange(selectedEvents.Values .SelectMany(v => v) .Select(e => e.Prefab.Identifier) .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId))); if (level.LevelData.EventHistory.Count > MaxEventHistory) { level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); } } level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); if (!registerFinishedOnly) { level.LevelData.FinishedEvents.Clear(); } bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); } public void SkipEventCooldown() { eventCoolDown = 0.0f; } private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; } float retVal = baseCommonness; if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; } return retVal; } private void CreateEvents(EventSet eventSet) { selectedEvents.Remove(eventSet); if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } if (eventSet.Exhaustible && level.LevelData.EventsExhausted) { return; } DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) { applyCount = level.Ruins.Count; foreach (var ruin in level.Ruins) { spawnPosFilter.Add(pos => pos.Ruin == ruin); } } else if (eventSet.PerCave) { applyCount = level.Caves.Count; foreach (var cave in level.Caves) { spawnPosFilter.Add(pos => pos.Cave == cave); } } else if (eventSet.PerWreck) { var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); applyCount = wrecks.Count(); foreach (var wreck in wrecks) { spawnPosFilter.Add(pos => pos.Submarine == wreck); } } foreach (var subEventPrefab in eventSet.EventPrefabs) { foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers()) { DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".", contentPackage: eventSet.ContentPackage); } } var suitablePrefabSubsets = eventSet.EventPrefabs.Where( e => IsFactionSuitable(e.Faction, level) && e.EventPrefabs.Any(ep => IsSuitable(ep, level))).ToArray(); for (int i = 0; i < applyCount; i++) { if (eventSet.ChooseRandom) { if (suitablePrefabSubsets.Any()) { var unusedEvents = suitablePrefabSubsets.ToList(); int eventCount = eventSet.GetEventCount(level); for (int j = 0; j < eventCount; j++) { if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random); (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; if (eventPrefabs != null && random.NextDouble() <= probability) { var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); } selectedEvents[eventSet].Add(newEvent); unusedEvents.Remove(subEventPrefab); } } } if (eventSet.ChildSets.Any()) { int setCount = eventSet.SubSetCount; if (setCount > 1) { var unusedSets = eventSet.ChildSets.ToList(); for (int j = 0; j < setCount; j++) { var newEventSet = SelectRandomEvents(unusedSets, random: random); if (newEventSet == null) { break; } unusedSets.Remove(newEventSet); CreateEvents(newEventSet); } } else { var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); if (newEventSet != null) { CreateEvents(newEventSet); } } } } else { foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { if (random.NextDouble() > probability) { continue; } var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); } selectedEvents[eventSet].Add(newEvent); } var location = GetEventLocation(); foreach (EventSet childEventSet in eventSet.ChildSets) { if (!IsValidForLevel(childEventSet, level)) { continue; } if (!IsValidForLocation(childEventSet, location)) { continue; } CreateEvents(childEventSet); } } } } private EventSet SelectRandomEvents(IReadOnlyList eventSets, bool? requireCampaignSet = null, Random random = null) { if (level == null) { return null; } Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); var allowedEventSets = eventSets.Where(set => IsValidForLevel(set, level)); if (requireCampaignSet.HasValue) { if (requireCampaignSet.Value) { if (allowedEventSets.Any(es => es.IsCampaignSet)) { allowedEventSets = allowedEventSets.Where(es => es.IsCampaignSet); } else { DebugConsole.AddWarning("No campaign event sets available. Using a non-campaign-specific set instead."); } } else { allowedEventSets = allowedEventSets.Where(es => !es.IsCampaignSet); } } var location = GetEventLocation(); allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly || (GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true })); int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location); int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location); if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex)) { allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex); } else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex)) { allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex); } else { // When there are no forced sets, only allow sets that aren't forced at any specific location allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0); } if (allowedEventSets.Count() == 1) { // When there's only a single set available, just select it directly return allowedEventSets.First(); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); float randomNumber = (float)rand.NextDouble(); randomNumber *= totalCommonness; foreach (EventSet eventSet in allowedEventSets) { float commonness = eventSet.GetCommonness(level); if (randomNumber <= commonness) { return eventSet; } randomNumber -= commonness; } return null; } public static bool IsSuitable(EventPrefab e, Level level) { return IsLevelSuitable(e, level) && IsFactionSuitable(e.Faction, level); } public static bool IsLevelSuitable(EventPrefab e, Level level) { return (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && (e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) && !level.LevelData.NonRepeatableEvents.Contains(e.Identifier); } private static bool IsFactionSuitable(Identifier factionId, Level level) { return factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; } private static bool IsValidForLevel(EventSet eventSet, Level level) { return level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) && level.LevelData.Type == eventSet.LevelType && (eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) && (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); } private bool IsValidForLocation(EventSet eventSet, Location location) { if (location is null) { return true; } if (!eventSet.Faction.IsEmpty) { if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; } } var locationType = location.GetLocationType(); bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents; if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; } return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier); } private Location GetEventLocation() { return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation; } private bool CanStartEventSet(EventSet eventSet) { if (!eventSet.AllowAtStart) { ISpatialEntity refEntity = GetRefEntity(); float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); if (distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) { return false; } } if (eventSet.DelayWhenCrewAway) { if ((isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) || crewAwayResetTimer > 0.0f) { return false; } } if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && roundDuration < eventSet.MinMissionTime) { return false; } if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity) { return false; } return true; } private bool eventsInitialized; public void Update(float deltaTime) { if (!Enabled) { return; } if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; } if (!eventsInitialized) { foreach (var eventSet in selectedEvents.Keys) { foreach (var ev in selectedEvents[eventSet]) { ev.Init(eventSet); } } eventsInitialized = true; } //clients only calculate the intensity but don't create any events //(the intensity is used for controlling the background music) CalculateCurrentIntensity(deltaTime); #if DEBUG && SERVER if (DateTime.Now > nextIntensityLogTime) { DebugConsole.NewMessage("EventManager intensity: " + (int)Math.Round(currentIntensity * 100) + " %"); nextIntensityLogTime = DateTime.Now + new TimeSpan(0, minutes: 1, seconds: 0); } #endif if (isClient) { return; } roundDuration += deltaTime; if (settings == null) { DebugConsole.ThrowError("Event settings not set before updating EventManager. Attempting to select..."); SelectSettings(); if (settings == null) { DebugConsole.ThrowError("Could not select EventManager settings. Disabling EventManager for the round..."); #if SERVER GameMain.Server?.SendChatMessage("Could not select EventManager settings. Disabling EventManager for the round...", Networking.ChatMessageType.Error); #endif Enabled = false; return; } } if (IsCrewAway()) { isCrewAway = true; crewAwayResetTimer = CrewAwayResetDelay; crewAwayDuration += deltaTime; } else if (crewAwayResetTimer > 0.0f) { isCrewAway = false; crewAwayResetTimer -= deltaTime; } else { isCrewAway = false; crewAwayDuration = 0.0f; eventThreshold += settings.EventThresholdIncrease * deltaTime; eventThreshold = Math.Min(eventThreshold, 1.0f); eventCoolDown -= deltaTime; } calculateDistanceTraveledTimer -= deltaTime; if (calculateDistanceTraveledTimer <= 0.0f) { distanceTraveled = CalculateDistanceTraveled(); calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; } bool recheck = false; do { recheck = false; //activate pending event sets that can be activated for (int i = pendingEventSets.Count - 1; i >= 0; i--) { var eventSet = pendingEventSets[i]; if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; } if (!CanStartEventSet(eventSet)) { continue; } pendingEventSets.RemoveAt(i); if (selectedEvents.ContainsKey(eventSet)) { //start events in this set foreach (Event ev in selectedEvents[eventSet]) { activeEvents.Add(ev); eventThreshold = settings.DefaultEventThreshold; if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) { eventCoolDown = settings.EventCooldown; } if (eventSet.ResetTime > 0) { ev.Finished += () => { pendingEventSets.Add(eventSet); CreateEvents(eventSet); foreach (Event newEvent in selectedEvents[eventSet]) { if (!newEvent.Initialized) { newEvent.Init(eventSet); } } }; } } } //add child event sets to pending foreach (EventSet childEventSet in eventSet.ChildSets) { pendingEventSets.Add(childEventSet); recheck = true; } } } while (recheck); foreach (Event ev in activeEvents) { if (!ev.IsFinished) { ev.Update(deltaTime); } else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab)) { if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) { if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } } finishedEvents.Add(ev); } } if (QueuedEvents.Count > 0) { activeEvents.Add(QueuedEvents.Dequeue()); } } public void EntitySpawned(Entity entity) { foreach (var ev in activeEvents) { if (ev is ScriptedEvent scriptedEvent) { scriptedEvent.EntitySpawned(entity); } } } private void CalculateCurrentIntensity(float deltaTime) { intensityUpdateTimer -= deltaTime; if (intensityUpdateTimer > 0.0f) { return; } intensityUpdateTimer = IntensityUpdateInterval; // crew health -------------------------------------------------------- avgCrewHealth = 0.0f; int characterCount = 0; foreach (Character character in Character.CharacterList) { if (character.IsDead || character.TeamID == CharacterTeamType.FriendlyNPC) { continue; } if (character.AIController is HumanAIController || character.IsRemotePlayer) { avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f); characterCount++; } } if (characterCount > 0) { avgCrewHealth /= characterCount; } else { avgCrewHealth = 0.5f; } // enemy amount -------------------------------------------------------- enemyDanger = 0.0f; monsterStrength = 0; foreach (Character character in Character.CharacterList) { if (character.IsIncapacitated || character.IsHandcuffed || !character.Enabled || character.IsPet) { continue; } if (character.AIController is EnemyAIController enemyAI) { if (!enemyAI.AIParams.StayInAbyss) { // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. monsterStrength += enemyAI.CombatStrength; } if (character.CurrentHull?.Submarine?.Info != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) { // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 enemyDanger += enemyAI.CombatStrength / 500.0f; } else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) { // Enemy outside targeting the sub or something in it // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. enemyDanger += enemyAI.CombatStrength / 5000.0f; } } else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1)) { if (character.Submarine != null && character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } && Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange) { //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), //so let's just go with a fixed value. //5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max enemyDanger += 0.2f; } } } // 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. // 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. // And if they get inside, we add 0.1 per crawler on that. // 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. // 10 Crawlers -> +0.2 (0.4 in total if all target the sub from outside). // 5 Mudraptors -> +0.21 (0.42 in total, before they get inside). // 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub). // 2 Molochs -> +0.5 (1.0 in total, if both target the sub). enemyDanger += monsterStrength / 5000f; enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); // The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle. // Some examples that result in the max intensity even when the creatures would just idle around. // 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. // 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. // 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: // 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). // 25 Tigerthreshers (Max 9 per event. 12 targeting the sub at the same time results in max intensity). // 10 Hammerheads (Max 3 per event. 5 targeting the sub at the same time results in max intensity). // 4 Molochs (Max 2 per event and 2 targeting the sub at the same time results in max intensity). // hull status (gaps, flooding, fire) -------------------------------------------------------- float holeCount = 0.0f; float waterAmount = 0.0f; float dryHullVolume = 0.0f; foreach (Hull hull in Hull.HullList) { if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; } if (GameMain.GameSession?.GameMode is PvPMode) { if (hull.Submarine.TeamID != CharacterTeamType.Team1 && hull.Submarine.TeamID != CharacterTeamType.Team2) { continue; } } else { if (hull.Submarine.TeamID != CharacterTeamType.Team1) { continue; } } fireAmount += hull.FireSources.Sum(fs => fs.Size.X); if (hull.IsWetRoom) { continue; } foreach (Gap gap in hull.ConnectedGaps) { if (!gap.IsRoomToRoom) { holeCount += gap.Open; } } waterAmount += hull.WaterVolume; dryHullVolume += hull.Volume; } if (dryHullVolume > 0) { floodingAmount = waterAmount / dryHullVolume; } //hull integrity at 0.0 if there are 10 or more wide-open holes avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f); //a fire of any size bumps up the fire amount to 20% //if the total width of the fires is 1000 or more, the fire amount is considered to be at 100% fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f); //flooding less than 10% of the sub is ignored //to prevent ballast tanks from affecting the intensity if (floodingAmount < 0.1f) { floodingAmount = 0.0f; } else { floodingAmount *= 1.5f; } // calculate final intensity -------------------------------------------------------- targetIntensity = ((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f; targetIntensity += fireAmount * 0.5f; targetIntensity += enemyDanger; targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f); if (targetIntensity > currentIntensity) { //25 seconds for intensity to go from 0.0 to 1.0 currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity); //20 seconds for intensity to go from 0.0 to 1.0 musicIntensity = Math.Min(musicIntensity + 0.05f * IntensityUpdateInterval, targetIntensity); } else { //400 seconds for intensity to go from 1.0 to 0.0 currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity); //20 seconds for intensity to go from 1.0 to 0.0 musicIntensity = Math.Max(musicIntensity - 0.05f * IntensityUpdateInterval, targetIntensity); } } private float CalculateDistanceTraveled() { if (level == null || pathFinder == null) { return 0.0f; } var refEntity = GetRefEntity(); if (refEntity == null) { return 0.0f; } Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition); var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target); if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength)) { //use horizontal position in the level as a fallback if a path can't be found return MathHelper.Clamp((refEntity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); } else { return MathHelper.Clamp(1.0f - steeringPath.TotalLength / totalPathLength, 0.0f, 1.0f); } } /// /// Get the entity that should be used in determining how far the player has progressed in the level. /// = The submarine or player character that has progressed the furthest. /// public static ISpatialEntity GetRefEntity() { ISpatialEntity refEntity = Submarine.MainSub; #if CLIENT if (Character.Controlled != null) { if (Character.Controlled.Submarine != null && Character.Controlled.Submarine.Info.Type == SubmarineType.Player) { refEntity = Character.Controlled.Submarine; } else { refEntity = Character.Controlled; } } #else foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients) { if (client.Character == null) { continue; } //only take the players inside a player sub into account. //Otherwise the system could be abused by for example making a respawned player wait //close to the destination outpost if (client.Character.Submarine != null && client.Character.Submarine.Info.Type == SubmarineType.Player) { if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X) { refEntity = client.Character.Submarine; } } } #endif return refEntity; } private bool IsCrewAway() { #if CLIENT return Character.Controlled != null && IsCharacterAway(Character.Controlled); #else int playerCount = 0; int awayPlayerCount = 0; foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients) { if (client.Character == null || client.Character.IsDead || client.Character.IsIncapacitated) { continue; } playerCount++; if (IsCharacterAway(client.Character)) { awayPlayerCount++; } } return playerCount > 0 && awayPlayerCount / (float)playerCount > 0.5f; #endif } private bool IsCharacterAway(Character character) { if (character.Submarine != null) { switch (character.Submarine.Info.Type) { case SubmarineType.Player: case SubmarineType.Outpost: case SubmarineType.OutpostModule: return false; case SubmarineType.Wreck: case SubmarineType.BeaconStation: case SubmarineType.Ruin: return true; } } const int maxDist = 1000; if (level != null && !level.Removed) { foreach (var ruin in level.Ruins) { Rectangle area = ruin.Area; area.Inflate(maxDist, maxDist); if (area.Contains(character.WorldPosition)) { return true; } } foreach (var cave in level.Caves) { Rectangle area = cave.Area; area.Inflate(maxDist, maxDist); if (area.Contains(character.WorldPosition)) { return true; } } } foreach (Submarine sub in Submarine.Loaded) { if (sub.Info.Type != SubmarineType.BeaconStation && sub.Info.Type != SubmarineType.Wreck) { continue; } Rectangle worldBorders = new Rectangle( sub.Borders.X + (int)sub.WorldPosition.X - maxDist, sub.Borders.Y + (int)sub.WorldPosition.Y + maxDist, sub.Borders.Width + maxDist * 2, sub.Borders.Height + maxDist * 2); if (Submarine.RectContains(worldBorders, character.WorldPosition)) { return true; } } return false; } public void Load(XElement element) { foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty())) { QueuedEventsForNextRound.Enqueue(id); } } public XElement Save() { return new XElement("eventmanager", new XAttribute(nameof(QueuedEventsForNextRound), string.Join(',', QueuedEventsForNextRound))); } } }