using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class EventManager { const float IntensityUpdateInterval = 5.0f; private List events; private Level level; //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; //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 float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger; private float roundDuration; private List selectedEventSets; private EventManagerSettings settings; public float CurrentIntensity { get { return currentIntensity; } } public List Events { get { return events; } } public EventManager(GameSession session) { events = new List(); selectedEventSets = new List(); } public bool Enabled = true; public void StartRound(Level level) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; var suitableSettings = EventManagerSettings.List.FindAll(s => level.Difficulty >= s.MinLevelDifficulty && level.Difficulty <= s.MaxLevelDifficulty); if (suitableSettings.Count == 0) { DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")"); settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)]; } else { settings = suitableSettings[Rand.Int(suitableSettings.Count, Rand.RandSync.Server)]; } this.level = level; var initialEventSet = SelectRandomEvents(ScriptedEventSet.List); if (initialEventSet != null) selectedEventSets.Add(initialEventSet); /*CreateInitialEvents(); foreach (ScriptedEvent ev in events) { ev.Init(false); }*/ roundDuration = 0.0f; intensityUpdateTimer = 0.0f; CalculateCurrentIntensity(0.0f); currentIntensity = targetIntensity; eventThreshold = settings.DefaultEventThreshold; eventCoolDown = 0.0f; } public void EndRound() { selectedEventSets.Clear(); events.Clear(); } private ScriptedEventSet SelectRandomEvents(List eventSets) { MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); var allowedEventSets = eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty); float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); float randomNumber = (float)rand.NextDouble() * totalCommonness; foreach (ScriptedEventSet eventSet in allowedEventSets) { float commonness = eventSet.GetCommonness(level); if (randomNumber <= commonness) { return eventSet; } randomNumber -= commonness; } return null; } private void CreateEvents() { //don't create new events if docked to the start oupost if (Level.Loaded?.StartOutpost != null && Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)) { return; } for (int i = selectedEventSets.Count - 1; i >= 0; i--) { ScriptedEventSet eventSet = selectedEventSets[i]; float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition); float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition); float distanceTraveled = MathHelper.Clamp( (Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); //don't create new events if within 50 meters of the start/end of the level if (distanceTraveled <= 0.0f || distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) { continue; } if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && roundDuration < eventSet.MinMissionTime) { continue; } if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity) { continue; } selectedEventSets.RemoveAt(i); if (eventSet.ChooseRandom) { if (eventSet.EventPrefabs.Count > 0) { MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); var newEvent = eventSet.EventPrefabs[rand.NextInt32() % eventSet.EventPrefabs.Count].CreateInstance(); newEvent.Init(true); DebugConsole.Log("Initialized event " + newEvent.ToString()); events.Add(newEvent); } if (eventSet.ChildSets.Count > 0) { MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); var newEventSet = SelectRandomEvents(eventSet.ChildSets); if (newEventSet != null) selectedEventSets.Add(newEventSet); } } else { foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs) { var newEvent = eventPrefab.CreateInstance(); newEvent.Init(true); DebugConsole.Log("Initialized event " + newEvent.ToString()); events.Add(newEvent); } selectedEventSets.AddRange(eventSet.ChildSets); } } } public void Update(float deltaTime) { if (!Enabled) return; //clients only calculate the intensity but don't create any events //(the intensity is used for controlling the background music) CalculateCurrentIntensity(deltaTime); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; roundDuration += deltaTime; eventThreshold += settings.EventThresholdIncrease * deltaTime; if (eventCoolDown > 0.0f) { eventCoolDown -= deltaTime; } else if (currentIntensity < eventThreshold) { CreateEvents(); eventThreshold = settings.DefaultEventThreshold; eventCoolDown = settings.EventCooldown; } foreach (ScriptedEvent ev in events) { if (!ev.IsFinished) { ev.Update(deltaTime); } } } 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) continue; #if CLIENT if ((character.AIController is HumanAIController || character.IsRemotePlayer || character == Character.Controlled) && (GameMain.Client?.Character == null || GameMain.Client.Character.TeamID == character.TeamID)) { avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f); characterCount++; } #endif } if (characterCount > 0) { avgCrewHealth = avgCrewHealth / characterCount; } // enemy amount -------------------------------------------------------- enemyDanger = 0.0f; foreach (Character character in Character.CharacterList) { if (character.IsDead || character.IsUnconscious || !character.Enabled) continue; EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null) continue; if (character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { //crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f enemyDanger += enemyAI.CombatStrength / 1000.0f; } else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) { //enemy outside and targeting the sub or something in it //moloch adds 0.24 to enemy danger, a crawler 0.02 enemyDanger += enemyAI.CombatStrength / 5000.0f; } } enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); // hull status (gaps, flooding, fire) -------------------------------------------------------- float holeCount = 0.0f; floodingAmount = 0.0f; foreach (Hull hull in Hull.hullList) { if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; } foreach (Gap gap in hull.ConnectedGaps) { if (!gap.IsRoomToRoom) holeCount += gap.Open; } floodingAmount += hull.WaterVolume / hull.Volume / Hull.hullList.Count; fireAmount += hull.FireSources.Sum(fs => fs.Size.X); } //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; // 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) { //50 seconds for intensity to go from 0.0 to 1.0 currentIntensity = MathHelper.Min(currentIntensity + 0.02f * IntensityUpdateInterval, targetIntensity); } else { //400 seconds for intensity to go from 1.0 to 0.0 currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity); } } } }