v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -623,7 +623,8 @@ namespace Barotrauma.MapCreatures.Behavior
List<BallastFloraBranch> list = branches[hull];
if (!list.Any(HasAcidEmitter))
{
BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced();
BallastFloraBranch? randomBranch = branches[hull].GetRandomUnsynced();
if (randomBranch == null) { continue; }
randomBranch.SpawningItem = true;
ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab);
@@ -239,6 +239,8 @@ namespace Barotrauma
OnlyInside = element.GetAttributeBool("onlyinside", false);
OnlyOutside = element.GetAttributeBool("onlyoutside", false);
DistanceFalloff = element.GetAttributeBool(nameof(DistanceFalloff), true);
flash = element.GetAttributeBool("flash", showEffects);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
@@ -436,19 +438,23 @@ namespace Barotrauma
}
}
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
if (!item.Indestructible)
{
float distFactor =
DistanceFalloff ?
1.0f - dist / displayRange :
1.0f;
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
if (item.Prefab.DamagedByExplosions ||
(item.Prefab.DamagedByContainedItemExplosions && item.ContainedItems.Contains(damageSource)))
{
float distFactor =
DistanceFalloff ?
1.0f - dist / displayRange :
1.0f;
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
Vector2 explosionPos = worldPosition;
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
Vector2 explosionPos = worldPosition;
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover);
item.Condition -= damageAmount * distFactor;
damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover);
item.Condition -= damageAmount * distFactor;
}
}
}
}
@@ -542,7 +542,7 @@ namespace Barotrauma
var clone = new Hull(rect, Submarine);
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
if (!property.Value.Attributes.OfType<Serialize>().Any()) { continue; }
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
}
#if CLIENT
@@ -96,6 +96,7 @@ namespace Barotrauma
Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant());
Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty);
float scale = entityElement.GetAttributeFloat("scale", 1.0f);
float rotation = MathHelper.ToRadians(entityElement.GetAttributeFloat("rotation", 0.0f));
if (!entityElement.GetAttributeBool("hideinassemblypreview", false))
{
@@ -180,7 +181,7 @@ namespace Barotrauma
if (ContentPackage is { Files: { Length: 1 } }
&& ContentPackageManager.LocalPackages.Contains(ContentPackage))
{
Directory.Delete(ContentPackage.Dir, recursive: true);
Directory.Delete(ContentPackage.Dir, recursive: true, catchUnauthorizedAccessExceptions: false);
ContentPackageManager.LocalPackages.Refresh();
ContentPackageManager.EnabledPackages.DisableRemovedMods();
}
@@ -322,8 +322,14 @@ namespace Barotrauma
public Submarine BeaconStation { get; private set; }
private Sonar beaconSonar;
/// <summary>
/// Special wall chunks that aren't part of the normal level geometry: includes things like the ocean floor, floating ice chunks and ice spires.
/// </summary>
public List<LevelWall> ExtraWalls { get; private set; }
/// <summary>
/// Purely decorative wall chunks whose positions don't need to be synced (e.g. the chunks created when a desctructible wall breaks)
/// </summary>
public List<LevelWall> UnsyncedExtraWalls { get; private set; }
public List<Tunnel> Tunnels { get; private set; } = new List<Tunnel>();
@@ -487,8 +493,7 @@ namespace Barotrauma
{
if (StartOutpost != null &&
Type == LevelData.LevelType.Outpost &&
(StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) &&
StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player))
((StartOutpost.Info.OutpostGenerationParams is { SpawnCrewInsideOutpost: true } && StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) || Submarine.MainSub == null))
{
return GameMain.GameSession.Campaign?.CurrentLocation is not { IsFactionHostile: true };
}
@@ -519,7 +524,8 @@ namespace Barotrauma
#if CLIENT
Debug.Assert(GenerationParams.Identifier != "coldcavernstutorial" || GameMain.GameSession?.GameMode == null || GameMain.GameSession.GameMode is TutorialMode);
#endif
Debug.Assert(GenerationParams.AnyBiomeAllowed || GenerationParams.AllowedBiomeIdentifiers.Contains(LevelData.Biome.Identifier));
Debug.Assert(GenerationParams.AnyBiomeAllowed || GenerationParams.AllowedBiomeIdentifiers.Contains(LevelData.Biome.Identifier),
"The selected generation parameters are not suitable for the current biome (resorted to a fallback due to no suitable parameters being found for this biome?)");
DebugConsole.NewMessage("Level identifier: " + GenerationParams.Identifier);
ClearEqualityCheckValues();
@@ -529,7 +535,7 @@ namespace Barotrauma
StartLocation = startLocation;
EndLocation = endLocation;
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
ResetRandomSeed();
GenerateEqualityCheckValue(LevelGenStage.GenStart);
SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier));
@@ -687,7 +693,9 @@ namespace Barotrauma
int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.ServerAndClient);
Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff));
}
Debug.WriteLine("Tunnels after generating main tunnels: " + Tunnels.Count);
CalculateTunnelDistanceField(null);
GenerateSeaFloorPositions();
@@ -698,6 +706,7 @@ namespace Barotrauma
GenerateEqualityCheckValue(LevelGenStage.AbyssGen);
GenerateCaves(mainPath);
Debug.WriteLine("Tunnels after generating caves: " + Tunnels.Count);
GenerateEqualityCheckValue(LevelGenStage.CaveGen);
@@ -706,6 +715,7 @@ namespace Barotrauma
//----------------------------------------------------------------------------------
GenerateVoronoiSites();
Debug.WriteLine("Site coords after generating voronoi sites: " + siteCoordsX.Count);
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen);
@@ -726,6 +736,7 @@ namespace Barotrauma
voronoiGraphInvalid = false;
//construct voronoi cells based on the graph edges
List<GraphEdge> graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height);
Debug.WriteLine("Graph edges after generating voronoi graph: " + graphEdges.Count);
cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid);
for (int i = 0; i < cells.Count; i++)
{
@@ -763,8 +774,11 @@ namespace Barotrauma
}
}
} while (remainingRetries > 0 && voronoiGraphInvalid);
Debug.WriteLine("Cells after generating initial cells:" + cells.Count);
GenerateAbyssGeometry();
Debug.WriteLine("Tunnels after generating abyss geometry: " + Tunnels.Count);
GenerateAbyssPositions();
Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms");
@@ -774,6 +788,7 @@ namespace Barotrauma
// generate a path through the tunnel nodes
//----------------------------------------------------------------------------------
ResetRandomSeed();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
foreach (Tunnel tunnel in Tunnels)
{
@@ -851,11 +866,17 @@ namespace Barotrauma
WayPoint wayPoint = new WayPoint(
positionOfInterest.Position.ToVector2(),
SpawnType.Enemy,
submarine: null);
submarine: null)
{
Cave = positionOfInterest.Cave,
Ruin = positionOfInterest.Ruin
};
}
startPosition.X = (int)pathCells[0].Site.Coord.X;
startExitPosition.X = startPosition.X;
Debug.WriteLine("Waypoints after voronoi gen2: " + WayPoint.WayPointList.Count);
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2);
@@ -894,7 +915,12 @@ namespace Barotrauma
abyssIsland.Cells.RemoveAll(c => c.CellType == CellType.Path);
cells.AddRange(abyssIsland.Cells);
}
// Reset the seed, because CreateHoles breaks the determinism between mirrored and non-mirrored levels. Happens because there's a different amount of cells, which leads to CreateHoles() doing different amount of calls to Rand.GetRNG() when iterating through the cells).
ResetRandomSeed();
Debug.WriteLine("Cells after creating holes: " + cells.Count);
List<Point> ruinPositions = new List<Point>();
int ruinCount = GenerationParams.UseRandomRuinCount()
? Rand.Range(GenerationParams.MinRuinCount, GenerationParams.MaxRuinCount + 1, Rand.RandSync.ServerAndClient)
@@ -908,6 +934,11 @@ namespace Barotrauma
for (int i = 0; i < ruinCount; i++)
{
if (!hasRuinMissions)
{
if (Rand.Range(0f, 1f, Rand.RandSync.ServerAndClient) >= GenerationParams.RuinSpawnProbability) { continue; }
}
Point ruinSize = new Point(5000);
int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2);
int limitRight = Math.Min(endPosition.X, Size.X - ruinSize.X / 2);
@@ -1068,11 +1099,17 @@ namespace Barotrauma
var connectingEdge = i > 0 ? cavePathCells[i].Edges.Find(e => e.AdjacentCell(cavePathCells[i]) == cavePathCells[i - 1]) : null;
if (connectingEdge != null)
{
var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null);
var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null)
{
Cave = cave
};
ConnectWaypoints(prevWp, edgeWayPoint, 500.0f);
prevWp = edgeWayPoint;
}
var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null);
var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null)
{
Cave = cave
};
ConnectWaypoints(prevWp, newWaypoint, 500.0f);
prevWp = newWaypoint;
}
@@ -1110,7 +1147,9 @@ namespace Barotrauma
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i);
GenerateRuin(ruinPositions[i], mirror, hasRuinMissions);
}
Debug.WriteLine("Waypoints after creating ruins: " + WayPoint.WayPointList.Count);
GenerateEqualityCheckValue(LevelGenStage.Ruins);
//----------------------------------------------------------------------------------
@@ -1254,6 +1293,9 @@ namespace Barotrauma
// create ice spires
//----------------------------------------------------------------------------------
// Reset the seed, so that the level geometry doesn't affect the spires. Level geometry can be slightly different between mirrored and non-mirrored levels, and spires affect where wrecks and beacons can be placed.
ResetRandomSeed();
List<GraphEdge> usedSpireEdges = new List<GraphEdge>();
for (int i = 0; i < GenerationParams.IceSpireCount; i++)
{
@@ -1303,6 +1345,8 @@ namespace Barotrauma
bodies.Add(TopBarrier);
GenerateSeaFloor();
Debug.WriteLine("Waypoints after creating sea floor: " + WayPoint.WayPointList.Count);
if (mirror)
{
@@ -1330,6 +1374,7 @@ namespace Barotrauma
GenerateEqualityCheckValue(LevelGenStage.TopAndBottom);
ResetRandomSeed();
LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount);
GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects);
@@ -1399,6 +1444,7 @@ namespace Barotrauma
private void GenerateVoronoiSites()
{
ResetRandomSeed();
Point siteInterval = GenerationParams.VoronoiSiteInterval;
int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
Point siteVariance = GenerationParams.VoronoiSiteVariance;
@@ -1505,6 +1551,28 @@ namespace Barotrauma
}
}
}
private bool isRandomHashSet;
private int _randomHash;
private string previousSeed;
private int RandomHash
{
get
{
if (Seed != previousSeed)
{
isRandomHashSet = false;
}
if (!isRandomHashSet)
{
_randomHash = ToolBox.StringToInt(Seed);
isRandomHashSet = true;
previousSeed = Seed;
}
return _randomHash;
}
}
private void ResetRandomSeed() => Rand.SetSyncedSeed(RandomHash);
private List<Point> GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance)
{
@@ -1886,6 +1954,7 @@ namespace Barotrauma
private void GenerateAbyssGeometry()
{
ResetRandomSeed();
//TODO: expose island parameters
Voronoi voronoi = new Voronoi(1.0);
@@ -1999,6 +2068,7 @@ namespace Barotrauma
private void GenerateSeaFloorPositions()
{
ResetRandomSeed();
BottomPos = GenerationParams.SeaFloorDepth;
SeaFloorTopPos = BottomPos;
@@ -2054,6 +2124,7 @@ namespace Barotrauma
private void GenerateCaves(Tunnel parentTunnel)
{
ResetRandomSeed();
for (int i = 0; i < GenerationParams.CaveCount; i++)
{
var caveParams = CaveGenerationParams.GetRandom(this, abyss: false, rand: Rand.RandSync.ServerAndClient);
@@ -2161,42 +2232,53 @@ namespace Barotrauma
float weight = MathUtils.Pow(1 - diff, 10);
return Math.Max(weight, 0);
}
IEnumerable<RuinGenerationParams> possibleRuinGenerationParams = RuinGenerationParams.RuinParams;
if (requireMissionReadyRuin)
RuinGenerationParams ruinGenerationParams = null;
if (LevelData.ForceRuinGenerationParams != null)
{
possibleRuinGenerationParams = possibleRuinGenerationParams.Where(p => p.IsMissionReady);
ruinGenerationParams = LevelData.ForceRuinGenerationParams;
}
if (possibleRuinGenerationParams.Multiple())
else
{
// Sort by weight and choose from the closest 25% of the candidates.
// Prevents choosing from the "wrong" end, which would otherwise be possible (yet rare), because we use a weighted random for the pick.
possibleRuinGenerationParams = possibleRuinGenerationParams
/* the prefabs aren't in a consistent order, so we need to sort them first to ensure the clients and server choose the same one */
.OrderByDescending(p => p.UintIdentifier)
.OrderByDescending(GetWeight)
.Take((int)Math.Max(Math.Round(possibleRuinGenerationParams.Count() / 4f), 1));
IEnumerable<RuinGenerationParams> possibleRuinGenerationParams = RuinGenerationParams.RuinParams;
if (requireMissionReadyRuin)
{
possibleRuinGenerationParams = possibleRuinGenerationParams.Where(p => p.IsMissionReady);
}
if (possibleRuinGenerationParams.Multiple())
{
// Sort by weight and choose from the closest 25% of the candidates.
// Prevents choosing from the "wrong" end, which would otherwise be possible (yet rare), because we use a weighted random for the pick.
possibleRuinGenerationParams = possibleRuinGenerationParams
/* the prefabs aren't in a consistent order, so we need to sort them first to ensure the clients and server choose the same one */
.OrderByDescending(p => p.UintIdentifier)
.ThenByDescending(GetWeight)
.Take((int)Math.Max(Math.Round(possibleRuinGenerationParams.Count() / 4f), 1));
}
ruinGenerationParams = possibleRuinGenerationParams.GetRandomByWeight(GetWeight, randSync: Rand.RandSync.ServerAndClient);
if (ruinGenerationParams == null)
{
DebugConsole.ThrowError("Failed to generate alien ruins. Could not find any RuinGenerationParameters!");
return;
}
}
var selectedRuinGenerationParams = possibleRuinGenerationParams.GetRandomByWeight(GetWeight, randSync: Rand.RandSync.ServerAndClient);
if (selectedRuinGenerationParams == null)
{
DebugConsole.ThrowError("Failed to generate alien ruins. Could not find any RuinGenerationParameters!");
return;
}
DebugConsole.NewMessage($"Creating alien ruins using {selectedRuinGenerationParams.Identifier} (preferred difficulty: {selectedRuinGenerationParams.PreferredDifficulty}, current difficulty {Difficulty})", color: Color.Yellow, debugOnly: true);
DebugConsole.NewMessage($"Creating alien ruins using {ruinGenerationParams.Identifier} (preferred difficulty: {ruinGenerationParams.PreferredDifficulty}, current difficulty {Difficulty})", color: Color.Yellow, debugOnly: true);
LocationType locationType = StartLocation?.Type;
if (locationType == null)
{
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
if (selectedRuinGenerationParams.AllowedLocationTypes.Any())
if (ruinGenerationParams.AllowedLocationTypes.Any())
{
locationType = LocationType.Prefabs.Where(lt =>
selectedRuinGenerationParams.AllowedLocationTypes.Any(allowedType =>
ruinGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient);
}
}
var ruin = new Ruin(this, selectedRuinGenerationParams, locationType, ruinPos, mirror);
var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror);
if (ruin.Submarine != null)
{
SetLinkedSubCrushDepth(ruin.Submarine);
@@ -2685,6 +2767,7 @@ namespace Barotrauma
// Such as the exploding crystals in The Great Sea
private void GenerateItems()
{
ResetRandomSeed();
var levelResources = new List<(ItemPrefab itemPrefab, ItemPrefab.CommonnessInfo commonnessInfo)>();
var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>();
Vector2 commonnessRange = new Vector2(float.MaxValue, float.MinValue), caveCommonnessRange = new Vector2(float.MaxValue, float.MinValue);
@@ -3724,28 +3807,75 @@ namespace Barotrauma
return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist;
}
private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type, bool forceThalamus = false)
/// <summary>
/// Attempts to spawn a submarine (or a beacon station).
/// </summary>
/// <param name="subName">Name of submarine.</param>
/// <param name="contentFile">Content file.</param>
/// <param name="type">Submarine type.</param>
/// <param name="thalamusSpawn">Optional parameter to control spawning of thalamus. Only implemented for wrecks.</param>
/// <param name="spawnInTheMiddle">Should the spawn position be at (or as close as possible to) the middle of the level.</param>
/// <returns>The spawned submarine.</returns>
private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type,
LevelData.ThalamusSpawn thalamusSpawn = LevelData.ThalamusSpawn.Random,
bool spawnInTheMiddle = false)
{
var tempSW = new Stopwatch();
var tempSW = Stopwatch.StartNew();
// Min distance between a sub and the start/end/other sub.
const float minDistance = Sonar.DefaultSonarRange;
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
wp.WorldPosition.X < EndExitPosition.X &&
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value)
{
Type = type
};
//place downwards by default
var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom;
const float horizontalMargin = 1000;
float distanceBetweenStartAndEnd = Math.Abs(endPosition.X - startPosition.X);
bool spawnAwayFromStartAndEnd = distanceBetweenStartAndEnd > minDistance * 2 + horizontalMargin;
var waypoints = WayPoint.WayPointList.Where(IsValidWaypoint).ToList();
bool IsValidWaypoint(WayPoint wp)
{
if (wp.Submarine != null) { return false; }
if (wp.SpawnType != SpawnType.Path) { return false; }
if (wp.Tunnel is not { Type: TunnelType.MainPath }) { return false; }
// Ensure that the wp is not farther than the end exit position.
if (wp.WorldPosition.X > EndExitPosition.X) { return false; }
if (spawnAwayFromStartAndEnd)
{
// Make sure that the wp is horizontally far enough from the start position (skips some of the first waypoints)
if (Math.Abs(wp.WorldPosition.X - startPosition.X) < minDistance) { return false; }
// Also have to check the end position, because it's the start position on mirrored levels (skips some of the last waypoints)
if (Math.Abs(wp.WorldPosition.X - endPosition.X) < minDistance) { return false; }
}
if (IsCloseToStart(wp.WorldPosition, minDistance)) { return false; }
if (IsCloseToEnd(wp.WorldPosition, minDistance)) { return false; }
return true;
}
if (spawnInTheMiddle)
{
float horizontalMiddlePoint = Size.X / 2f;
float GetHorizontalDistanceToMiddlePoint(WayPoint wp) => Math.Abs(wp.WorldPosition.X - horizontalMiddlePoint);
waypoints.Sort((wp1, wp2) => GetHorizontalDistanceToMiddlePoint(wp2).CompareTo(GetHorizontalDistanceToMiddlePoint(wp1)));
}
else
{
// Randomize the list in advance, so that the initial waypoints are taken in the same order (determined by the random seed).
// If we'd just get a random wp from it when we need, it's not guaranteed that there's equal amount of random calls between mirrored and non-mirrored levels.
waypoints.Shuffle(Rand.RandSync.ServerAndClient);
}
if (waypoints.None())
{
DebugConsole.ThrowError("No valid waypoints to spawn sub: " + subName);
return null;
}
Debug.WriteLine("Possible waypoints for positioning subs: " + waypoints.Count);
Debug.WriteLine("First wp: " + waypoints.First().ID);
Debug.WriteLine("Last wp: " + waypoints.Last().ID);
var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
// Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
int padding = 1500;
@@ -3757,29 +3887,39 @@ namespace Barotrauma
var positions = new List<Vector2>();
var rects = new List<Rectangle>();
int maxAttempts = 50;
const int maxAttempts = 100;
int attemptsLeft = maxAttempts;
bool success = false;
WayPoint wayPoint = null;
Vector2 spawnPoint = Vector2.Zero;
var allCells = Loaded.GetAllCells();
int attempt = 0;
Loaded.GetAllCells();
var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom;
bool isReshuffled = false;
while (attemptsLeft > 0)
{
if (attemptsLeft < maxAttempts)
{
Debug.WriteLine($"Failed to position the sub {subName}. Trying again.");
Debug.WriteLine($"Failed to position the sub {subName}. Trying again (attempt: {attempt}/{maxAttempts}).");
}
if (!isReshuffled && attemptsLeft < 10)
{
DebugConsole.AddWarning($"Could not find a suitable position for {subName}. Reshuffling the waypoints and trying a few more times.");
waypoints.Shuffle(Rand.RandSync.ServerAndClient);
isReshuffled = true;
}
attemptsLeft--;
if (TryGetSpawnPoint(out spawnPoint))
if (TryGetWayPoint(ref wayPoint))
{
attempt++;
spawnPoint = wayPoint.WorldPosition;
success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
positionHistory.Add($"{info.Name}: {attempt}", positions.ToList());
positions.Clear();
if (success)
{
break;
}
else
{
positions.Clear();
}
}
else
{
@@ -3806,8 +3946,18 @@ namespace Barotrauma
Math.Max(hull.WaterVolume, hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient));
}
}
// Only spawn thalamus when the wreck has some thalamus items defined.
if ((forceThalamus || Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability) && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus")))
bool spawnThalamusByChance = Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability;
bool subHasThalamusItems = sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"));
bool spawnThalamus = thalamusSpawn switch
{
LevelData.ThalamusSpawn.Disabled => false,
LevelData.ThalamusSpawn.Forced => true,
LevelData.ThalamusSpawn.Random => spawnThalamusByChance,
_ => false
};
if (spawnThalamus && subHasThalamusItems)
{
if (!sub.CreateWreckAI())
{
@@ -3822,6 +3972,7 @@ namespace Barotrauma
}
else if (type == SubmarineType.BeaconStation)
{
// todo: implement spawning of thalamus for beacon stations
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub));
sub.ShowSonarMarker = false;
@@ -3832,7 +3983,6 @@ namespace Barotrauma
tempSW.Stop();
Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false);
wreckPositions.Add(sub, positions);
blockedRects.Add(sub, rects);
return sub;
}
@@ -3843,11 +3993,10 @@ namespace Barotrauma
}
bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint)
{
{
positions.Add(spawnPoint);
bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint);
positions.Add(spawnPoint);
bool leftSideBlocked = IsSideBlocked(subBorders, false);
bool rightSideBlocked = IsSideBlocked(subBorders, true);
int step = 5;
@@ -3871,14 +4020,14 @@ namespace Barotrauma
}
else
{
Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
Debug.WriteLine($"({info.Name}) Invalid position {spawnPoint}. Does not touch the ground.");
return false;
}
}
positions.Add(spawnPoint);
//shrink the bounds a bit to allow the sub to go slightly inside the wall
//(just enough that it doesn't look like it's floating)
int shrinkAmount = step + 50;
int shrinkAmount = step + 100;
Rectangle shrunkenBorders = new Rectangle(
subBorders.X + shrinkAmount,
subBorders.Y - shrinkAmount,
@@ -3887,19 +4036,32 @@ namespace Barotrauma
bool isBlocked = IsBlocked(spawnPoint, shrunkenBorders);
if (isBlocked)
{
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint() + subBorders.Location, subBorders.Size));
Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls.");
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size));
Debug.WriteLine($"({info.Name}) Invalid position {spawnPoint}. Blocked by level walls.");
}
else if (!bottomFound)
{
Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size));
Debug.WriteLine($"({info.Name}) Invalid position {spawnPoint}. Does not touch the ground.");
}
else
{
var sp = spawnPoint;
if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance))
float wreckMinDist = minDistance * minDistance;
float startMinDist = wreckMinDist * 2;
if (Vector2.DistanceSquared(sp, startPosition.ToVector2()) < startMinDist)
{
Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s).");
Debug.WriteLine($"({info.Name}) Invalid position {spawnPoint}. Too close to the start pos.");
return false;
}
if (Vector2.DistanceSquared(sp, endPosition.ToVector2()) < startMinDist)
{
Debug.WriteLine($"({info.Name}) Invalid position {spawnPoint}. Too close to the end pos.");
return false;
}
if (Wrecks.Any(w => Vector2.DistanceSquared(sp, w.WorldPosition) < wreckMinDist))
{
Debug.WriteLine($"({info.Name}) Invalid position {spawnPoint}. Too close to other wreck(s).");
return false;
}
}
@@ -3918,7 +4080,7 @@ namespace Barotrauma
spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y);
if (Math.Abs(totalAmount) > maxMovement)
{
Debug.WriteLine($"Moving the sub {subName} failed.");
Debug.WriteLine($"({info.Name}) Moving the sub {subName} failed.");
break;
}
}
@@ -3926,16 +4088,17 @@ namespace Barotrauma
}
}
bool TryGetSpawnPoint(out Vector2 spawnPoint)
bool TryGetWayPoint(ref WayPoint wp)
{
spawnPoint = Vector2.Zero;
while (waypoints.Any())
{
var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
waypoints.Remove(wp);
if (!IsBlocked(wp.WorldPosition, paddedBorders))
WayPoint previousWp = wp;
// Get the first waypoint from the randomized collection and the start going further from it, so that we have higher chances to end up close to the original position (matters for mirrored levels where the geometry can be slightly different).
WayPoint newWp = previousWp == null ? waypoints.Last() : waypoints.OrderBy(wP => Vector2.DistanceSquared(wP.Position, previousWp.Position)).First();
waypoints.Remove(newWp);
if (!IsBlocked(newWp.WorldPosition, paddedBorders))
{
spawnPoint = wp.WorldPosition;
wp = newWp;
return true;
}
}
@@ -4042,7 +4205,7 @@ namespace Barotrauma
}
// For debugging
private readonly Dictionary<Submarine, List<Vector2>> wreckPositions = new Dictionary<Submarine, List<Vector2>>();
private readonly Dictionary<string, List<Vector2>> positionHistory = new Dictionary<string, List<Vector2>>();
private readonly Dictionary<Submarine, List<Rectangle>> blockedRects = new Dictionary<Submarine, List<Rectangle>>();
private readonly record struct PlaceableWreck(WreckFile WreckFile, WreckInfo WreckInfo)
@@ -4064,6 +4227,24 @@ namespace Barotrauma
{
var totalSW = new Stopwatch();
totalSW.Start();
// Reset the seed to prevent the level geometry changes affecting the outcome (-> should get the same wrecks with the same seed, regardless of the other parameters).
ResetRandomSeed();
if (LevelData.ConsoleForceWreck != null)
{
LevelData.ForceWreck = LevelData.ConsoleForceWreck;
}
if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && !networkMember.ServerSettings.PvPSpawnWrecks)
{
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage("PvP setting: Skipping wreck generation", Color.Yellow);
}
Wrecks = new List<Submarine>();
return;
}
var placeableWrecks = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<WreckFile>())
@@ -4072,22 +4253,30 @@ namespace Barotrauma
.Where(w => w.IsSome())
.Select(o => o.TryUnwrap(out var w) ? w : throw new InvalidOperationException())
.ToList();
for (int i = placeableWrecks.Count - 1; i >= 0; i--)
if (LevelData.ForceWreck != null)
{
var wreckInfo = placeableWrecks[i].WreckInfo;
if (!IsAllowedDifficulty(wreckInfo.MinLevelDifficulty, wreckInfo.MaxLevelDifficulty))
DebugConsole.NewMessage($"Level Generation - Forcing wreck {LevelData.ForceWreck.DisplayName}");
}
else
{
for (int i = placeableWrecks.Count - 1; i >= 0; i--)
{
placeableWrecks.RemoveAt(i);
var wreckInfo = placeableWrecks[i].WreckInfo;
if (!IsAllowedDifficulty(wreckInfo.MinLevelDifficulty, wreckInfo.MaxLevelDifficulty))
{
placeableWrecks.RemoveAt(i);
}
}
if (placeableWrecks.None())
{
DebugConsole.ThrowError($"No wreck files found for the level difficulty {LevelData.Difficulty}!");
Wrecks = new List<Submarine>();
return;
}
placeableWrecks.Shuffle(Rand.RandSync.ServerAndClient);
}
if (placeableWrecks.None())
{
DebugConsole.ThrowError($"No wreck files found for the level difficulty {LevelData.Difficulty}!");
Wrecks = new List<Submarine>();
return;
}
placeableWrecks.Shuffle(Rand.RandSync.ServerAndClient);
int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, placeableWrecks.Count);
int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, placeableWrecks.Count);
@@ -4103,20 +4292,24 @@ namespace Barotrauma
{
requireThalamus = true;
}
if (LevelData.ForceWreck != null)
{
//force the desired wreck to be chosen first
var matchingFile = placeableWrecks.FirstOrDefault(w => w.WreckFile.Path == LevelData.ForceWreck.FilePath);
var matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath);
if (matchingFile.WreckFile != null)
{
placeableWrecks.Remove(matchingFile);
placeableWrecks.Insert(0, matchingFile);
if (LevelData.ForceThalamus == LevelData.ThalamusSpawn.Forced && matchingFile.WreckInfo.WreckContainsThalamus == WreckInfo.HasThalamus.No)
{
DebugConsole.ThrowError($"Forced wreck {LevelData.ForceWreck.DisplayName} can't have thalamus!");
}
}
wreckCount = Math.Max(wreckCount, 1);
}
if (requireThalamus)
else if (requireThalamus)
{
var thalamusWrecks = placeableWrecks
.Where(static w => w.WreckInfo.WreckContainsThalamus == WreckInfo.HasThalamus.Yes)
@@ -4145,20 +4338,31 @@ namespace Barotrauma
var placeableWreck = placeableWrecks.First();
var wreckFile = placeableWreck.WreckFile;
placeableWrecks.RemoveAt(0);
LevelData.ThalamusSpawn thalamusSpawn = requireThalamus ? LevelData.ThalamusSpawn.Forced : LevelData.ThalamusSpawn.Random;
if (LevelData.ForceWreck != null) { thalamusSpawn = LevelData.ForceThalamus; }
// disable thalamus spawning in pvp if monster spawning is disabled
if (GameMain.NetworkMember is { } netMember &&
GameMain.GameSession?.GameMode is PvPMode &&
!netMember.ServerSettings.PvPSpawnMonsters)
{
thalamusSpawn = LevelData.ThalamusSpawn.Disabled;
}
if (wreckFile == null) { continue; }
string wreckName = System.IO.Path.GetFileNameWithoutExtension(wreckFile.Path.Value);
if (SpawnSubOnPath(wreckName, wreckFile, SubmarineType.Wreck, forceThalamus: requireThalamus) is { } wreck)
if (SpawnSubOnPath(wreckName, wreckFile, SubmarineType.Wreck, thalamusSpawn: thalamusSpawn) is Submarine wreck)
{
if (wreck.WreckAI is not null)
{
requireThalamus = false;
}
//placed successfully
// Disabled at least for now, because labels etc. can cause issues.
// if (Mirrored)
// {
// wreck.FlipX();
// }
break;
}
attempts++;
}
}
totalSW.Stop();
Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)");
@@ -4200,6 +4404,9 @@ namespace Barotrauma
private void CreateOutposts()
{
// Reset the seed so the outposts keep the same on mirrored levels.
ResetRandomSeed();
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.OrderBy(f => f.UintIdentifier).ToList();
@@ -4209,10 +4416,10 @@ namespace Barotrauma
return;
}
// This breaks the determinism between mirrored and non-mirrored levels, because the random calls will come in different order.
// TODO: Should refactor the code so that the order keeps the same regardless of whether the level is mirrored or not (start outpost needs to be generated first, then the end outpost)
for (int i = 0; i < 2; i++)
{
if (GameMain.GameSession?.GameMode is PvPMode) { continue; }
bool isStart = (i == 0) == !Mirrored;
if (isStart)
{
@@ -4233,7 +4440,24 @@ namespace Barotrauma
{
Location location = isStart ? StartLocation : EndLocation;
OutpostGenerationParams outpostGenerationParams = null;
if (LevelData.ForceOutpostGenerationParams != null)
Identifier missionForcedOutpostParamsId = Identifier.Empty;
if (GameMain.GameSession?.GameMode?.Missions is IEnumerable<Mission> missions)
{
foreach (var mission in missions)
{
if (!mission.Prefab.ForceOutpostGenerationParameters.IsEmpty)
{
missionForcedOutpostParamsId = mission.Prefab.ForceOutpostGenerationParameters;
break;
}
}
}
if (missionForcedOutpostParamsId != null &&
OutpostGenerationParams.OutpostParams.TryGet(missionForcedOutpostParamsId, out var missionForcedOutpostParams))
{
outpostGenerationParams = missionForcedOutpostParams;
}
else if (LevelData.ForceOutpostGenerationParams != null)
{
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
}
@@ -4294,6 +4518,11 @@ namespace Barotrauma
outpostInfo.Type = SubmarineType.Outpost;
outpost = new Submarine(outpostInfo);
}
// Outposts tend to have labels, flags, and other texts that can't be flipped.
// if (Mirrored)
// {
// outpost.FlipX();
// }
Point? minSize = null;
DockingPort subPort = null;
@@ -4420,7 +4649,22 @@ namespace Barotrauma
private void CreateBeaconStation()
{
if (!LevelData.HasBeaconStation && LevelData.ForceBeaconStation == null && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; }
// Reset the seed to prevent the wreck generation affecting the outcome (-> should get the same beacons with the same seed, regardless of the other parameters).
ResetRandomSeed();
if (LevelData.ConsoleForceBeaconStation != null)
{
LevelData.ForceBeaconStation = LevelData.ConsoleForceBeaconStation;
}
bool missionRequiresBeaconStation = GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireBeaconStation) ?? false;
if (!missionRequiresBeaconStation &&!LevelData.HasBeaconStation && LevelData.ForceBeaconStation == null && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation))
{
return;
}
bool spawnInMiddle = GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireBeaconStation && m.Prefab.SpawnBeaconStationInMiddle) ?? false;
var beaconStationFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<BeaconStationFile>())
.OrderBy(f => f.UintIdentifier).ToList();
@@ -4470,12 +4714,17 @@ namespace Barotrauma
}
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation);
BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation, spawnInTheMiddle: spawnInMiddle);
if (BeaconStation == null)
{
LevelData.HasBeaconStation = false;
return;
}
// Disabled at least for now, because labels etc. can cause issues.
// if (Mirrored)
// {
// BeaconStation.FlipX();
// }
Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent<Sonar>() != null);
if (sonarItem == null)
@@ -4633,7 +4882,7 @@ namespace Barotrauma
public bool CheckBeaconActive()
{
if (beaconSonar == null) { return false; }
return beaconSonar.Voltage > beaconSonar.MinVoltage && beaconSonar.CurrentMode == Sonar.Mode.Active;
return beaconSonar.HasPower && beaconSonar.CurrentMode == Sonar.Mode.Active;
}
private void SetLinkedSubCrushDepth(Submarine parentSub)
@@ -4664,7 +4913,7 @@ namespace Barotrauma
var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
pathPoints.Shuffle(Rand.RandSync.Unsynced);
pathPoints.Shuffle(Rand.RandSync.ServerAndClient);
// Sort by job so that we first spawn those with a predefined job (might have special id cards)
corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
var usedJobs = new HashSet<JobPrefab>();
@@ -4717,49 +4966,54 @@ namespace Barotrauma
// Only spawn one of these jobs per wreck
usedJobs.Add(job);
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient);
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job);
var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
selectedPrefab.GiveItems(corpse, wreck, sp);
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200));
bool applyBurns = Rand.Value() < 0.1f;
bool applyDamage = Rand.Value() < 0.3f;
foreach (var limb in corpse.AnimController.Limbs)
bool spawnAsHusk = Rand.Value() <= Loaded.GenerationParams.HuskProbability;
if (spawnAsHusk)
{
if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f))
corpse.TurnIntoHusk(playDead: true);
}
else
{
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(AfflictionPrefab.OxygenLow.MaxStrength));
bool applyBurns = Rand.Value() < 0.1f;
bool applyDamage = Rand.Value() < 0.3f;
foreach (var limb in corpse.AnimController.Limbs)
{
var prefab = AfflictionPrefab.BiteWounds;
float max = prefab.MaxStrength / prefab.DamageOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
if (applyBurns)
{
var prefab = AfflictionPrefab.Burn;
float max = prefab.MaxStrength / prefab.BurnOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
static float GetStrength(Limb limb, float max)
{
float strength = Rand.Range(0, max);
if (limb.type != LimbType.Head)
if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f))
{
strength = Math.Min(strength, Rand.Range(0, max));
var prefab = AfflictionPrefab.BiteWounds;
float max = prefab.MaxStrength / prefab.DamageOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
if (applyBurns)
{
var prefab = AfflictionPrefab.Burn;
float max = prefab.MaxStrength / prefab.BurnOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
static float GetStrength(Limb limb, float max)
{
float strength = Rand.Range(0, max);
if (limb.type != LimbType.Head)
{
strength = Math.Min(strength, Rand.Range(0, max));
}
return strength;
}
return strength;
}
corpse.CharacterHealth.ForceUpdateVisuals();
}
corpse.CharacterHealth.ForceUpdateVisuals();
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
if (selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
{
corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced));
corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney));
}
spawnCounter++;
static CorpsePrefab GetCorpsePrefab(HashSet<JobPrefab> usedJobs, Func<CorpsePrefab, bool> predicate = null)
@@ -4816,6 +5070,15 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the position above the upper boundary of the level ("outside bounds", where nothing should be able to get to normally)?
/// E.g. respawn shuttles are moved above the level when they despawn.
/// </summary>
public static bool IsPositionAboveLevel(Vector2 worldPosition)
{
return Loaded != null && worldPosition.Y > Loaded.Size.Y;
}
public void DebugSetStartLocation(Location newStartLocation)
{
StartLocation = newStartLocation;
@@ -4850,7 +5113,7 @@ namespace Barotrauma
PathPoints?.Clear();
PositionsOfInterest?.Clear();
wreckPositions?.Clear();
positionHistory?.Clear();
Wrecks?.Clear();
BeaconStation = null;
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.RuinGeneration;
namespace Barotrauma
{
@@ -47,6 +48,19 @@ namespace Barotrauma
public SubmarineInfo ForceWreck;
public RuinGenerationParams ForceRuinGenerationParams;
public enum ThalamusSpawn
{
Random,
Forced,
Disabled
}
public static SubmarineInfo ConsoleForceWreck;
public static SubmarineInfo ConsoleForceBeaconStation;
public static ThalamusSpawn ForceThalamus = ThalamusSpawn.Random;
public bool AllowInvalidOutpost;
public readonly Point Size;
@@ -216,10 +230,11 @@ namespace Barotrauma
public LevelData(LocationConnection locationConnection)
{
Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed;
bool connectionIsBiomeTransition = locationConnection.Locations[0].Biome.Identifier != locationConnection.Locations[1].Biome.Identifier;
Biome = locationConnection.Biome;
Type = LevelType.LocationConnection;
Difficulty = locationConnection.Difficulty;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier);
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier, biomeTransition: connectionIsBiomeTransition);
float sizeFactor = MathUtils.InverseLerp(
MapGenerationParams.Instance.SmallLevelConnectionLength,
@@ -264,7 +279,7 @@ namespace Barotrauma
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
}
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, bool requireOutpost = false)
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, Identifier biomeId = default, bool requireOutpost = false, bool pvpOnly = false)
{
if (string.IsNullOrEmpty(seed))
{
@@ -273,14 +288,21 @@ namespace Barotrauma
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
LevelType type = generationParams == null ?
(requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) :
generationParams.Type;
LevelType type = generationParams?.Type ??
(requireOutpost
? LevelType.Outpost
: LevelType.LocationConnection);
float selectedDifficulty = difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient);
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type, selectedDifficulty); }
var biome =
Biome biome = null;
if (!biomeId.IsEmpty && biomeId != "Random")
{
Biome.Prefabs.TryGet(biomeId, out biome);
}
generationParams ??= LevelGenerationParams.GetRandom(seed, type, selectedDifficulty, pvpOnly: pvpOnly, biomeId: biomeId);
biome ??=
Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ??
Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
@@ -11,6 +11,9 @@ namespace Barotrauma
{
public readonly static PrefabCollection<LevelGenerationParams> LevelParams = new PrefabCollection<LevelGenerationParams>();
public LocalizedString DisplayName { get; private set; }
public LocalizedString Description { get; private set; }
public string Name => Identifier.Value;
public Identifier OldIdentifier { get; }
@@ -68,12 +71,18 @@ namespace Barotrauma
set;
}
[Serialize(false, IsPropertySaveable.Yes, "If the given level is only used in PvP modes"), Editable]
public bool IsPvPLevel { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float Commonness
{
get;
set;
}
[Serialize(false, IsPropertySaveable.Yes, "If the level is a transition from the previous biome to this one."), Editable]
public bool TransitionFromPreviousBiome { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be above or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float MinLevelDifficulty
@@ -497,6 +506,9 @@ namespace Barotrauma
[Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MaxRuinCount { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability of spawning a ruin in the level. If the level can have multiple ruins, the probability is evaluated separately for each."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2)]
public float RuinSpawnProbability { get; set; }
// TODO: Move the wreck parameters under a separate class?
#region Wreck parameters
@@ -512,6 +524,9 @@ namespace Barotrauma
[Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MaxCorpseCount { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float HuskProbability { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float ThalamusProbability { get; set; }
@@ -673,7 +688,7 @@ namespace Barotrauma
}
}
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default)
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default, bool pvpOnly = false, bool biomeTransition = false)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
@@ -688,7 +703,41 @@ namespace Barotrauma
lp.Type == type &&
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
if (biomeId.IsEmpty)
if (biomeTransition)
{
var biomeTransitionParams = matchingLevelParams.Where(lp =>
lp.TransitionFromPreviousBiome && lp.AllowedBiomeIdentifiers.Contains(biomeId));
if (biomeTransitionParams.Any())
{
return ToolBox.SelectWeightedRandom(biomeTransitionParams, p => p.Commonness, Rand.RandSync.ServerAndClient);
}
}
else
{
matchingLevelParams = matchingLevelParams.Where(lp => !lp.TransitionFromPreviousBiome);
}
if (pvpOnly)
{
var pvpOnlyLevels = matchingLevelParams.Where(static lp => lp.IsPvPLevel);
if (pvpOnlyLevels.Any())
{
matchingLevelParams = pvpOnlyLevels;
}
else
{
DebugConsole.AddWarning("No PvP specific level generation presets found - using all level generation presets instead.");
}
}
else
{
matchingLevelParams = matchingLevelParams.Where(static lp => !lp.IsPvPLevel);
}
if (biomeId.IsEmpty || biomeId == "Random")
{
//we don't want end levels when generating a completely random level (e.g. in mission mode)
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome));
@@ -746,6 +795,20 @@ namespace Barotrauma
allowedBiomeIdentifiers.Remove("any".ToIdentifier());
AllowedBiomeIdentifiers = allowedBiomeIdentifiers.ToImmutableHashSet();
DisplayName = TextManager.Get($"levelname.{Identifier}");
Description = TextManager.Get($"leveldescription.{Identifier}");
var nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
var descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", Identifier.Empty);
if (!nameIdentifier.IsEmpty)
{
DisplayName = TextManager.Get(nameIdentifier);
}
if (!descriptionIdentifier.IsEmpty)
{
Description = TextManager.Get(descriptionIdentifier);
}
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -178,12 +178,21 @@ namespace Barotrauma
{
float minDistance = level.Size.X * 0.2f;
bool allowAtStart = prefab.AllowAtStart;
bool allowAtEnd = prefab.AllowAtEnd;
if (GameMain.GameSession?.GameMode is PvPMode)
{
//in PvP mode, the object must be allowed at both the start and end to be placed at either end
//since the 2nd team starts at the end of the level, it'd be unfair to allow e.g. ballast flora to spawn at the end of the level but not the start
allowAtEnd = allowAtStart = allowAtEnd && allowAtStart;
}
suitableSpawnPositions.Add(prefab,
availableSpawnPositions.Where(sp =>
sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) &&
sp.Length >= prefab.MinSurfaceWidth &&
(prefab.AllowAtStart || !level.IsCloseToStart(sp.GraphEdge.Center, minDistance)) &&
(prefab.AllowAtEnd || !level.IsCloseToEnd(sp.GraphEdge.Center, minDistance)) &&
(allowAtStart || !level.IsCloseToStart(sp.GraphEdge.Center, minDistance)) &&
(allowAtEnd || !level.IsCloseToEnd(sp.GraphEdge.Center, minDistance)) &&
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
spawnPositionWeights.Add(prefab,
@@ -6,7 +6,9 @@ using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -55,6 +57,8 @@ namespace Barotrauma
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly TriggererType triggeredBy;
private readonly Identifier triggerSpeciesOrGroup;
private readonly PropertyConditional.LogicalComparison conditionals;
private readonly float randomTriggerInterval;
private readonly float randomTriggerProbability;
@@ -255,7 +259,16 @@ namespace Barotrauma
string triggeredByStr = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByStr, out triggeredBy))
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
Identifier speciesOrGroup = triggeredByStr.ToIdentifier();
if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup)))
{
triggerSpeciesOrGroup = speciesOrGroup;
triggeredBy = TriggererType.Character;
}
else
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
}
}
if (PhysicsBody != null)
{
@@ -293,6 +306,8 @@ namespace Barotrauma
break;
}
}
conditionals = PropertyConditional.LoadConditionals(element);
forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval);
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
@@ -341,7 +356,7 @@ namespace Barotrauma
{
Entity entity = GetEntity(fixtureB);
if (entity == null) { return false; }
if (!IsTriggeredByEntity(entity, triggeredBy, mustBeOutside: true)) { return false; }
if (!IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup: triggerSpeciesOrGroup, conditionals: conditionals, mustBeOutside: true)) { return false; }
if (!triggerers.Contains(entity))
{
if (!IsTriggered)
@@ -354,12 +369,22 @@ namespace Barotrauma
return true;
}
public static bool IsTriggeredByEntity(Entity entity, TriggererType triggeredBy, bool mustBeOutside = false, (bool mustBe, Submarine sub) mustBeOnSpecificSub = default)
public static bool IsTriggeredByEntity(
Entity entity,
TriggererType triggeredBy,
Identifier triggerSpeciesOrGroup,
PropertyConditional.LogicalComparison conditionals,
(bool mustBe, Submarine sub) mustBeOnSpecificSub = default,
bool mustBeOutside = false)
{
if (entity is Character character)
{
if (mustBeOutside && character.CurrentHull != null) { return false; }
if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; }
if (!triggerSpeciesOrGroup.IsEmpty)
{
if (character.SpeciesName != triggerSpeciesOrGroup && character.Group != triggerSpeciesOrGroup) { return false; }
}
if (character.IsHuman)
{
if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; }
@@ -379,6 +404,10 @@ namespace Barotrauma
{
if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; }
}
if (conditionals != null && entity is ISerializableEntity serializableEntity)
{
if (!PropertyConditional.CheckConditionals(serializableEntity, conditionals.Conditionals, conditionals.LogicalOperator)) { return false; }
}
return true;
}
@@ -497,7 +526,7 @@ namespace Barotrauma
triggeredTimer = stayTriggeredDelay;
if (!wasAlreadyTriggered)
{
if (!IsTriggeredByEntity(triggerer, triggeredBy, mustBeOutside: true)) { return; }
if (!IsTriggeredByEntity(triggerer, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOutside: true)) { return; }
if (!triggerers.Contains(triggerer))
{
if (!IsTriggered)
@@ -652,13 +681,20 @@ namespace Barotrauma
}
}
public static void ApplyStatusEffects(List<StatusEffect> statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List<ISerializableEntity> targets)
public static void ApplyStatusEffects(List<StatusEffect> statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List<ISerializableEntity> targets, Item targetItem = null)
{
foreach (StatusEffect effect in statusEffects)
{
if (effect.type == ActionType.OnBroken) { return; }
Vector2? position = null;
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = worldPosition; }
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
position = worldPosition;
if (targetItem != null)
{
effect.Apply(effect.type, deltaTime, triggerer, targetItem.AllPropertyObjects, position);
}
}
if (triggerer is Character character)
{
effect.Apply(effect.type, deltaTime, triggerer, character, position);
@@ -728,6 +764,8 @@ namespace Barotrauma
if (distFactor < 0.0f) return;
}
if (MathUtils.NearlyEqual(currentForceFluctuation, 0.0f)) { return; }
switch (ForceMode)
{
case TriggerForceMode.Force:
@@ -3,6 +3,7 @@ using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -62,6 +63,8 @@ namespace Barotrauma
private int nameFormatIndex;
private Identifier nameIdentifier;
public int NameFormatIndex => nameFormatIndex;
/// <summary>
/// For backwards compatibility: a non-localizable name from the old text files.
/// </summary>
@@ -80,7 +83,7 @@ namespace Barotrauma
/// <summary>
/// Is some mission blocking this location from changing its type, or have location type changes been forcibly disabled on the location?
/// </summary>
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges);
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => !m.Completed && m.Prefab.BlockLocationTypeChanges);
public bool DisallowLocationTypeChanges;
@@ -1177,9 +1180,22 @@ namespace Barotrauma
}
}
private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
public static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
{
if (type?.NameFormats == null || !type.NameFormats.Any())
if (LocationType.Prefabs.TryGet(locationTypeIdentifier, out LocationType locationType))
{
return GetName(locationType, nameFormatIndex, nameId);
}
else
{
DebugConsole.ThrowError($"Could not find the location type {locationTypeIdentifier}.\n" + Environment.StackTrace.CleanUpPath());
return new RawLString(nameId.Value);
}
}
public static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
{
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
{
return TextManager.Get(nameId);
}
@@ -33,6 +33,11 @@ namespace Barotrauma
public readonly CharacterTeamType OutpostTeam;
/// <summary>
/// Is this location type considered valid for e.g. events and missions that are should be available in "any outpost"
/// </summary>
public bool IsAnyOutpost;
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public readonly ImmutableArray<Identifier> MissionIdentifiers;
@@ -160,6 +165,8 @@ namespace Barotrauma
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
IsAnyOutpost = element.GetAttributeBool(nameof(IsAnyOutpost), def: HasOutpost);
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
@@ -179,7 +186,7 @@ namespace Barotrauma
try
{
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
names.AddRange(File.ReadAllLines(path.Value).ToList());
names.AddRange(File.ReadAllLines(path.Value, catchUnauthorizedAccessExceptions: false).ToList());
}
catch (Exception e)
{
@@ -449,17 +449,6 @@ namespace Barotrauma
}
LocationType forceLocationType = null;
if (!possibleStartOutpostCreated)
{
float zoneWidth = Width / generationParams.DifficultyZones;
float threshold = zoneWidth * 0.1f;
if (position.X < threshold)
{
LocationType.Prefabs.TryGet("outpost", out forceLocationType);
possibleStartOutpostCreated = true;
}
}
if (forceLocationType == null)
{
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
@@ -604,6 +593,10 @@ namespace Barotrauma
{
connectionsBetweenZones[zone1].Add(connection);
}
if (connectionsBetweenZones[zone1].None())
{
DebugConsole.ThrowError($"Potential error during map generation: no connections between zones {zone1} and {zone2} found. Traversing through to the end of the map may be impossible.");
}
}
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
@@ -673,8 +666,9 @@ namespace Barotrauma
connection.Locations[0] :
connection.Locations[1];
//if there's only one connection (= the connection between biomes), create a new connection to the closest location to the right
if (rightMostLocation.Connections.Count == 1)
//if all of the other connected locations are to the left (= if there's no path forwards from the outpost),
//create a new connection to the closest location to the right
if (rightMostLocation.Connections.All(c => c.OtherLocation(rightMostLocation).MapPosition.X < rightMostLocation.MapPosition.X))
{
Location closestLocation = null;
float closestDist = float.PositiveInfinity;
@@ -714,6 +708,13 @@ namespace Barotrauma
}
}
//ensure there's an outpost (a valid starting location) at the very left side of the map
Location startLocation = Locations.MinBy(l => l.MapPosition.X);
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
{
startLocation.ChangeType(campaign, startLocationType, createStores: false);
}
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
@@ -763,7 +764,7 @@ namespace Barotrauma
partial void GenerateLocationConnectionVisuals(LocationConnection connection);
private int GetZoneIndex(float xPos)
public int GetZoneIndex(float xPos)
{
float zoneWidth = Width / generationParams.DifficultyZones;
return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
@@ -10,19 +10,17 @@ namespace Barotrauma
class MapGenerationParams : Prefab, ISerializableEntity
{
public static readonly PrefabSelector<MapGenerationParams> Params = new PrefabSelector<MapGenerationParams>();
public static MapGenerationParams Instance
{
get
{
return Params.ActivePrefab;
}
get { return Params.ActivePrefab; }
}
#if DEBUG
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowLocations { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool ShowLevelTypeNames { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
@@ -49,8 +47,8 @@ namespace Barotrauma
public float LargeLevelConnectionLength { get; set; }
[Serialize("20,20", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " +
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
public Point VoronoiSiteInterval { get; set; }
[Serialize("5,5", IsPropertySaveable.Yes), Editable]
@@ -259,7 +259,7 @@ namespace Barotrauma
if (string.IsNullOrWhiteSpace(AllowedUpgrades)) { return Enumerable.Empty<Identifier>(); }
if (allowedUpgradeSet is null || cachedAllowedUpgrades != AllowedUpgrades)
{
allowedUpgradeSet = AllowedUpgrades.Split(",").ToIdentifiers().ToImmutableHashSet();
allowedUpgradeSet = AllowedUpgrades.ToIdentifiers().ToImmutableHashSet();
cachedAllowedUpgrades = AllowedUpgrades;
}
@@ -43,7 +43,7 @@ namespace Barotrauma
}
}
public void Save(XElement element)
public virtual void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
}
@@ -126,4 +126,46 @@ namespace Barotrauma
WreckContainsThalamus = HasThalamus.No;
}
}
class EnemySubmarineInfo : ExtraSubmarineInfo
{
[Serialize(4000.0f, IsPropertySaveable.Yes), Editable]
public float Reward { get; set; }
[Serialize(50.0f, IsPropertySaveable.Yes), Editable]
public float PreferredDifficulty { get; set; }
private readonly HashSet<Identifier> missionTags = new HashSet<Identifier>();
public HashSet<Identifier> MissionTags => missionTags;
public EnemySubmarineInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element)
{
Name = $"{nameof(EnemySubmarineInfo)} ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (var missionTag in element.GetAttributeIdentifierArray(nameof(MissionTags), Array.Empty<Identifier>()))
{
missionTags.Add(missionTag);
}
}
public EnemySubmarineInfo(SubmarineInfo submarineInfo) : base(submarineInfo)
{
Name = $"{nameof(EnemySubmarineInfo)} ({submarineInfo.Name})";
}
public EnemySubmarineInfo(EnemySubmarineInfo original) : base(original)
{
foreach (var missionTag in original.missionTags)
{
missionTags.Add(missionTag);
}
}
public override void Save(XElement element)
{
base.Save(element);
element.Add(new XAttribute(nameof(MissionTags), string.Join(',', missionTags)));
}
}
}
@@ -140,24 +140,44 @@ namespace Barotrauma
public ContentPath OutpostFilePath { get; set; }
public class ModuleCount
[Serialize("", IsPropertySaveable.Yes, description: "If set, a fully pre-built outpost with this tag will be used instead of generating the outpost."), Editable]
public Identifier OutpostTag { get; set; }
public class ModuleCount : ISerializableEntity
{
public Identifier Identifier;
public int Count;
public int Order;
public Identifier RequiredFaction;
[Serialize(0, IsPropertySaveable.Yes), Editable]
public int Count { get; set; }
[Serialize(0, IsPropertySaveable.Yes, description: "Can be used to enforce the modules to be placed in a specific order, starting from the docking module (0 = first, 1 = second, etc)."), Editable]
public int Order { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum difficulty of the current level for the module to appear in the outpost."), Editable]
public float MinDifficulty { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Maximum difficulty of the current level for the module to appear in the outpost."), Editable]
public float MaxDifficulty { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Probability for this type of module to be included in the outpost."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float Probability { get; set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
public Identifier RequiredFaction { get; set; }
public string Name => Identifier.Value;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
public ModuleCount(ContentXElement element)
{
Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", ""));
Count = element.GetAttributeInt("count", 0);
Order = element.GetAttributeInt("order", 0);
RequiredFaction = element.GetAttributeIdentifier("requiredfaction", Identifier.Empty);
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public ModuleCount(Identifier id, int count)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element: null);
Identifier = id;
Count = count;
RequiredFaction = Identifier.Empty;
@@ -261,6 +281,7 @@ namespace Barotrauma
}
OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath));
OutpostTag = element.GetAttributeIdentifier(nameof(OutpostTag), Identifier.Empty);
var humanPrefabCollections = new List<NpcCollection>();
foreach (var subElement in element.Elements())
@@ -268,7 +289,23 @@ namespace Barotrauma
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "modulecount":
moduleCounts.Add(new ModuleCount(subElement));
var newModuleCount = new ModuleCount(subElement);
if (moduleCounts.None() && newModuleCount.Probability < 1.0f)
{
DebugConsole.AddWarning(
$"Potential error in outpost generation parameters \"{Identifier}\"." +
$" The first module is set to spawn with a probability of {newModuleCount.Probability}%. The first module must always spawn, so the probability will be ignored.",
contentPackage: ContentPackage);
newModuleCount.Probability = 1.0f;
}
else if (newModuleCount.Probability <= 0.0f)
{
DebugConsole.AddWarning(
$"Potential error in outpost generation parameters \"{Identifier}\"." +
$" Probability of the module {newModuleCount.Identifier} is 0% (the module should never spawn, so there's no reason to include it in the generation parameters.",
contentPackage: ContentPackage);
}
moduleCounts.Add(newModuleCount);
break;
case "npcs":
var newCollection = new NpcCollection();
@@ -299,7 +336,7 @@ namespace Barotrauma
return moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag)?.Count ?? 0;
}
public void SetModuleCount(Identifier moduleFlag, int count)
public void SetModuleCount(Identifier moduleFlag, int count, float? probability = null, float? minDifficulty = null, float? maxDifficulty = null)
{
if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; }
if (count <= 0)
@@ -311,12 +348,20 @@ namespace Barotrauma
var moduleCount = moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag);
if (moduleCount == null)
{
moduleCounts.Add(new ModuleCount(moduleFlag, count));
}
else
{
moduleCount.Count = count;
moduleCount = new ModuleCount(moduleFlag, count);
if (moduleCount.Probability <= 0.0f)
{
DebugConsole.AddWarning(
$"Potential error in outpost generation parameters \"{Identifier}\"."+
$" Probability of the module {moduleCount.Identifier} is 0 (the module should never spawn, so there's no reason to include it in the generation parameters.",
contentPackage: ContentPackage);
}
moduleCounts.Add(moduleCount);
}
moduleCount.Count = count;
if (probability.HasValue) { moduleCount.Probability = probability.Value; }
if (minDifficulty.HasValue) { moduleCount.MinDifficulty = minDifficulty.Value; }
if (maxDifficulty.HasValue) { moduleCount.MaxDifficulty = maxDifficulty.Value; }
}
}
@@ -330,12 +375,15 @@ namespace Barotrauma
}
}
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(IEnumerable<FactionPrefab> factions, Rand.RandSync randSync)
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(IEnumerable<FactionPrefab> factions, Submarine sub, Rand.RandSync randSync)
{
if (!humanPrefabCollections.Any()) { return Array.Empty<HumanPrefab>(); }
var collection = humanPrefabCollections.GetRandom(randSync);
return collection.GetByFaction(factions).ToImmutableList();
return collection
.GetByFaction(factions)
.Where(humanPrefab => !humanPrefab.RequireSpawnPointTag || WayPoint.WayPointList.Any(wp => wp.Submarine == sub && humanPrefab.GetSpawnPointTags().Any(tag => wp.Tags.Contains(tag))))
.ToImmutableList();
}
public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
@@ -66,6 +66,8 @@ namespace Barotrauma
return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost);
}
private static SubmarineInfo usedForceOutpostModule;
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
@@ -87,6 +89,69 @@ namespace Barotrauma
locationType = location.GetLocationType();
}
Submarine sub = null;
if (generationParams.OutpostTag.IsEmpty)
{
var forceOutpostModule = GameMain.GameSession?.ForceOutpostModule;
sub = GenerateFromModules(generationParams, outpostModuleFiles, sub, locationType, location, onlyEntrance, allowInvalidOutpost);
if (sub != null)
{
return sub;
}
else if (forceOutpostModule != null)
{
//failed to force the module, abort
return null;
}
}
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
.OrderBy(f => f.UintIdentifier).ToList();
List<SubmarineInfo> outpostInfos = new List<SubmarineInfo>();
foreach (var outpostFile in outpostFiles)
{
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
}
if (!generationParams.OutpostTag.IsEmpty)
{
if (outpostInfos.Any(o => o.OutpostTags.Contains(generationParams.OutpostTag)))
{
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.Contains(generationParams.OutpostTag));
}
else
{
DebugConsole.ThrowError($"Could not find any outposts with the tag {generationParams.OutpostTag}. Choosing a random one instead...");
}
}
if (!outpostInfos.Any())
{
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
}
var prebuiltOutpostInfo = outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
serverSettings.SelectedOutpostName != "Random")
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
if (matchingOutpost != null)
{
prebuiltOutpostInfo = matchingOutpost;
}
}
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
sub = new Submarine(prebuiltOutpostInfo);
sub.Info.OutpostGenerationParams = generationParams;
location?.RemoveTakenItems();
EnableFactionSpecificEntities(sub, location);
return sub;
}
private static Submarine GenerateFromModules(OutpostGenerationParams generationParams, OutpostModuleFile[] outpostModuleFiles, Submarine sub, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
//load the infos of the outpost module files
List<SubmarineInfo> outpostModules = new List<SubmarineInfo>();
foreach (var outpostModuleFile in outpostModuleFiles)
@@ -114,7 +179,6 @@ namespace Barotrauma
List<PlacedModule> selectedModules = new List<PlacedModule>();
bool generationFailed = false;
int remainingTries = 5;
Submarine sub = null;
while (remainingTries > -1 && outpostModules.Any())
{
if (sub != null)
@@ -140,10 +204,10 @@ namespace Barotrauma
GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveRange(uniqueEventCount, GameMain.Server.EntityEventManager.UniqueEvents.Count - uniqueEventCount);
#endif
if (remainingTries <= 0)
if (remainingTries <= 0)
{
generationFailed = true;
break;
break;
}
}
@@ -180,9 +244,18 @@ namespace Barotrauma
//the first module is spawned separately, remove it from the list of pending modules
Identifier initialModuleFlag = pendingModuleFlags.FirstOrDefault().IfEmpty("airlock".ToIdentifier());
pendingModuleFlags.Remove(initialModuleFlag);
pendingModuleFlags.Remove(initialModuleFlag);
bool hasForceOutpostWithInitialFlag = GameMain.GameSession?.ForceOutpostModule != null && GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(initialModuleFlag);
var initialModule = hasForceOutpostWithInitialFlag ? GameMain.GameSession.ForceOutpostModule : GetRandomModule(outpostModules, initialModuleFlag, locationType);
if (hasForceOutpostWithInitialFlag)
{
DebugConsole.NewMessage($"Using Force outpost module as initial in Outpost generation: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}", Color.Yellow);
usedForceOutpostModule = GameMain.GameSession.ForceOutpostModule;
GameMain.GameSession.ForceOutpostModule = null;
}
var initialModule = GetRandomModule(outpostModules, initialModuleFlag, locationType);
if (initialModule == null)
{
throw new Exception("Failed to generate an outpost (no airlock modules found).");
@@ -202,12 +275,24 @@ namespace Barotrauma
selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag);
AppendToModule(
selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags,
selectedModules,
locationType,
selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags,
selectedModules,
locationType,
allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams,
allowDifferentLocationType: remainingTries == 1);
if (GameMain.GameSession?.ForceOutpostModule != null)
{
if (remainingTries > 0)
{
remainingTries--;
continue;
}
DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
GameMain.GameSession.ForceOutpostModule = null;
return null;
}
if (pendingModuleFlags.Any(flag => flag != "none"))
{
if (!allowInvalidOutpost)
@@ -255,34 +340,13 @@ namespace Barotrauma
}
}
EnableFactionSpecificEntities(sub, location);
return sub;
return sub;
}
remainingTries--;
}
#if DEBUG
DebugConsole.ThrowError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
#else
DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
#endif
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
.OrderBy(f => f.UintIdentifier).ToArray();
if (!outpostFiles.Any())
{
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
}
var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.ServerAndClient).Path.Value)
{
Type = SubmarineType.Outpost
};
sub = new Submarine(prebuiltOutpostInfo);
sub.Info.OutpostGenerationParams = generationParams;
location?.RemoveTakenItems();
EnableFactionSpecificEntities(sub, location);
return sub;
DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
return null;
List<MapEntity> loadEntities(Submarine sub)
{
@@ -293,13 +357,18 @@ namespace Barotrauma
var selectedModule = selectedModules[i];
sub.Info.GameVersion = selectedModule.Info.GameVersion;
var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset);
if (usedForceOutpostModule != null && usedForceOutpostModule == selectedModule.Info)
{
sub.ForcedOutpostModuleWayPoints = moduleEntities.OfType<WayPoint>().ToList();
}
MapEntity.InitializeLoadedLinks(moduleEntities);
foreach (MapEntity entity in moduleEntities.ToList())
{
entity.OriginalModuleIndex = i;
if (!(entity is Item item)) { continue; }
if (entity is not Item item) { continue; }
var door = item.GetComponent<Door>();
if (door != null)
{
@@ -319,7 +388,15 @@ namespace Barotrauma
{
hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags);
}
if (Screen.Selected is { IsEditor: false })
{
foreach (Identifier layer in selectedModule.Info.LayersHiddenByDefault)
{
Submarine.SetLayerEnabled(layer, enabled: false, entities: moduleEntities);
}
}
if (!hullEntities.Any())
{
selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint());
@@ -388,25 +465,25 @@ namespace Barotrauma
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0)
{
overlapsFound = true;
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary<PlacedModule,Vector2> solution))
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary<PlacedModule, Vector2> solution))
{
foreach (KeyValuePair<PlacedModule, Vector2> kvp in solution)
{
kvp.Key.Offset += kvp.Value;
}
}
}
else
{
break;
}
remainingTries--;
}
remainingTries--;
}
}
iteration++;
if (iteration > 10)
if (iteration > 10)
{
generationFailed = true;
break;
break;
}
}
@@ -438,10 +515,10 @@ namespace Barotrauma
//eww
structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth);
#if CLIENT
foreach (var light in structure.Lights)
{
light.IsBackground = true;
}
foreach (var light in structure.Lights)
{
light.IsBackground = true;
}
#endif
}
}
@@ -478,21 +555,42 @@ namespace Barotrauma
private static List<Identifier> SelectModules(IEnumerable<SubmarineInfo> modules, Location location, OutpostGenerationParams generationParams)
{
int totalModuleCount = generationParams.TotalModuleCount;
int totalModuleCountExcludingOptional = totalModuleCount - generationParams.ModuleCounts.Count(m => m.Probability < 1.0f);
var pendingModuleFlags = new List<Identifier>();
bool availableModulesFound = true;
Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Identifier;
pendingModuleFlags.Add(initialModuleFlag);
while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound)
while (pendingModuleFlags.Count < totalModuleCountExcludingOptional && availableModulesFound)
{
availableModulesFound = false;
foreach (var moduleCount in generationParams.ModuleCounts)
{
if (!moduleCount.RequiredFaction.IsEmpty &&
location?.Faction?.Prefab.Identifier != moduleCount.RequiredFaction &&
location?.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction)
float? difficulty = Level.ForcedDifficulty ?? location?.LevelData?.Difficulty;
if (difficulty.HasValue)
{
continue;
if (difficulty.Value < moduleCount.MinDifficulty || difficulty.Value > moduleCount.MaxDifficulty)
{
continue;
}
}
//if this is a module that we're trying to force into the outpost,
//ignore probability and faction requirements
if (GameMain.GameSession?.ForceOutpostModule == null ||
!GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier))
{
if (moduleCount.Probability < 1.0f &&
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > moduleCount.Probability)
{
continue;
}
if (!moduleCount.RequiredFaction.IsEmpty &&
location?.Faction?.Prefab.Identifier != moduleCount.RequiredFaction &&
location?.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction)
{
continue;
}
}
if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier))
{
@@ -914,20 +1012,21 @@ namespace Barotrauma
}
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
if (!suitableModules.Any())
{
//no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
suitableModules = suitableModulesForAnyOutpost;
//still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module
if (!suitableModules.Any())
{
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
}
//still not found! Try if we can find a generic module that's not meant to attach to the previous module
if (!suitableModules.Any())
{
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
}
}
@@ -945,10 +1044,31 @@ namespace Barotrauma
}
else
{
return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
var suitableModule = ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
if (GameMain.GameSession?.ForceOutpostModule != null)
{
if (suitableModules.Any(module => module.OutpostModuleInfo.Name == GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name) ||
suitableModulesForAnyOutpost.Any(module => module.OutpostModuleInfo.Name == GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name))
{
var forceOutpostModule = GameMain.GameSession.ForceOutpostModule;
System.Diagnostics.Debug.WriteLine($"Inserting Force outpost module in Outpost generation: {forceOutpostModule.OutpostModuleInfo.Name}");
GameMain.GameSession.ForceOutpostModule = null;
usedForceOutpostModule = forceOutpostModule;
return forceOutpostModule;
}
else if (GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag))
{
// if our force module has the same tag as the selected random one, return nothing
// because we don't want another module of the same type to be hogging the only spot for that type
return null;
}
}
return suitableModule;
}
IEnumerable<SubmarineInfo> GetSuitable(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
{
IEnumerable<SubmarineInfo> suitable = modules;
if (requireCorrectLocationType)
@@ -1199,7 +1319,14 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
if (thisWayPoint == null)
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
}
if (previousWayPoint == null)
{
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
}
}
gapToRemove.ConnectedDoor?.Item.Remove();
@@ -1215,7 +1342,7 @@ namespace Barotrauma
var suitableHallwayModules = hallwayModules.Where(m =>
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
if (suitableHallwayModules.Count() == 0)
if (suitableHallwayModules.None())
{
suitableHallwayModules = hallwayModules.Where(m =>
!m.OutpostModuleInfo.AllowAttachToModules.Any() ||
@@ -1359,19 +1486,41 @@ namespace Barotrauma
(endWaypoint, startWaypoint) = (startWaypoint, endWaypoint);
}
if (hallwayLength > 100 && isHorizontal)
//if the hallway is longer than 100 pixels, generate some waypoints inside it
//for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders
const float distanceBetweenWaypoints = 100.0f;
if (hallwayLength > distanceBetweenWaypoints)
{
//if the hallway is longer than 100 pixels, generate some waypoints inside it
//for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders
WayPoint prevWayPoint = startWaypoint;
WayPoint firstWayPoint = null;
for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f)
if (isHorizontal)
{
var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub);
firstWayPoint ??= newWayPoint;
prevWayPoint.linkedTo.Add(newWayPoint);
newWayPoint.linkedTo.Add(prevWayPoint);
prevWayPoint = newWayPoint;
for (float x = leftHull.Rect.Right + distanceBetweenWaypoints / 2; x < rightHull.Rect.X - distanceBetweenWaypoints / 2; x += distanceBetweenWaypoints)
{
var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub);
firstWayPoint ??= newWayPoint;
prevWayPoint.linkedTo.Add(newWayPoint);
newWayPoint.linkedTo.Add(prevWayPoint);
prevWayPoint = newWayPoint;
}
}
else if (startWaypoint.Ladders == null)
{
float bottom = bottomHull.Rect.Y;
float top = topHull.Rect.Y - topHull.Rect.Height;
for (float y = bottom + distanceBetweenWaypoints; y < top - distanceBetweenWaypoints; y += distanceBetweenWaypoints)
{
var newWayPoint = new WayPoint(new Vector2(startWaypoint.Position.X, y), SpawnType.Path, sub);
firstWayPoint ??= newWayPoint;
prevWayPoint.linkedTo.Add(newWayPoint);
newWayPoint.linkedTo.Add(prevWayPoint);
prevWayPoint = newWayPoint;
}
}
else
{
startWaypoint.linkedTo.Add(endWaypoint);
endWaypoint.linkedTo.Add(startWaypoint);
}
if (firstWayPoint != null)
{
@@ -1387,9 +1536,9 @@ namespace Barotrauma
else
{
startWaypoint.linkedTo.Add(endWaypoint);
endWaypoint.linkedTo.Add(startWaypoint);
endWaypoint.linkedTo.Add(startWaypoint);
}
}
}
}
return placedEntities;
}
@@ -1499,12 +1648,16 @@ namespace Barotrauma
static bool ShouldRemoveLinkedEntity(MapEntity e, bool doorInUse, PlacedModule module)
{
if (e is Item it && it.IsLadder)
if (e is Item { IsLadder: true } ladderItem)
{
if (module.UsedGapPositions.HasFlag(OutpostModuleInfo.GapPosition.Top) || module.UsedGapPositions.HasFlag(OutpostModuleInfo.GapPosition.Bottom))
int linkedToLadderCount = Door.DoorList.Count(otherDoor => otherDoor.Item.linkedTo.Contains(ladderItem));
if (linkedToLadderCount > 1)
{
//if there's multiple doors linked to the ladder, never remove it
//(the ladder is presumably not just for moving between two modules in that case, but might e.g. go through the whole module)
return false;
}
return ladderItem.RemoveIfLinkedOutpostDoorInUse == doorInUse;
}
if (e is Structure structure)
@@ -1670,7 +1823,7 @@ namespace Barotrauma
if (location?.Faction != null) { factions.Add(location.Faction.Prefab); }
if (location?.SecondaryFaction != null) { factions.Add(location.SecondaryFaction.Prefab); }
var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, Rand.RandSync.ServerAndClient);
var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, outpost, Rand.RandSync.ServerAndClient);
foreach (HumanPrefab humanPrefab in humanPrefabs)
{
if (humanPrefab is null) { continue; }
@@ -46,6 +46,11 @@ namespace Barotrauma
/// </summary>
private const float DefaultMaxAvailabilityRelativeToMin = 1.2f;
/// <summary>
/// If set, the item is only available in outposts with this faction.
/// </summary>
public Identifier RequiredFaction { get; private set; }
private readonly Dictionary<Identifier, float> minReputation = new Dictionary<Identifier, float>();
/// <summary>
@@ -67,7 +72,7 @@ namespace Barotrauma
MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultAmount), CargoManager.MaxQuantity);
MaxAvailableAmount = MathHelper.Clamp(GetMaxAmount(element, defaultValue: (int)(MinAvailableAmount * DefaultMaxAvailabilityRelativeToMin)), MinAvailableAmount, CargoManager.MaxQuantity);
RequiresUnlock = element.GetAttributeBool("requiresunlock", false);
RequiredFaction = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
System.Diagnostics.Debug.Assert(MaxAvailableAmount >= MinAvailableAmount);
}
@@ -115,6 +120,7 @@ namespace Barotrauma
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
bool requiresUnlock = element.GetAttributeBool("requiresunlock", false);
Identifier requiredFactionByDefault = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
foreach (XElement childElement in element.GetChildElements("price"))
{
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
@@ -137,7 +143,10 @@ namespace Barotrauma
buyingPriceMultiplier: storeBuyingMultiplier,
displayNonEmpty: displayNonEmpty,
requiresUnlock: requiresUnlock,
storeIdentifier: storeIdentifier);
storeIdentifier: storeIdentifier)
{
RequiredFaction = childElement.GetAttributeIdentifier(nameof(RequiredFaction), requiredFactionByDefault)
};
priceInfo.LoadReputationRestrictions(childElement);
priceInfos.Add(priceInfo);
}
@@ -150,7 +159,10 @@ namespace Barotrauma
minLevelDifficulty: minLevelDifficulty,
buyingPriceMultiplier: buyingPriceMultiplier,
displayNonEmpty: displayNonEmpty,
requiresUnlock: requiresUnlock);
requiresUnlock: requiresUnlock)
{
RequiredFaction = requiredFactionByDefault
};
defaultPrice.LoadReputationRestrictions(element);
return priceInfos;
}
@@ -499,6 +499,10 @@ namespace Barotrauma
{
CastShadow = Prefab.CastShadow;
}
if (element?.GetAttribute(nameof(Indestructible)) == null)
{
Indestructible = Prefab.ConfigElement.GetAttributeBool(nameof(Indestructible), false);
}
if (Prefab.Body)
{
@@ -583,7 +587,7 @@ namespace Barotrauma
};
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
if (!property.Value.Attributes.OfType<Serialize>().Any()) { continue; }
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
}
if (FlippedX) clone.FlipX(false);
@@ -11,6 +11,7 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.PerkBehaviors;
using Voronoi2;
namespace Barotrauma
@@ -26,6 +27,21 @@ namespace Barotrauma
public CharacterTeamType TeamID = CharacterTeamType.None;
public static ImmutableArray<SubItemSwapPerk> GetSubItemSwapPerksFromTeamPerks(ImmutableArray<DisembarkPerkPrefab> teamPerks)
{
var builder = ImmutableArray.CreateBuilder<SubItemSwapPerk>();
foreach (DisembarkPerkPrefab prefab in teamPerks)
{
foreach (var perk in prefab.PerkBehaviors)
{
if (perk is not SubItemSwapPerk subSwapPerk) { continue; }
builder.Add(subSwapPerk);
}
}
return builder.ToImmutable();
}
public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f);
//position of the "actual submarine" which is rendered wherever the SubmarineBody is
//should be in an unreachable place
@@ -46,6 +62,10 @@ namespace Barotrauma
public static readonly Vector2 GridSize = new Vector2(16.0f, 16.0f);
public static readonly Submarine[] MainSubs = new Submarine[2];
/// <summary>
/// Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
/// </summary>
public static Submarine MainSub
{
get { return MainSubs[0]; }
@@ -123,6 +143,8 @@ namespace Barotrauma
set;
}
public List<WayPoint> ForcedOutpostModuleWayPoints = new List<WayPoint>();
public static List<Submarine> Loaded
{
get { return loaded; }
@@ -206,6 +228,12 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the submarine above the upper boundary of the level ("outside bounds", where the submarine shouldn't be able to get to normally)?
/// E.g. respawn shuttles are moved above the level when they despawn.
/// </summary>
public bool IsAboveLevel => Level.IsPositionAboveLevel(WorldPosition);
public bool AtEndExit
{
get
@@ -324,6 +352,9 @@ namespace Barotrauma
}
}
public bool IsRespawnShuttle =>
GameMain.NetworkMember?.RespawnManager is { } respawnManager && respawnManager.RespawnShuttles.Contains(this);
private readonly List<WayPoint> exitPoints = new List<WayPoint>();
public IReadOnlyList<WayPoint> ExitPoints { get { return exitPoints; } }
@@ -1143,11 +1174,23 @@ namespace Barotrauma
return false;
}
public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false)
{
SetLayerEnabled(layer, enabled, MapEntity.MapEntityList.Where(m => m.Submarine == this));
#if SERVER
if (sendNetworkEvent)
{
GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled));
}
#endif
}
public static void SetLayerEnabled(Identifier layer, bool enabled, IEnumerable<MapEntity> entities)
{
foreach (MapEntity entity in MapEntity.MapEntityList)
{
if (string.IsNullOrEmpty(entity.Layer) || entity.Submarine != this || entity.Layer != layer) { continue; }
if (string.IsNullOrEmpty(entity.Layer) || entity.Layer != layer) { continue; }
entity.IsLayerHidden = !enabled;
if (entity is WayPoint wp)
@@ -1163,34 +1206,37 @@ namespace Barotrauma
}
else if (entity is Item item)
{
foreach (var connectionPanel in item.GetComponents<ConnectionPanel>())
{
foreach (var connection in connectionPanel.Connections)
{
foreach (var wire in connection.Wires)
{
wire.Item.IsLayerHidden = entity.IsLayerHidden;
}
}
}
#if CLIENT
if (entity.IsLayerHidden)
{
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
foreach (var lightComponent in item.GetComponents<LightComponent>())
{
lightComponent.Light.Enabled = false;
}
}
#endif
SetItemHidden(item, entity.IsLayerHidden);
}
}
#if SERVER
if (sendNetworkEvent)
static void SetItemHidden(Item item, bool isHidden)
{
GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled));
}
foreach (var containedItem in item.ContainedItems)
{
SetItemHidden(containedItem, isHidden);
}
foreach (var connectionPanel in item.GetComponents<ConnectionPanel>())
{
foreach (var connection in connectionPanel.Connections)
{
foreach (var wire in connection.Wires)
{
wire.Item.IsLayerHidden = isHidden;
}
}
}
#if CLIENT
if (isHidden)
{
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
foreach (var lightComponent in item.GetComponents<LightComponent>())
{
lightComponent.Light.Enabled = false;
}
}
#endif
}
}
public void Update(float deltaTime)
@@ -1208,7 +1254,7 @@ namespace Barotrauma
if (Level.Loaded != null &&
WorldPosition.Y < Level.MaxEntityDepth &&
subBody.Body.Enabled &&
(GameMain.NetworkMember?.RespawnManager == null || this != GameMain.NetworkMember.RespawnManager.RespawnShuttle))
!IsRespawnShuttle)
{
subBody.Body.ResetDynamics();
subBody.Body.Enabled = false;
@@ -1412,11 +1458,8 @@ namespace Barotrauma
foreach (Submarine sub in loaded)
{
if (ignoreOutposts && sub.Info.IsOutpost) { continue; }
if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (ignoreRespawnShuttle)
{
if (sub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
}
if (ignoreOutsideLevel && Level.Loaded != null && sub.IsAboveLevel) { continue; }
if (ignoreRespawnShuttle && sub.IsRespawnShuttle) { continue; }
if (teamType.HasValue && sub.TeamID != teamType) { continue; }
float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition);
if (closest == null || dist < closestDist)
@@ -1557,7 +1600,10 @@ namespace Barotrauma
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
}
public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
public Submarine(SubmarineInfo info,
bool showErrorMessages = true,
Func<Submarine, List<MapEntity>> loadEntities = null,
IdRemap linkedRemap = null) : base(null, NullEntityID)
{
Stopwatch sw = Stopwatch.StartNew();
@@ -1651,6 +1697,10 @@ namespace Barotrauma
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = CharacterTeamType.FriendlyNPC;
foreach (var dockedSub in DockedTo)
{
dockedSub.TeamID = CharacterTeamType.FriendlyNPC;
}
bool indestructible =
GameMain.NetworkMember != null &&
@@ -1837,7 +1887,7 @@ namespace Barotrauma
public bool CheckFuel()
{
float fuel = GetItems(true).Where(i => i.HasTag(Tags.Fuel)).Sum(i => i.Condition);
float fuel = GetItems(true).Where(i => i.HasTag(Tags.ReactorFuel)).Sum(i => i.Condition);
Info.LowFuel = fuel < 200;
return !Info.LowFuel;
}
@@ -1859,6 +1909,7 @@ namespace Barotrauma
element.Add(new XAttribute("class", Info.SubmarineClass.ToString()));
}
element.Add(new XAttribute("tags", Info.Tags.ToString()));
element.Add(new XAttribute("outposttags", Info.OutpostTags.ConvertToString()));
element.Add(new XAttribute("gameversion", GameMain.Version.ToString()));
Rectangle dimensions = VisibleBorders;
@@ -1917,35 +1968,14 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap is not { } connectedItemsToSwap) { continue; }
foreach (var (requiredTag, swapTo) in connectedItemsToSwap)
if (item.PendingItemSwap?.SwappableItem == null) { continue; }
var connectedItemsToSwap = item.GetConnectedItemsToSwap(item.PendingItemSwap.SwappableItem);
foreach (var kvp in connectedItemsToSwap)
{
List<Item> itemsToSwap = new List<Item>();
itemsToSwap.AddRange(item.linkedTo.Where(lt => (lt as Item)?.HasTag(requiredTag) ?? false).Cast<Item>());
if (item.GetComponent<ConnectionPanel>() is ConnectionPanel connectionPanel)
{
foreach (Connection c in connectionPanel.Connections)
{
foreach (var connectedComponent in item.GetConnectedComponentsRecursive<ItemComponent>(c))
{
if (!itemsToSwap.Contains(connectedComponent.Item) && connectedComponent.Item.HasTag(requiredTag))
{
itemsToSwap.Add(connectedComponent.Item);
}
}
}
}
ItemPrefab itemPrefab = ItemPrefab.Find("", swapTo);
if (itemPrefab == null)
{
DebugConsole.ThrowError($"Failed to swap an item connected to \"{item.Name}\" into \"{swapTo}\".");
continue;
}
foreach (Item itemToSwap in itemsToSwap)
{
itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap;
if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; }
}
Item itemToSwap = kvp.Key;
ItemPrefab swapTo = kvp.Value;
itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap;
if (itemToSwap.Prefab != swapTo) { itemToSwap.PendingItemSwap = swapTo; }
}
}
@@ -2004,6 +2034,7 @@ namespace Barotrauma
FilePath = filePath,
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null,
EnemySubmarineInfo = Info.EnemySubmarineInfo != null ? new EnemySubmarineInfo(Info.EnemySubmarineInfo) : null,
WreckInfo = Info.WreckInfo != null ? new WreckInfo(Info.WreckInfo) : null,
Name = Path.GetFileNameWithoutExtension(filePath)
};
@@ -585,9 +585,7 @@ namespace Barotrauma
private void UpdateDepthDamage(float deltaTime)
{
#if CLIENT
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
#endif
if (Level.Loaded == null) { return; }
//camera shake and sounds start playing 500 meters before crush depth
@@ -598,7 +596,7 @@ namespace Barotrauma
const float MaxWallDamageProbability = 1.0f;
const float MinWallDamage = 50f;
const float MaxWallDamage = 500.0f;
const float MinCameraShake = 5f;
const float MinCameraShake = 10f;
const float MaxCameraShake = 50.0f;
//delay at the start of the round during which you take no depth damage
//(gives you a bit of time to react and return if you start the round in a level that's too deep)
@@ -612,8 +610,11 @@ namespace Barotrauma
damageSoundTimer -= deltaTime;
if (damageSoundTimer <= 0.0f)
{
const float PressureSoundRange = -CosmeticEffectThreshold;
//Ratio between 0 (where the 'approaching crush depth' indication starts) and 1 (at crush depth or past it)
float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)) / PressureSoundRange, 0f, 1f);
#if CLIENT
SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f);
SoundPlayer.PlayDamageSound("pressure", MathHelper.Lerp(0f, 100f, closenessToCrushDepthRatio), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f, gain: 1f + closenessToCrushDepthRatio * 2);
#endif
damageSoundTimer = Rand.Range(5.0f, 10.0f);
}
@@ -919,6 +920,13 @@ namespace Barotrauma
{
Debug.Assert(otherSub != submarine);
//submarine outside the level (despawned respawn shuttle?)
//no need to apply impacts between colliding subs
if (submarine.IsAboveLevel)
{
return;
}
Vector2 normal = impact.Normal;
if (impact.Target.Body == otherSub.SubBody.Body.FarseerBody)
{
@@ -123,15 +123,30 @@ namespace Barotrauma
public OutpostModuleInfo OutpostModuleInfo { get; set; }
public BeaconStationInfo BeaconStationInfo { get; set; }
public WreckInfo WreckInfo { get; set; }
public EnemySubmarineInfo EnemySubmarineInfo { get; set; }
public ExtraSubmarineInfo GetExtraSubmarineInfo => BeaconStationInfo ?? WreckInfo as ExtraSubmarineInfo;
public ImmutableHashSet<Identifier> OutpostTags { get; set; } = ImmutableHashSet<Identifier>.Empty;
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
public bool IsWreck => Type == SubmarineType.Wreck;
public bool IsBeacon => Type == SubmarineType.BeaconStation;
public bool IsEnemySubmarine => Type == SubmarineType.EnemySubmarine;
public bool IsPlayer => Type == SubmarineType.Player;
public bool IsRuin => Type == SubmarineType.Ruin;
/// <summary>
/// Ruin modules are of type SubmarineType.OutpostModule, until the ruin generator (or the test game mode) sets them as ruins.
/// This is a helper workaround check intended to be used only in the context of the sub editor and the test game mode, where ruins aren't generated.
/// </summary>
public bool ShouldBeRuin => Type is SubmarineType.Ruin or SubmarineType.OutpostModule &&
(OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinentrance".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinvault".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinworkshop".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinshrine".ToIdentifier()));
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);
@@ -325,6 +340,7 @@ namespace Barotrauma
Tags = original.Tags;
OutpostGenerationParams = original.OutpostGenerationParams;
LayersHiddenByDefault = original.LayersHiddenByDefault;
OutpostTags = original.OutpostTags;
if (original.OutpostModuleInfo != null)
{
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
@@ -333,6 +349,10 @@ namespace Barotrauma
{
BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo);
}
else if (original.EnemySubmarineInfo != null)
{
EnemySubmarineInfo = new EnemySubmarineInfo(original.EnemySubmarineInfo);
}
else if (original.WreckInfo != null)
{
WreckInfo = new WreckInfo(original.WreckInfo);
@@ -416,6 +436,8 @@ namespace Barotrauma
}
Tier = SubmarineElement.GetAttributeInt("tier", GetDefaultTier(Price));
OutpostTags = SubmarineElement.GetAttributeIdentifierImmutableHashSet(nameof(OutpostTags), ImmutableHashSet<Identifier>.Empty);
if (SubmarineElement?.Attribute("type") != null)
{
if (Enum.TryParse(SubmarineElement.GetAttributeString("type", ""), out SubmarineType type))
@@ -429,6 +451,10 @@ namespace Barotrauma
{
BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement);
}
else if (Type == SubmarineType.EnemySubmarine)
{
EnemySubmarineInfo = new EnemySubmarineInfo(this, SubmarineElement);
}
else if (Type == SubmarineType.Wreck)
{
WreckInfo = new WreckInfo(this, SubmarineElement);
@@ -612,6 +638,11 @@ namespace Barotrauma
BeaconStationInfo.Save(newElement);
BeaconStationInfo = new BeaconStationInfo(this, newElement);
}
else if (Type == SubmarineType.EnemySubmarine)
{
EnemySubmarineInfo.Save(newElement);
EnemySubmarineInfo = new EnemySubmarineInfo(this, newElement);
}
else if (Type == SubmarineType.Wreck)
{
WreckInfo.Save(newElement);
@@ -73,6 +73,8 @@ namespace Barotrauma
public Level.Tunnel Tunnel;
public RuinGeneration.Ruin Ruin;
public Level.Cave Cave;
public SpawnType SpawnType
{
get { return spawnType; }
@@ -226,7 +228,7 @@ namespace Barotrauma
door.Body.Enabled = true;
}
}
bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier());
bool isRuin = submarine.Info.ShouldBeRuin;
float diffFromHullEdge = 50;
float minDist = 100.0f;
float heightFromFloor = 110.0f;
@@ -235,7 +237,7 @@ namespace Barotrauma
var removals = new HashSet<WayPoint>();
foreach (Hull hull in Hull.HullList)
{
if (isFlooded)
if (isRuin)
{
diffFromHullEdge = 75;
var hullWaypoints = new List<WayPoint>();
@@ -405,7 +407,7 @@ namespace Barotrauma
}
float outSideWaypointInterval = 100.0f;
if (!isFlooded && submarine.Info.Type != SubmarineType.OutpostModule)
if (!isRuin && submarine.Info.Type != SubmarineType.OutpostModule)
{
List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>();
@@ -732,7 +734,7 @@ namespace Barotrauma
{
if (gap.IsHorizontal)
{
if ( isFlooded)
if ( isRuin)
{
// Too small to swim through
if (gap.Rect.Height < 50) { continue; }
@@ -744,13 +746,13 @@ namespace Barotrauma
}
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
if (isFlooded)
if (isRuin)
{
pos.Y = gap.Rect.Y - gap.Rect.Height / 2;
}
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
// The closest waypoint can be quite far if the gap is at an exterior door.
Vector2 tolerance = gap.IsRoomToRoom && !isFlooded ? new Vector2(150, 70) : new Vector2(1000, 1000);
Vector2 tolerance = gap.IsRoomToRoom && !isRuin ? new Vector2(150, 70) : new Vector2(1000, 1000);
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody);
@@ -763,7 +765,7 @@ namespace Barotrauma
else
{
// Create waypoints on vertical gaps on the outer walls, also hatches.
if (!isFlooded && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; }
if (!isRuin && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; }
// Too small to swim through
if (gap.Rect.Width < 50.0f) { continue; }
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2);
@@ -772,14 +774,14 @@ namespace Barotrauma
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100));
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isRuin ? new Vector2(500, 500) : new Vector2(50, 100));
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
if (isFlooded)
if (isRuin)
{
closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100));
closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isRuin ? new Vector2(500, 500) : new Vector2(50, 100));
if (closest != null)
{
wayPoint.ConnectTo(closest);
@@ -944,7 +946,13 @@ namespace Barotrauma
public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
{
List<WayPoint> subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine);
subWayPoints.Shuffle();
if (submarine.ForcedOutpostModuleWayPoints != null && submarine.ForcedOutpostModuleWayPoints.Any())
{
// narrow selection of spawn points to within the module
subWayPoints = new List<WayPoint>(submarine.ForcedOutpostModuleWayPoints);
submarine.ForcedOutpostModuleWayPoints.Clear();
}
subWayPoints.Shuffle(Rand.RandSync.Unsynced);
List<WayPoint> unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human);
@@ -1003,6 +1011,28 @@ namespace Barotrauma
return assignedWayPoints;
}
public static List<WayPoint> GetOutpostSpawnPoints(CharacterTeamType teamID)
{
List<WayPoint> spawnWaypoints = WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost);
if (GameMain.GameSession.GameMode is PvPMode)
{
Identifier teamSpawnTag = ("deathmatch" + teamID).ToIdentifier();
if (spawnWaypoints.Any(wp => wp.Tags.Contains(teamSpawnTag)))
{
spawnWaypoints = spawnWaypoints.FindAll(wp => wp.Tags.Contains(teamSpawnTag));
}
}
else
{
spawnWaypoints = spawnWaypoints.FindAll(wp =>
wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull.OutpostModuleTags.Contains(Barotrauma.Tags.Airlock));
}
return spawnWaypoints;
}
public void FindHull()
{
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);