1326 lines
56 KiB
C#
1326 lines
56 KiB
C#
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<Sprite> preloadedSprites = new List<Sprite>();
|
|
|
|
//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<EventSet> pendingEventSets = new List<EventSet>();
|
|
|
|
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
|
|
|
|
private readonly List<Event> activeEvents = new List<Event>();
|
|
|
|
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
|
|
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
|
|
|
|
|
|
#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<Event> ActiveEvents
|
|
{
|
|
get { return activeEvents; }
|
|
}
|
|
|
|
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
|
|
|
|
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
|
|
|
|
private readonly struct TimeStamp
|
|
{
|
|
public readonly double Time;
|
|
public readonly Event Event;
|
|
|
|
public TimeStamp(Event e)
|
|
{
|
|
Event = e;
|
|
Time = Timing.TotalTime;
|
|
}
|
|
}
|
|
|
|
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
|
|
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();
|
|
UnlockPathAction.ResetPathsUnlockedThisRound();
|
|
#endif
|
|
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), 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;
|
|
|
|
//ensure that the sets that have been configured to be always selected get selected if there's any available
|
|
EventSet initialEventSet = null;
|
|
EventSet additiveSet = null;
|
|
var selectAlwaysEventSets = GetAllowedEventSets(EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign).Where(s => s.SelectAlways);
|
|
foreach (var eventSet in selectAlwaysEventSets)
|
|
{
|
|
if (eventSet.GetCommonness(level) <= 0.0f)
|
|
{
|
|
//you might be wondering why an event set would be configured to SelectAlways, but have a commonness of 0:
|
|
//the set might have a non-zero commonness in some other biome or level type, but not this one
|
|
continue;
|
|
}
|
|
if (eventSet.Additive)
|
|
{
|
|
additiveSet = eventSet;
|
|
}
|
|
else
|
|
{
|
|
if (initialEventSet == null)
|
|
{
|
|
initialEventSet = eventSet;
|
|
}
|
|
else //initial set already chosen, ignore this one
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
AddSet(eventSet);
|
|
}
|
|
|
|
if (initialEventSet == null)
|
|
{
|
|
initialEventSet = SelectRandomEvents(
|
|
EventSet.Prefabs.ToList(),
|
|
requireCampaignSet: playingCampaign,
|
|
random: random);
|
|
}
|
|
|
|
//we happened to choose an additive set as the initial one, choose an additive one too
|
|
if (initialEventSet != null && initialEventSet.Additive)
|
|
{
|
|
additiveSet = initialEventSet;
|
|
initialEventSet = SelectRandomEvents(
|
|
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
|
|
requireCampaignSet: playingCampaign,
|
|
random: random);
|
|
}
|
|
|
|
if (initialEventSet != null) { AddSet(initialEventSet); }
|
|
if (additiveSet != null) { AddSet(additiveSet); }
|
|
|
|
void AddSet(EventSet eventSet)
|
|
{
|
|
if (pendingEventSets.Contains(eventSet)) { return; }
|
|
pendingEventSets.Add(eventSet);
|
|
CreateEvents(eventSet);
|
|
}
|
|
|
|
bool isOutpostLevel = level?.LevelData is { Type: LevelData.LevelType.Outpost } ||
|
|
(GameMain.GameSession?.GameMode is TestGameMode && Submarine.MainSub?.Info?.Type == SubmarineType.Outpost);
|
|
if (isOutpostLevel)
|
|
{
|
|
//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);
|
|
}
|
|
}
|
|
Submarine outpost = level?.StartOutpost ?? Submarine.MainSub;
|
|
if (GameMain.NetworkMember is not { IsClient: true } && outpost != null)
|
|
{
|
|
foreach (var eventTag in outpost.Info.TriggerOutpostMissionEvents)
|
|
{
|
|
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, outpost.ContentPackage);
|
|
if (eventPrefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"Outpost {outpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: outpost.ContentPackage);
|
|
}
|
|
else
|
|
{
|
|
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
|
ActivateEvent(newEvent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (level?.LevelData != null)
|
|
{
|
|
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) ?? EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(id)).GetRandomUnsynced();
|
|
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<ContentFile> GetFilesToPreload()
|
|
{
|
|
foreach (List<Event> eventList in selectedEvents.Values)
|
|
{
|
|
foreach (Event ev in eventList)
|
|
{
|
|
foreach (ContentFile contentFile in ev.GetFilesToPreload())
|
|
{
|
|
yield return contentFile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void PreloadContent(IEnumerable<ContentFile> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
|
|
/// </summary>
|
|
public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false)
|
|
{
|
|
if (level?.LevelData == null) { return; }
|
|
|
|
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.ExhaustEventSet(parentSet);
|
|
}
|
|
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.IsEventSetExhausted(eventSet)) { return; }
|
|
|
|
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
|
|
|
|
int applyCount = 1;
|
|
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
|
|
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<EventPrefab> 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<Event>());
|
|
}
|
|
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<EventPrefab> 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<Event>());
|
|
}
|
|
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 IEnumerable<EventSet> GetAllowedEventSets(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null)
|
|
{
|
|
if (level == null) { return Enumerable.Empty<EventSet>(); }
|
|
|
|
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);
|
|
}
|
|
return allowedEventSets;
|
|
}
|
|
|
|
private EventSet SelectRandomEvents(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
|
|
{
|
|
var allowedEventSets = GetAllowedEventSets(eventSets, requireCampaignSet);
|
|
|
|
if (allowedEventSets.Count() == 1)
|
|
{
|
|
// When there's only a single set available, just select it directly
|
|
return allowedEventSets.First();
|
|
}
|
|
|
|
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
|
|
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)) &&
|
|
(e.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(e.RequiredSpawnPointTag))) &&
|
|
!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) &&
|
|
eventSet.LevelType.HasFlag(level.LevelData.Type) &&
|
|
(eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
|
|
(eventSet.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(eventSet.RequiredSpawnPointTag))) &&
|
|
(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.GetLocationTypeToDisplay();
|
|
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
|
|
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
|
|
if (eventSet.LocationTypeIdentifiers == null) { return false; }
|
|
|
|
// EventLocationType is used to have the event set consider the location id as something else, for example "city" to get events that go to city locations
|
|
bool hasMatchingEventLocationId = !locationType.EventLocationType.IsEmpty &&
|
|
eventSet.LocationTypeIdentifiers.Contains(locationType.EventLocationType);
|
|
|
|
bool hasMatchingLocationId = eventSet.LocationTypeIdentifiers.Contains(locationType.Identifier);
|
|
|
|
return hasMatchingEventLocationId || hasMatchingLocationId;
|
|
}
|
|
|
|
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 (Submarine.MainSub != null &&
|
|
character.CurrentHull?.Submarine.Info is { Type: SubmarineType.Player } &&
|
|
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
|
|
{
|
|
// 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 && Submarine.MainSub != 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static ISpatialEntity GetRefEntity(bool acceptRemoteControlledSubs = false)
|
|
{
|
|
ISpatialEntity refEntity = Submarine.MainSub;
|
|
#if CLIENT
|
|
if (Character.Controlled != null)
|
|
{
|
|
if (Character.Controlled.Submarine is { Info.Type: SubmarineType.Player } playerSub)
|
|
{
|
|
GetRefSubForCharacter(Character.Controlled);
|
|
}
|
|
else
|
|
{
|
|
refEntity = Character.Controlled;
|
|
}
|
|
}
|
|
#else
|
|
if (refEntity == null) { return null; }
|
|
|
|
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
|
|
{
|
|
if (client.Character == null) { continue; }
|
|
GetRefSubForCharacter(client.Character);
|
|
|
|
}
|
|
#endif
|
|
|
|
void GetRefSubForCharacter(Character character)
|
|
{
|
|
if (character.Submarine is { Info.Type: SubmarineType.Player } playerSub)
|
|
{
|
|
if (playerSub.WorldPosition.X > refEntity.WorldPosition.X)
|
|
{
|
|
refEntity = playerSub;
|
|
}
|
|
}
|
|
if (acceptRemoteControlledSubs)
|
|
{
|
|
if (character.ViewTarget?.Submarine is { Info.Type: SubmarineType.Player } viewedSub)
|
|
{
|
|
if (viewedSub.WorldPosition.X > refEntity.WorldPosition.X)
|
|
{
|
|
refEntity = viewedSub;
|
|
}
|
|
}
|
|
if (character.SelectedItem?.GetComponent<Steering>()?.ControlledSub is { } controlledSub)
|
|
{
|
|
if (controlledSub.WorldPosition.X > refEntity.WorldPosition.X)
|
|
{
|
|
refEntity = controlledSub;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<Identifier>()))
|
|
{
|
|
QueuedEventsForNextRound.Enqueue(id);
|
|
}
|
|
}
|
|
|
|
public XElement Save()
|
|
{
|
|
return new XElement("eventmanager",
|
|
new XAttribute(nameof(QueuedEventsForNextRound),
|
|
string.Join(',', QueuedEventsForNextRound)));
|
|
}
|
|
}
|
|
}
|