Release 1.9.7.0 - Summer Update 2025
This commit is contained in:
@@ -34,6 +34,7 @@ namespace Barotrauma
|
||||
this.Linkable = linkable;
|
||||
this.AllowedLinks = (allowedLinks ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
|
||||
this.Aliases = (aliases ?? Enumerable.Empty<string>()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet();
|
||||
this.Scale = 1;
|
||||
}
|
||||
|
||||
public static CoreEntityPrefab HullPrefab { get; private set; }
|
||||
|
||||
@@ -570,11 +570,11 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
|
||||
if (HasBrokenThrough)
|
||||
{
|
||||
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 5 seconds
|
||||
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 10 seconds
|
||||
if (fireCheckCooldown <= 0)
|
||||
{
|
||||
UpdateFireSources();
|
||||
fireCheckCooldown = 5f;
|
||||
fireCheckCooldown = 10f;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Barotrauma
|
||||
private Vector2 maxSize;
|
||||
|
||||
public bool CausedByPsychosis;
|
||||
|
||||
protected override float SpreadToOtherHullsProbability => 0.0f;
|
||||
|
||||
public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) :
|
||||
base(worldPosition, spawningHull, sourceCharacter: null, isNetworkMessage: isNetworkMessage)
|
||||
|
||||
@@ -22,10 +22,11 @@ namespace Barotrauma
|
||||
/// How often the FireSource checks whether it can spread to nearby hulls.
|
||||
/// </summary>
|
||||
const float SpreadToOtherHullsInterval = 5.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The probability of the fire spreading to a nearby hull when the <see cref="TrySpreadToNearbyHulls"/> check is made.
|
||||
/// </summary>
|
||||
const float SpreadToOtherHullsProbability = 0.15f;
|
||||
protected virtual float SpreadToOtherHullsProbability => 0.15f;
|
||||
|
||||
protected Hull hull;
|
||||
|
||||
@@ -252,11 +253,14 @@ namespace Barotrauma
|
||||
|
||||
LimitSize();
|
||||
|
||||
spreadToOtherHullsTimer -= deltaTime;
|
||||
if (spreadToOtherHullsTimer <= 0.0f)
|
||||
if (SpreadToOtherHullsProbability > 0.0f)
|
||||
{
|
||||
TrySpreadToNearbyHulls();
|
||||
spreadToOtherHullsTimer = SpreadToOtherHullsInterval;
|
||||
spreadToOtherHullsTimer -= deltaTime;
|
||||
if (spreadToOtherHullsTimer <= 0.0f)
|
||||
{
|
||||
TrySpreadToNearbyHulls();
|
||||
spreadToOtherHullsTimer = SpreadToOtherHullsInterval;
|
||||
}
|
||||
}
|
||||
|
||||
if (size.X > 256.0f && this is not DummyFireSource)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -829,13 +830,14 @@ namespace Barotrauma
|
||||
foreach (var connectedGap in hull.ConnectedGaps)
|
||||
{
|
||||
if (connectedGap == this) { continue; }
|
||||
if (connectedGap.IsRoomToRoom != IsRoomToRoom) { continue; }
|
||||
//let the "more open" gap reduce this gap's flow rate
|
||||
//or if they're both equally open, let the one that was created first handle it
|
||||
//(note that we can't use Entity.ID here because gaps on walls don't have IDs)
|
||||
if (connectedGap.open > open ||
|
||||
(connectedGap.open == open && connectedGap.CreationIndex < CreationIndex))
|
||||
{
|
||||
Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect);
|
||||
Rectangle intersection = Rectangle.Intersect(rect.ToWorldRect(), connectedGap.rect.ToWorldRect());
|
||||
if (intersection.Width > 0 && intersection.Height > 0)
|
||||
{
|
||||
//reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is
|
||||
@@ -843,6 +845,7 @@ namespace Barotrauma
|
||||
intersection.Height / (float)rect.Height :
|
||||
intersection.Width / (float)rect.Width;
|
||||
overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open;
|
||||
overlappingGaps.Add(connectedGap);
|
||||
}
|
||||
}
|
||||
if (overlappingGapFlowRateReduction >= 1.0f)
|
||||
|
||||
@@ -442,6 +442,8 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool IsBlue => ColorExtensions.IsBlueDominant(AveragePaintedColor, minimumAlpha: 100);
|
||||
|
||||
public const int MaxFireSources = 16;
|
||||
|
||||
public List<FireSource> FireSources { get; private set; }
|
||||
|
||||
public List<DummyFireSource> FakeFireSources { get; private set; }
|
||||
@@ -731,6 +733,10 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FireSources.Count >= MaxFireSources)
|
||||
{
|
||||
return;
|
||||
}
|
||||
FireSources.Add(fireSource);
|
||||
}
|
||||
}
|
||||
@@ -781,8 +787,9 @@ namespace Barotrauma
|
||||
{
|
||||
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
|
||||
|
||||
msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16);
|
||||
for (int i = 0; i < Math.Min(FireSources.Count, 16); i++)
|
||||
System.Diagnostics.Debug.Assert(FireSources.Count <= MaxFireSources, $"Too many fire sources ({FireSources.Count}) in hull {ID} (max {MaxFireSources}).");
|
||||
msg.WriteRangedInteger(Math.Min(FireSources.Count, MaxFireSources), 0, MaxFireSources);
|
||||
for (int i = 0; i < Math.Min(FireSources.Count, MaxFireSources); i++)
|
||||
{
|
||||
var fireSource = FireSources[i];
|
||||
Vector2 normalizedPos = new Vector2(
|
||||
@@ -828,7 +835,7 @@ namespace Barotrauma
|
||||
{
|
||||
newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
|
||||
|
||||
int fireSourceCount = msg.ReadRangedInteger(0, 16);
|
||||
int fireSourceCount = msg.ReadRangedInteger(0, MaxFireSources);
|
||||
newFireSources = new NetworkFireSource[fireSourceCount];
|
||||
for (int i = 0; i < fireSourceCount; i++)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
|
||||
@@ -4481,6 +4481,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool onlyEntrance = LevelData.Type != LevelData.LevelType.Outpost;
|
||||
|
||||
LocationType locationType = location?.Type;
|
||||
if (missionForcedOutpostParamsId != null &&
|
||||
OutpostGenerationParams.OutpostParams.TryGet(missionForcedOutpostParamsId, out var missionForcedOutpostParams))
|
||||
{
|
||||
@@ -4490,6 +4494,13 @@ namespace Barotrauma
|
||||
{
|
||||
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
|
||||
}
|
||||
else if (locationType != null &&
|
||||
locationType.GetForcedOutpostGenerationParams() is { } forcedOutpostGenerationParams &&
|
||||
//do not use the forced parameters if we want to generate only the entrance, and the parameters define a full, pre-built outpost
|
||||
(!onlyEntrance || forcedOutpostGenerationParams.OutpostFilePath.IsNullOrEmpty()))
|
||||
{
|
||||
outpostGenerationParams = forcedOutpostGenerationParams;
|
||||
}
|
||||
else
|
||||
{
|
||||
outpostGenerationParams =
|
||||
@@ -4497,7 +4508,6 @@ namespace Barotrauma
|
||||
LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
LocationType locationType = location?.Type;
|
||||
if (locationType == null)
|
||||
{
|
||||
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
@@ -4512,7 +4522,7 @@ namespace Barotrauma
|
||||
if (location != null)
|
||||
{
|
||||
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
|
||||
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
|
||||
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: onlyEntrance, LevelData.AllowInvalidOutpost);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -370,19 +370,36 @@ namespace Barotrauma
|
||||
|
||||
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
|
||||
{
|
||||
var suitableParams = OutpostGenerationParams.OutpostParams
|
||||
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
|
||||
var paramsForGameMode = OutpostGenerationParams.OutpostParams.Where(p =>
|
||||
p.AllowedGameModeIdentifiers.None() || GameMain.GameSession?.GameMode is not GameMode gameMode || p.AllowedGameModeIdentifiers.Contains(gameMode.Preset.Identifier));
|
||||
|
||||
var paramsWithMatchingLevelType = paramsForGameMode
|
||||
.Where(p => p.LevelType == null || levelData.Type == p.LevelType);
|
||||
|
||||
//1. try finding params specifically for this location type
|
||||
var suitableParams = paramsWithMatchingLevelType
|
||||
.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
|
||||
if (!suitableParams.Any())
|
||||
{
|
||||
suitableParams = OutpostGenerationParams.OutpostParams
|
||||
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
|
||||
.Where(p => location == null || !p.AllowedLocationTypes.Any());
|
||||
//2. not found, if the location type is configured to use the modules of some other location type,
|
||||
// see if we could use that location type's generation params
|
||||
if (!location.Type.UseOutpostModulesOfLocationType.IsEmpty)
|
||||
{
|
||||
suitableParams = paramsWithMatchingLevelType
|
||||
.Where(p => p.AllowedLocationTypes.Contains(location.Type.UseOutpostModulesOfLocationType));
|
||||
}
|
||||
if (!suitableParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
|
||||
suitableParams = OutpostGenerationParams.OutpostParams;
|
||||
//3. still not found, choose some parameters that are suitable for any location type
|
||||
suitableParams = paramsWithMatchingLevelType
|
||||
.Where(p => location == null || !p.AllowedLocationTypes.Any());
|
||||
if (!suitableParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
|
||||
suitableParams = paramsForGameMode;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return suitableParams;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Barotrauma.RuinGeneration
|
||||
|
||||
public void Generate(Level level, LocationType locationType, Point position, bool mirror = false)
|
||||
{
|
||||
Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false);
|
||||
Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false, allowInvalidOutpost: level.LevelData.AllowInvalidOutpost);
|
||||
Submarine.Info.Name = $"Ruin ({level.Seed})";
|
||||
Submarine.Info.Type = SubmarineType.Ruin;
|
||||
Submarine.TeamID = CharacterTeamType.None;
|
||||
|
||||
@@ -478,6 +478,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Missions that are available and visible in menus (<see cref="MissionPrefab.ShowInMenus"/>)
|
||||
/// </summary>
|
||||
public IEnumerable<Mission> AvailableAndVisibleMissions => AvailableMissions.Where(m => m.Prefab.ShowInMenus);
|
||||
|
||||
private readonly List<Mission> selectedMissions = new List<Mission>();
|
||||
public IEnumerable<Mission> SelectedMissions
|
||||
{
|
||||
@@ -580,9 +585,9 @@ namespace Barotrauma
|
||||
return $"Location ({DisplayName ?? "null"})";
|
||||
}
|
||||
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
public Location(Vector2 mapPosition, int? zone, Identifier? biomeId, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
|
||||
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, biomeId, requireOutpost);
|
||||
CreateRandomName(Type, rand, existingLocations);
|
||||
MapPosition = mapPosition;
|
||||
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
|
||||
@@ -782,12 +787,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
public static Location CreateRandom(Vector2 position, int? zone, Identifier? biomeId, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
|
||||
return new Location(position, zone, biomeId, rand, requireOutpost, forceLocationType, existingLocations);
|
||||
}
|
||||
|
||||
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true)
|
||||
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true, bool unlockInitialMissions = true)
|
||||
{
|
||||
if (newType == Type) { return; }
|
||||
|
||||
@@ -826,10 +831,10 @@ namespace Barotrauma
|
||||
if (Type.Faction == Identifier.Empty) { Faction = null; }
|
||||
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
|
||||
}
|
||||
|
||||
if (!IsCriticallyRadiated())
|
||||
|
||||
if (unlockInitialMissions && !IsCriticallyRadiated())
|
||||
{
|
||||
UnlockInitialMissions(Rand.RandSync.Unsynced);
|
||||
UnlockInitialMissions(Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
if (createStores)
|
||||
@@ -1257,7 +1262,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
|
||||
{
|
||||
return TextManager.Get(nameId);
|
||||
return TextManager.Get(nameId).Fallback(nameId.Value);
|
||||
}
|
||||
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Barotrauma
|
||||
class LocationType : PrefabWithUintIdentifier
|
||||
{
|
||||
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
|
||||
|
||||
|
||||
private readonly ImmutableArray<string> rawNames;
|
||||
private readonly ImmutableArray<Sprite> portraits;
|
||||
|
||||
@@ -21,13 +21,14 @@ namespace Barotrauma
|
||||
private readonly ImmutableArray<(Identifier Identifier, float Commonness, bool AlwaysAvailableIfMissingFromCrew)> hireableJobs;
|
||||
private readonly float totalHireableWeight;
|
||||
|
||||
public readonly Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
|
||||
public readonly Dictionary<int, int> MinCountPerZone = new Dictionary<int, int>();
|
||||
|
||||
public readonly LocalizedString Name;
|
||||
|
||||
public readonly LocalizedString Description;
|
||||
|
||||
public readonly Identifier ForceLocationName;
|
||||
/// <summary>
|
||||
/// Forces all locations of this LocationType to use the given name. Can either be a text tag or the actual name.
|
||||
/// </summary>
|
||||
public readonly Identifier ForceLocationName;
|
||||
|
||||
public readonly float BeaconStationChance;
|
||||
|
||||
@@ -43,11 +44,151 @@ namespace Barotrauma
|
||||
public readonly ImmutableArray<Identifier> MissionIdentifiers;
|
||||
public readonly ImmutableArray<Identifier> MissionTags;
|
||||
|
||||
public readonly List<string> HideEntitySubcategories = new List<string>();
|
||||
public abstract class AreaSettingData
|
||||
{
|
||||
public int? MinCount { get; }
|
||||
public int? MaxCount { get; }
|
||||
public float Commonness { get; }
|
||||
|
||||
public bool IsEnterable { get; private set; }
|
||||
/// <summary>
|
||||
/// Desired position in the biome or difficulty zone. A value between 0 and 1, where 0 is the left side of the zone/biome, and 1 the right side.
|
||||
/// </summary>
|
||||
public float? DesiredPosition { get; }
|
||||
|
||||
public bool HasCounts => MinCount.HasValue && MaxCount.HasValue && (MaxCount > 0 || MinCount > 0);
|
||||
public virtual bool HasValidData => true;
|
||||
|
||||
public bool AllowAsBiomeGate { get; private set; }
|
||||
internal AreaSettingData(int? minCount, int? maxCount, float commonness, float? desiredPosition)
|
||||
{
|
||||
MinCount = minCount;
|
||||
MaxCount = maxCount;
|
||||
Commonness = commonness;
|
||||
DesiredPosition = desiredPosition;
|
||||
}
|
||||
|
||||
public virtual bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool MatchesLocation(Map map, Location location)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool MatchesZone(int zoneIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool MatchesBiome(Identifier biomeIdentifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool Matches(int? zone = null, Identifier? biomeId = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class BiomeSettingData : AreaSettingData
|
||||
{
|
||||
public Identifier BiomeIdentifier { get; }
|
||||
|
||||
public override bool HasValidData => Biome.Prefabs.ContainsKey(BiomeIdentifier);
|
||||
|
||||
public BiomeSettingData(Identifier biomeIdentifier, int? minCount, int? maxCount, float commonness, float? desiredPosition, LocationType locationType)
|
||||
: base(minCount, maxCount, commonness, desiredPosition)
|
||||
{
|
||||
if (minCount > maxCount)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in location type {locationType.Identifier}: minimum count larger than maximum count in biome {biomeIdentifier}.",
|
||||
contentPackage: locationType.ContentPackage);
|
||||
}
|
||||
BiomeIdentifier = biomeIdentifier;
|
||||
}
|
||||
|
||||
public override bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
|
||||
{
|
||||
return locationTypeCount.BiomeId == BiomeIdentifier;
|
||||
}
|
||||
|
||||
public override bool MatchesLocation(Map map, Location location)
|
||||
{
|
||||
return BiomeIdentifier == location.Biome?.Identifier;
|
||||
}
|
||||
|
||||
public override bool MatchesBiome(Identifier biomeIdentifier)
|
||||
{
|
||||
return BiomeIdentifier == biomeIdentifier;
|
||||
}
|
||||
|
||||
public override bool Matches(int? zone = null, Identifier? biomeId = null)
|
||||
{
|
||||
return biomeId.HasValue && biomeId == BiomeIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
public class DifficultyZoneSettingData : AreaSettingData
|
||||
{
|
||||
public int DifficultyZone { get; }
|
||||
|
||||
public DifficultyZoneSettingData(int difficultyZone, int? minCount, int? maxCount, float commonness, float? desiredPosition, LocationType locationType)
|
||||
: base(minCount, maxCount, commonness, desiredPosition)
|
||||
{
|
||||
if (minCount > maxCount)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in location type {locationType.Identifier}: minimum count larger than maximum count in difficulty zone {difficultyZone}.",
|
||||
contentPackage: locationType.ContentPackage);
|
||||
}
|
||||
DifficultyZone = difficultyZone;
|
||||
}
|
||||
|
||||
public override bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
|
||||
{
|
||||
return locationTypeCount.DifficultyZone == DifficultyZone;
|
||||
}
|
||||
|
||||
public override bool MatchesLocation(Map map, Location location)
|
||||
{
|
||||
return DifficultyZone == map.GetZoneIndex(location.MapPosition.X);
|
||||
}
|
||||
|
||||
public override bool MatchesZone(int zoneIndex)
|
||||
{
|
||||
return DifficultyZone == zoneIndex;
|
||||
}
|
||||
|
||||
public override bool Matches(int? zone = null, Identifier? biomeId = null)
|
||||
{
|
||||
return zone.HasValue && zone == DifficultyZone;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<AreaSettingData> AreaSettings = new List<AreaSettingData>();
|
||||
|
||||
public readonly List<string> HideEntitySubcategories;
|
||||
|
||||
public enum BiomeGateSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// Can be used as a gate between biomes, but not required
|
||||
/// </summary>
|
||||
Allow,
|
||||
/// <summary>
|
||||
/// Cannot be used as a gate between biomes
|
||||
/// </summary>
|
||||
Deny,
|
||||
/// <summary>
|
||||
/// Must be used as a gate between biomes during map generation
|
||||
/// </summary>
|
||||
Force
|
||||
}
|
||||
|
||||
public BiomeGateSetting BiomeGate { get; private set; }
|
||||
|
||||
public bool ForceAsStartOutpost { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone
|
||||
@@ -108,10 +249,26 @@ namespace Barotrauma
|
||||
|
||||
private readonly Identifier forceOutpostGenerationParamsIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to make the location type use the same background music tracks as another location type.
|
||||
/// </summary>
|
||||
public readonly Identifier BackgroundMusicLocationType;
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, only event sets that explicitly define this location type in <see cref="EventSet.LocationTypeIdentifiers"/> can be selected at this location. Defaults to false.
|
||||
/// </summary>
|
||||
public bool IgnoreGenericEvents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used as criteria for validating if a given event set is suitable for this locationType.
|
||||
/// For example, if set to "city", events that appear in "city" type locations can also appear here.
|
||||
/// </summary>
|
||||
public Identifier EventLocationType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, outpost modules configured to be suitable for the specified location type can also be used in this type of location.
|
||||
/// </summary>
|
||||
public Identifier UseOutpostModulesOfLocationType { get; set; }
|
||||
|
||||
public Color SpriteColor
|
||||
{
|
||||
@@ -134,7 +291,7 @@ namespace Barotrauma
|
||||
public int DailySpecialsCount { get; } = 1;
|
||||
public int RequestedGoodsCount { get; } = 1;
|
||||
|
||||
public readonly bool ShowSonarMarker = true;
|
||||
public readonly bool ShowSonarMarker;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -145,13 +302,41 @@ namespace Barotrauma
|
||||
{
|
||||
Name = TextManager.Get("LocationName." + Identifier, "unknown");
|
||||
Description = TextManager.Get("LocationDescription." + Identifier, "");
|
||||
|
||||
|
||||
// for location types based on others, e.g., Named Unique outpost, we may want to override the name of the type to still say Outpost on the map:
|
||||
var forceNameId = element.GetAttributeIdentifier("ForceLocationTypeName", string.Empty);
|
||||
if (!forceNameId.IsEmpty)
|
||||
{
|
||||
var forcedName = TextManager.Get("LocationName." + forceNameId);
|
||||
if (!forcedName.IsNullOrEmpty())
|
||||
{
|
||||
Name = forcedName;
|
||||
}
|
||||
}
|
||||
|
||||
var forceDescriptionId = element.GetAttributeIdentifier("ForceLocationTypeDescription", string.Empty);
|
||||
if (!forceDescriptionId.IsEmpty)
|
||||
{
|
||||
var forcedDescription = TextManager.Get("LocationDescription." + forceDescriptionId);
|
||||
if (!forcedDescription.IsNullOrEmpty())
|
||||
{
|
||||
Description = forcedDescription;
|
||||
}
|
||||
}
|
||||
|
||||
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
|
||||
|
||||
UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true);
|
||||
HasOutpost = element.GetAttributeBool("hasoutpost", true);
|
||||
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
|
||||
AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true);
|
||||
bool allowAsBiomeGateLegacy = element.GetAttributeBool("allowasbiomegate", true);
|
||||
BiomeGate = element.GetAttributeEnum("BiomeGate", def: allowAsBiomeGateLegacy ? BiomeGateSetting.Allow : BiomeGateSetting.Deny);
|
||||
if (BiomeGate != BiomeGateSetting.Deny && !HasOutpost)
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in location type {Identifier}: the location is set to be allowed as a biome gate, but will never be chosen as one because it has no outpost.",
|
||||
contentPackage: ContentPackage);
|
||||
}
|
||||
|
||||
ForceAsStartOutpost = element.GetAttributeBool(nameof(ForceAsStartOutpost), false);
|
||||
AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true);
|
||||
|
||||
Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
|
||||
@@ -168,17 +353,24 @@ namespace Barotrauma
|
||||
DescriptionInRadiation = element.GetAttributeIdentifier(nameof(DescriptionInRadiation), "locationdescription.abandonedirradiated");
|
||||
|
||||
forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty);
|
||||
BackgroundMusicLocationType = element.GetAttributeIdentifier(nameof(BackgroundMusicLocationType), Identifier.Empty);
|
||||
|
||||
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
|
||||
|
||||
EventLocationType = element.GetAttributeIdentifier(nameof(EventLocationType), Identifier.Empty);
|
||||
UseOutpostModulesOfLocationType = element.GetAttributeIdentifier(nameof(UseOutpostModulesOfLocationType), Identifier.Empty);
|
||||
|
||||
IsAnyOutpost = element.GetAttributeBool(nameof(IsAnyOutpost), def: HasOutpost);
|
||||
|
||||
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
|
||||
Enum.TryParse(teamStr, out OutpostTeam);
|
||||
|
||||
if (element.GetAttribute("name") != null)
|
||||
if (element.GetAttribute(nameof(ForceLocationName)) != null ||
|
||||
element.GetAttribute("name") != null)
|
||||
{
|
||||
ForceLocationName = element.GetAttributeIdentifier("name", string.Empty);
|
||||
ForceLocationName = element.GetAttributeIdentifier(nameof(ForceLocationName),
|
||||
//backwards compatibility
|
||||
def: element.GetAttributeIdentifier("name", string.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -214,15 +406,18 @@ namespace Barotrauma
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
|
||||
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
|
||||
{
|
||||
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
|
||||
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
|
||||
if (splitCommonnessPerZone.Length != 2 ||
|
||||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
|
||||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
|
||||
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"", contentPackage: element.ContentPackage);
|
||||
break;
|
||||
}
|
||||
CommonnessPerZone[zoneIndex] = zoneCommonness;
|
||||
|
||||
if (zoneCommonness <= 0.0f) { continue; }
|
||||
|
||||
AugmentDifficultyZoneSettings(zoneIndex, zoneCommonness, minCount: null);
|
||||
}
|
||||
|
||||
string[] minCountPerZoneStrs = element.GetAttributeStringArray("mincountperzone", Array.Empty<string>());
|
||||
@@ -233,13 +428,40 @@ namespace Barotrauma
|
||||
!int.TryParse(splitMinCountPerZone[0].Trim(), out int zoneIndex) ||
|
||||
!int.TryParse(splitMinCountPerZone[1].Trim(), out int minCount))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read minimum count values for location type \"" + Identifier + "\" - minimum counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"");
|
||||
DebugConsole.ThrowError("Failed to read minimum zone count values for location type \"" + Identifier +
|
||||
"\" - minimum zone counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"", contentPackage: element.ContentPackage);
|
||||
break;
|
||||
}
|
||||
MinCountPerZone[zoneIndex] = minCount;
|
||||
|
||||
if (minCount <= 0) { continue; }
|
||||
|
||||
AugmentDifficultyZoneSettings(zoneIndex, zoneCommonness: null, minCount);
|
||||
}
|
||||
var portraits = new List<Sprite>();
|
||||
var hireableJobs = new List<(Identifier, float, bool)>();
|
||||
|
||||
void AugmentDifficultyZoneSettings(int zoneIndex, float? zoneCommonness, int? minCount)
|
||||
{
|
||||
|
||||
var existingSettings = AreaSettings.Find(areaSettingData => areaSettingData is DifficultyZoneSettingData difficultyZoneSettingData &&
|
||||
difficultyZoneSettingData.DifficultyZone == zoneIndex);
|
||||
|
||||
if (existingSettings != null)
|
||||
{
|
||||
int index = AreaSettings.IndexOf(existingSettings);
|
||||
AreaSettings[index] = new DifficultyZoneSettingData(zoneIndex,
|
||||
minCount ?? existingSettings.MinCount,
|
||||
//note that assigning minCount to maxCount is intentional here:
|
||||
//previously it was only possible to define minCount (essentially the same as just defining "count" now)
|
||||
maxCount: minCount ?? existingSettings.MaxCount,
|
||||
commonness: zoneCommonness ?? existingSettings.Commonness, desiredPosition: null, locationType: this);
|
||||
}
|
||||
else
|
||||
{
|
||||
AreaSettings.Add(new DifficultyZoneSettingData(zoneIndex, minCount ?? 0, maxCount: minCount ?? 0, commonness: zoneCommonness ?? 0, desiredPosition: null, locationType: this));
|
||||
}
|
||||
}
|
||||
|
||||
var portraitsList = new List<Sprite>();
|
||||
var hireableJobsList = new List<(Identifier, float, bool)>();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -249,7 +471,7 @@ namespace Barotrauma
|
||||
float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f);
|
||||
bool availableIfMissing = subElement.GetAttributeBool("AlwaysAvailableIfMissingFromCrew", false);
|
||||
totalHireableWeight += jobCommonness;
|
||||
hireableJobs.Add((jobIdentifier, jobCommonness, availableIfMissing));
|
||||
hireableJobsList.Add((jobIdentifier, jobCommonness, availableIfMissing));
|
||||
break;
|
||||
case "symbol":
|
||||
Sprite = new Sprite(subElement, lazyLoad: true);
|
||||
@@ -265,7 +487,7 @@ namespace Barotrauma
|
||||
var portrait = new Sprite(subElement, lazyLoad: true);
|
||||
if (portrait != null)
|
||||
{
|
||||
portraits.Add(portrait);
|
||||
portraitsList.Add(portrait);
|
||||
}
|
||||
break;
|
||||
case "store":
|
||||
@@ -281,10 +503,71 @@ namespace Barotrauma
|
||||
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
|
||||
RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount);
|
||||
break;
|
||||
case "areasettings":
|
||||
ParseAreaSettings(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.portraits = portraitsList.ToImmutableArray();
|
||||
this.hireableJobs = hireableJobsList.ToImmutableArray();
|
||||
|
||||
void ParseAreaSettings(ContentXElement areaSettingsElement)
|
||||
{
|
||||
Identifier biomeIdentifier = areaSettingsElement.GetAttributeIdentifier("biome", Identifier.Empty);
|
||||
int zone = areaSettingsElement.GetAttributeInt("zone", 0);
|
||||
|
||||
if (biomeIdentifier == Identifier.Empty && zone == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read area settings for locationType \"" + Identifier + "\" - biome identifier and zone are both missing.", contentPackage: element.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (biomeIdentifier != Identifier.Empty && zone != 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read area settings for locationType \"" + Identifier + "\" - both biome identifier and zone are defined. Must be one or the other.", contentPackage: element.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
bool HasComma(string intAttributeName)
|
||||
{
|
||||
var attr = areaSettingsElement.GetAttribute(intAttributeName);
|
||||
if (attr == null) { return false;}
|
||||
return attr.Value.Contains(',');
|
||||
}
|
||||
|
||||
if (HasComma("mincount") || HasComma("maxcount") || HasComma("count"))
|
||||
{
|
||||
DebugConsole.LogError($"AreaSettings for locationType {Identifier} has comma inside int count attribute. This causes the resulting parse to combine the numbers, resulting in incorrect amount of locations.",
|
||||
contentPackage: ContentPackage);
|
||||
}
|
||||
|
||||
int? minCount = areaSettingsElement.GetAttributeNullableInt("mincount");
|
||||
int? maxCount = areaSettingsElement.GetAttributeNullableInt("maxcount");
|
||||
int? count = areaSettingsElement.GetAttributeNullableInt("count");
|
||||
float? desiredPosition = areaSettingsElement.GetAttributeNullableFloat("desiredposition");
|
||||
float commonness = areaSettingsElement.GetAttributeFloat("commonness", 0);
|
||||
|
||||
// if set, count overrides min and max count to eliminate randomness
|
||||
if (count.HasValue)
|
||||
{
|
||||
minCount = count;
|
||||
maxCount = count;
|
||||
}
|
||||
else if (minCount.HasValue && maxCount.HasValue && minCount <= 0 && maxCount <= 0)
|
||||
{
|
||||
DebugConsole.AddWarning("Failed to read count value for location type \"" + Identifier + "\" - both min and max count are 0.", contentPackage: element.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (biomeIdentifier != Identifier.Empty)
|
||||
{
|
||||
AreaSettings.Add(new BiomeSettingData(biomeIdentifier, minCount, maxCount, commonness, desiredPosition, locationType: this));
|
||||
}
|
||||
else
|
||||
{
|
||||
AreaSettings.Add(new DifficultyZoneSettingData(zone, minCount, maxCount, commonness, desiredPosition, locationType: this));
|
||||
}
|
||||
}
|
||||
this.portraits = portraits.ToImmutableArray();
|
||||
this.hireableJobs = hireableJobs.ToImmutableArray();
|
||||
}
|
||||
|
||||
public IEnumerable<JobPrefab> GetHireablesMissingFromCrew()
|
||||
@@ -383,7 +666,7 @@ namespace Barotrauma
|
||||
return rawNames[rand.Next() % rawNames.Length];
|
||||
}
|
||||
|
||||
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
|
||||
public static LocationType Random(Random rand, int? zone = null, Identifier? biomeId = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
|
||||
{
|
||||
Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
|
||||
|
||||
@@ -392,19 +675,22 @@ namespace Barotrauma
|
||||
(predicate == null || predicate(lt)) && IsValid(lt))
|
||||
.OrderBy(p => p.UintIdentifier).ToArray();
|
||||
|
||||
bool IsValid(LocationType lt)
|
||||
bool IsValid(LocationType locationType)
|
||||
{
|
||||
if (requireOutpost && !lt.HasOutpost) { return false; }
|
||||
if (zone.HasValue)
|
||||
{
|
||||
if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; }
|
||||
}
|
||||
if (requireOutpost && !locationType.HasOutpost) { return false; }
|
||||
|
||||
bool validZone = !zone.HasValue || locationType.AreaSettings.Any(areaSetting => areaSetting.MatchesZone(zone.Value));
|
||||
bool validBiome = !biomeId.HasValue || locationType.AreaSettings.Any(areaSetting => areaSetting.MatchesBiome(biomeId.Value));
|
||||
|
||||
if (!validZone && !validBiome) { return false; }
|
||||
|
||||
//if zone is not defined, this is a "random" (non-campaign) level
|
||||
//-> don't choose location types that aren't allowed in those
|
||||
else if (!lt.AllowInRandomLevels)
|
||||
if (!zone.HasValue && !biomeId.HasValue && !locationType.AllowInRandomLevels)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -413,11 +699,12 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
|
||||
}
|
||||
|
||||
if (zone.HasValue)
|
||||
if (zone.HasValue || biomeId.HasValue)
|
||||
{
|
||||
return ToolBox.SelectWeightedRandom(
|
||||
allowedLocationTypes,
|
||||
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToArray(),
|
||||
allowedLocationTypes.Select(allowedType =>
|
||||
allowedType.AreaSettings.Find(areaSetting => areaSetting.MatchesZone(zone.Value) || areaSetting.MatchesBiome(biomeId.Value))?.Commonness ?? 0).ToArray(),
|
||||
rand);
|
||||
}
|
||||
else
|
||||
@@ -425,6 +712,17 @@ namespace Barotrauma
|
||||
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Length];
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValidForZoneOrBiome(int? zone, Identifier? biomeIdentifier)
|
||||
{
|
||||
//if zone is not defined, this is a "random" (non-campaign) level
|
||||
//-> don't choose location types that aren't allowed in those
|
||||
if (!zone.HasValue && !AllowInRandomLevels) { return false; }
|
||||
|
||||
if (!zone.HasValue && !biomeIdentifier.HasValue) { return true; }
|
||||
|
||||
return AreaSettings.Any(setting => setting.Matches(zone, biomeIdentifier));
|
||||
}
|
||||
|
||||
public OutpostGenerationParams GetForcedOutpostGenerationParams()
|
||||
{
|
||||
@@ -434,6 +732,11 @@ namespace Barotrauma
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool HasCounts()
|
||||
{
|
||||
return AreaSettings.Any(setting => setting.HasCounts);
|
||||
}
|
||||
|
||||
public override void Dispose() { }
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ namespace Barotrauma
|
||||
public Radiation Radiation;
|
||||
|
||||
private bool trackedLocationDiscoveryAndVisitOrder = true;
|
||||
|
||||
private IOrderedEnumerable<Biome> _orderedBiomes;
|
||||
public IOrderedEnumerable<Biome> OrderedBiomes => _orderedBiomes ??= Biome.Prefabs.GetOrdered();
|
||||
|
||||
public Map(CampaignSettings settings)
|
||||
{
|
||||
@@ -240,10 +243,7 @@ namespace Barotrauma
|
||||
Vector2 mapPos = new Vector2(
|
||||
MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
|
||||
Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
|
||||
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations)
|
||||
{
|
||||
Biome = endLocations.First().Biome
|
||||
};
|
||||
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, firstEndLocation.Biome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
|
||||
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
|
||||
Locations.Add(newEndLocation);
|
||||
endLocations.Add(newEndLocation);
|
||||
@@ -365,6 +365,14 @@ namespace Barotrauma
|
||||
{
|
||||
CurrentLocation.ChangeType(campaign, tutorialOutpost);
|
||||
}
|
||||
else
|
||||
{
|
||||
var forceStartOutpostType = LocationType.Prefabs.Where(lt => lt.ForceAsStartOutpost).GetRandom(Rand.RandSync.ServerAndClient);
|
||||
if (forceStartOutpostType != null)
|
||||
{
|
||||
CurrentLocation.ChangeType(campaign, forceStartOutpostType);
|
||||
}
|
||||
}
|
||||
Discover(CurrentLocation);
|
||||
Visit(CurrentLocation);
|
||||
CurrentLocation.CreateStores();
|
||||
@@ -396,13 +404,16 @@ namespace Barotrauma
|
||||
y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient)));
|
||||
}
|
||||
}
|
||||
|
||||
// put some of this stuff in a helper class, this function is getting unwieldy
|
||||
MapLocationTypeGenerator mapLocationTypeGenerator = new MapLocationTypeGenerator(campaign, this);
|
||||
|
||||
Voronoi voronoi = new Voronoi(0.5f);
|
||||
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
|
||||
|
||||
Vector2 margin = new Vector2(
|
||||
Math.Min(10, Width * 0.1f),
|
||||
Math.Min(10, Height * 0.2f));
|
||||
Math.Min(10, Width * 0.1f),
|
||||
Math.Min(10, Height * 0.2f));
|
||||
|
||||
float startX = margin.X, endX = Width - margin.X;
|
||||
float startY = margin.Y, endY = Height - margin.Y;
|
||||
@@ -413,8 +424,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
voronoiSites.Clear();
|
||||
Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
|
||||
bool possibleStartOutpostCreated = false;
|
||||
foreach (GraphEdge edge in edges)
|
||||
{
|
||||
if (edge.Point1 == edge.Point2) { continue; }
|
||||
@@ -442,33 +451,18 @@ namespace Barotrauma
|
||||
|
||||
Vector2 position = points[positionIndex];
|
||||
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
|
||||
|
||||
int zone = GetZoneIndex(position.X);
|
||||
if (!locationsPerZone.ContainsKey(zone))
|
||||
{
|
||||
locationsPerZone[zone] = new List<Location>();
|
||||
}
|
||||
|
||||
LocationType forceLocationType = null;
|
||||
if (forceLocationType == null)
|
||||
{
|
||||
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
|
||||
{
|
||||
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
|
||||
{
|
||||
forceLocationType = locationType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
|
||||
requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
|
||||
locationsPerZone[zone].Add(newLocations[i]);
|
||||
newLocations[i] = Location.CreateRandom(position, zone, GetBiome(position.X)?.Identifier, Rand.GetRNG(Rand.RandSync.ServerAndClient),
|
||||
requireOutpost: false, forceLocationType: null, existingLocations: Locations);
|
||||
|
||||
mapLocationTypeGenerator.AddToLocationsPerZone(zone, newLocations[i]);
|
||||
Locations.Add(newLocations[i]);
|
||||
}
|
||||
|
||||
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
|
||||
Connections.Add(newConnection);
|
||||
Connections.Add(newConnection);
|
||||
}
|
||||
|
||||
//remove connections that are too short
|
||||
@@ -481,7 +475,6 @@ namespace Barotrauma
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//locations.Remove(connection.Locations[0]);
|
||||
Connections.Remove(connection);
|
||||
|
||||
@@ -572,7 +565,6 @@ namespace Barotrauma
|
||||
{
|
||||
(zone1, zone2) = (zone2, zone1);
|
||||
}
|
||||
|
||||
if (generationParams.GateCount[zone1] == 0) { continue; }
|
||||
|
||||
if (!connectionsBetweenZones[zone1].Any())
|
||||
@@ -589,7 +581,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
|
||||
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
|
||||
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
|
||||
{
|
||||
connectionsBetweenZones[zone1].Add(connection);
|
||||
}
|
||||
@@ -599,6 +591,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var orderedPrefabs = LocationType.Prefabs.GetOrdered();
|
||||
List<Location> forciblyReassignedGateLocations = new List<Location>();
|
||||
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
|
||||
for (int i = Connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -617,20 +611,37 @@ namespace Barotrauma
|
||||
{
|
||||
var leftMostLocation =
|
||||
Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
|
||||
Connections[i].Locations[0] :
|
||||
Connections[i].Locations[1];
|
||||
Connections[i].Locations[0] :
|
||||
Connections[i].Locations[1];
|
||||
if (!AllowAsBiomeGate(leftMostLocation.Type))
|
||||
{
|
||||
var potentialGateLocationTypes = orderedPrefabs.Where(AllowAsBiomeGate);
|
||||
LocationType gateLocationType =
|
||||
//choose some location type that's allowed in this zone/biome, and has a non zero commonness (instead of some fixed count)
|
||||
potentialGateLocationTypes.Where(lt => lt.AreaSettings.Any(areaSettings => areaSettings.MatchesLocation(this, leftMostLocation) && areaSettings.Commonness > 0)).GetRandom(Rand.RandSync.ServerAndClient) ??
|
||||
//if not found, use something with a fixed count
|
||||
potentialGateLocationTypes.Where(lt => lt.AreaSettings.Any(areaSettings => areaSettings.MatchesLocation(this, leftMostLocation) && areaSettings.MinCount > 0)).GetRandom(Rand.RandSync.ServerAndClient) ??
|
||||
//if that's not found either, try finding a type that doesn't spawn in any biome, but is allowed as a biome gate
|
||||
//(a mod might have some special "biome gate" location types that are meant just for the gate locations)
|
||||
potentialGateLocationTypes.Where(lt => lt.AreaSettings.None(areaSettings => areaSettings.Commonness > 0.0f || areaSettings.HasCounts)).GetRandom(Rand.RandSync.ServerAndClient);
|
||||
|
||||
if (gateLocationType == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find a suitable location type for a gate location between zones {zone1} and {zone2}.");
|
||||
continue;
|
||||
}
|
||||
leftMostLocation.ChangeType(
|
||||
campaign,
|
||||
LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
|
||||
createStores: false);
|
||||
gateLocationType,
|
||||
createStores: false,
|
||||
unlockInitialMissions: false);
|
||||
forciblyReassignedGateLocations.Add(leftMostLocation);
|
||||
}
|
||||
static bool AllowAsBiomeGate(LocationType lt)
|
||||
{
|
||||
//checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate
|
||||
//but might be better to keep it for backwards compatibility (previously we relied only on that check)
|
||||
return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate;
|
||||
return lt.HasOutpost && lt.Identifier != "abandoned" && lt.BiomeGate != LocationType.BiomeGateSetting.Deny;
|
||||
}
|
||||
|
||||
leftMostLocation.IsGateBetweenBiomes = true;
|
||||
@@ -663,8 +674,8 @@ namespace Barotrauma
|
||||
if (!connection.Locked) { continue; }
|
||||
var rightMostLocation =
|
||||
connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
|
||||
connection.Locations[0] :
|
||||
connection.Locations[1];
|
||||
connection.Locations[0] :
|
||||
connection.Locations[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
|
||||
@@ -693,8 +704,18 @@ namespace Barotrauma
|
||||
|
||||
//remove orphans
|
||||
Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
|
||||
|
||||
AssignBiomes(Rand.GetRNG(Rand.RandSync.ServerAndClient));
|
||||
|
||||
AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed)));
|
||||
var gateLocations = Locations.Where(l => l.IsGateBetweenBiomes);
|
||||
foreach (var gateLocation in forciblyReassignedGateLocations)
|
||||
{
|
||||
//remove the gate locations who's types we've reassigned from the remaining types left to assign,
|
||||
//(i.e. if we want just 1 of some location type, and we were forced to choose it as a gate, don't use that type again)
|
||||
//must be done after assigning biomes
|
||||
mapLocationTypeGenerator.RemoveOneFromTotals(gateLocation.Type, gateLocation);
|
||||
}
|
||||
mapLocationTypeGenerator.AssignForcedBiomeGateTypes(gateLocations);
|
||||
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
@@ -713,8 +734,13 @@ namespace Barotrauma
|
||||
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
|
||||
{
|
||||
startLocation.ChangeType(campaign, startLocationType, createStores: false);
|
||||
mapLocationTypeGenerator.AddToFilled(startLocation);
|
||||
}
|
||||
|
||||
mapLocationTypeGenerator.AssignLocationTypesBasedOnDesiredPosition(gateLocations);
|
||||
|
||||
//create proper level data and stores for all locations
|
||||
//(needs to be done before AssignLocationCounts, since LevelData may be required for the location type changes)
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
@@ -733,6 +759,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
//needs to be done after the LevelData has been assigned above
|
||||
foreach (var gateLocation in forciblyReassignedGateLocations)
|
||||
{
|
||||
gateLocation.UnlockInitialMissions(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
List<Location> locationsToAssign = Locations.ToList();
|
||||
locationsToAssign.Remove(GetPreviousToEndLocation());
|
||||
mapLocationTypeGenerator.AssignLocationTypesBasedOnCount(gateLocations, locations: locationsToAssign);
|
||||
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
@@ -740,7 +775,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
CreateEndLocation(campaign);
|
||||
|
||||
float CalculateDifficulty(float mapPosition, Biome biome)
|
||||
{
|
||||
float settingsFactor = campaign.Settings.LevelDifficultyMultiplier;
|
||||
@@ -779,23 +813,27 @@ namespace Barotrauma
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
|
||||
zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
|
||||
return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
|
||||
return OrderedBiomes.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assign biomes for the connections between locations, ad for the locations that don't yet have a biome assigned.
|
||||
/// </summary>
|
||||
private void AssignBiomes(Random rand)
|
||||
{
|
||||
var biomes = Biome.Prefabs;
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
|
||||
List<Biome> allowedBiomes = new List<Biome>(10);
|
||||
for (int i = 0; i < generationParams.DifficultyZones; i++)
|
||||
{
|
||||
int zoneIndex = i + 1;
|
||||
allowedBiomes.Clear();
|
||||
allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
|
||||
float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
|
||||
allowedBiomes.AddRange(OrderedBiomes.Where(b => b.AllowedZones.Contains(zoneIndex)));
|
||||
float zoneX = zoneWidth * (zoneIndex);
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.Biome != null) { continue; }
|
||||
if (location.MapPosition.X < zoneX)
|
||||
{
|
||||
location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
|
||||
@@ -812,6 +850,9 @@ namespace Barotrauma
|
||||
System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the location prior to the final location. The type of this location is hard-coded just as that of the final location.
|
||||
/// </summary>
|
||||
private Location GetPreviousToEndLocation()
|
||||
{
|
||||
Location previousToEndLocation = null;
|
||||
@@ -970,9 +1011,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion Generation
|
||||
|
||||
|
||||
public void MoveToNextLocation()
|
||||
{
|
||||
if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
|
||||
@@ -1167,7 +1207,6 @@ namespace Barotrauma
|
||||
{
|
||||
List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
|
||||
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
|
||||
|
||||
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
|
||||
{
|
||||
SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
|
||||
@@ -1285,8 +1324,8 @@ namespace Barotrauma
|
||||
{
|
||||
location.PendingLocationTypeChange =
|
||||
(location.PendingLocationTypeChange.Value.typeChange,
|
||||
location.PendingLocationTypeChange.Value.delay - 1,
|
||||
location.PendingLocationTypeChange.Value.parentMission);
|
||||
location.PendingLocationTypeChange.Value.delay - 1,
|
||||
location.PendingLocationTypeChange.Value.parentMission);
|
||||
if (location.PendingLocationTypeChange.Value.delay <= 0)
|
||||
{
|
||||
return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
|
||||
@@ -1317,8 +1356,8 @@ namespace Barotrauma
|
||||
{
|
||||
location.PendingLocationTypeChange =
|
||||
(selectedTypeChange,
|
||||
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
|
||||
null);
|
||||
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
|
||||
parentMission: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1554,7 +1593,7 @@ namespace Barotrauma
|
||||
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
|
||||
LocalizedString prevLocationName = location.DisplayName;
|
||||
LocationType prevLocationType = location.Type;
|
||||
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
|
||||
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.GetOrdered().First();
|
||||
location.ChangeType(campaign, newLocationType);
|
||||
|
||||
if (showNotifications && prevLocationType != location.Type)
|
||||
@@ -1636,7 +1675,7 @@ namespace Barotrauma
|
||||
if (index < 0) { return null; }
|
||||
return Locations[index];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void Discover(Location location)
|
||||
@@ -1774,4 +1813,4 @@ namespace Barotrauma
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal class MapLocationTypeGenerator
|
||||
{
|
||||
internal class LocationTypeCount
|
||||
{
|
||||
public int AmountToAssign;
|
||||
public int? DifficultyZone;
|
||||
public Identifier? BiomeId;
|
||||
|
||||
public LocationTypeCount(int amountToAssign, int difficultyZone)
|
||||
{
|
||||
AmountToAssign = amountToAssign;
|
||||
DifficultyZone = difficultyZone;
|
||||
}
|
||||
|
||||
public LocationTypeCount(int amountToAssign, Identifier biomeId)
|
||||
{
|
||||
AmountToAssign = amountToAssign;
|
||||
BiomeId = biomeId;
|
||||
}
|
||||
|
||||
public string ToDebugString()
|
||||
{
|
||||
if (DifficultyZone.HasValue)
|
||||
{
|
||||
return $"x{AmountToAssign} in (zone {DifficultyZone.Value})";
|
||||
}
|
||||
else if (BiomeId.HasValue)
|
||||
{
|
||||
return $"x{AmountToAssign} in (biome {BiomeId.Value})";
|
||||
}
|
||||
return $"x{AmountToAssign}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actual amounts of location types that need to be assigned to locations, after resolving random variation from min/max counts.
|
||||
/// </summary>
|
||||
private Dictionary<LocationType, List<LocationTypeCount>> locationTypeAmountsToAssign;
|
||||
private readonly Map map;
|
||||
|
||||
private readonly CampaignMode campaign;
|
||||
/// <summary>
|
||||
/// List of locations that have been filled from specific requirements and should not be overwritten by some subsequent generation pass (e.g. the start outpost).
|
||||
/// </summary>
|
||||
private readonly List<Location> filledLocations;
|
||||
private readonly Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
|
||||
|
||||
private readonly IOrderedEnumerable<LocationType> orderedLocationTypes = LocationType.Prefabs.GetOrdered();
|
||||
|
||||
private bool IsEveryLocationTypeAssigned
|
||||
{
|
||||
get
|
||||
{
|
||||
return locationTypeAmountsToAssign.SelectMany(kvp => kvp.Value)
|
||||
.None(locationTypeCount => locationTypeCount.AmountToAssign > 0);
|
||||
}
|
||||
}
|
||||
|
||||
public MapLocationTypeGenerator(CampaignMode campaign, Map map)
|
||||
{
|
||||
filledLocations = new List<Location>();
|
||||
this.map = map;
|
||||
this.campaign = campaign;
|
||||
locationTypeAmountsToAssign = new Dictionary<LocationType, List<LocationTypeCount>>();
|
||||
GenerateTotalAmountsToAssign();
|
||||
}
|
||||
|
||||
private void GenerateTotalAmountsToAssign()
|
||||
{
|
||||
foreach (var locationTypePrefab in orderedLocationTypes)
|
||||
{
|
||||
foreach (var areaSetting in locationTypePrefab.AreaSettings)
|
||||
{
|
||||
if (!areaSetting.HasCounts) { continue; }
|
||||
|
||||
if (!areaSetting.HasValidData)
|
||||
{
|
||||
// the only case for invalid data right now is the biome id
|
||||
DebugConsole.AddWarning($"Biome ID is invalid for AreaSetting in locationType '{locationTypePrefab.Identifier}'. Skipping invalid setting.", locationTypePrefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
int amountToAdd = Rand.GetRNG(Rand.RandSync.ServerAndClient).Next(areaSetting.MinCount ?? 0, areaSetting.MaxCount ?? 0 + 1);
|
||||
if (amountToAdd <= 0) { continue; }
|
||||
|
||||
// data is either for a biome or a zone, but not both
|
||||
var existingCount = GetExistingCount(locationTypePrefab, areaSetting);
|
||||
|
||||
if (existingCount != null)
|
||||
{
|
||||
existingCount.AmountToAssign += amountToAdd;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!locationTypeAmountsToAssign.ContainsKey(locationTypePrefab))
|
||||
{
|
||||
locationTypeAmountsToAssign[locationTypePrefab] = new List<LocationTypeCount>();
|
||||
}
|
||||
locationTypeAmountsToAssign[locationTypePrefab].Add(CreateNewCount(areaSetting, amountToAdd));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LocationTypeCount CreateNewCount(LocationType.AreaSettingData areaSettingData, int amountToAdd)
|
||||
{
|
||||
if (areaSettingData is LocationType.BiomeSettingData biomeSettingData)
|
||||
{
|
||||
return new LocationTypeCount(amountToAdd, biomeSettingData.BiomeIdentifier);
|
||||
}
|
||||
else if (areaSettingData is LocationType.DifficultyZoneSettingData difficultyZoneSettingData)
|
||||
{
|
||||
return new LocationTypeCount(amountToAdd, difficultyZoneSettingData.DifficultyZone);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Unrecognized areaSettingData");
|
||||
}
|
||||
}
|
||||
|
||||
LocationTypeCount? GetExistingCount(LocationType locationTypePrefab, LocationType.AreaSettingData areaSettingData)
|
||||
{
|
||||
if (!locationTypeAmountsToAssign.TryGetValue(locationTypePrefab, out List<LocationTypeCount>? value)) { return null; }
|
||||
return value.FirstOrDefault(areaSettingData.MatchesRemainingCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddToLocationsPerZone(int zone, Location location)
|
||||
{
|
||||
if (!locationsPerZone.ContainsKey(zone))
|
||||
{
|
||||
locationsPerZone[zone] = new List<Location>();
|
||||
}
|
||||
locationsPerZone[zone].Add(location);
|
||||
}
|
||||
|
||||
public void AddToFilled(Location location)
|
||||
{
|
||||
if (filledLocations.Contains(location)) { return; }
|
||||
filledLocations.Add(location);
|
||||
}
|
||||
|
||||
public bool IsFilled(Location location)
|
||||
{
|
||||
return filledLocations.Contains(location);
|
||||
}
|
||||
|
||||
public static void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType)
|
||||
{
|
||||
location.ChangeType(campaign, suitableLocationType, createStores: false, unlockInitialMissions: false);
|
||||
if (!suitableLocationType.ForceLocationName.IsEmpty)
|
||||
{
|
||||
location.ForceName(suitableLocationType.ForceLocationName);
|
||||
}
|
||||
}
|
||||
|
||||
public void AssignForcedBiomeGateTypes(IEnumerable<Location> gateLocations)
|
||||
{
|
||||
foreach (Location gateLocation in gateLocations)
|
||||
{
|
||||
foreach (LocationType locationType in orderedLocationTypes)
|
||||
{
|
||||
if (locationType.BiomeGate != LocationType.BiomeGateSetting.Force) { continue; }
|
||||
|
||||
int zone = map.GetZoneIndex(gateLocation.MapPosition.X);
|
||||
|
||||
// if there are no counts left for this location type, skip it
|
||||
if (locationType.HasCounts() && !TypeHasRemainingCountForLocation(locationType, gateLocation)) { continue; }
|
||||
|
||||
// wrong faction, can't place here
|
||||
if (!locationType.Faction.IsEmpty && locationType.Faction != gateLocation.Faction?.Prefab.Identifier)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the location already happens to be of the type we want to assign, skip and remove from totals
|
||||
if (gateLocation.Type == locationType)
|
||||
{
|
||||
AddToFilled(gateLocation);
|
||||
RemoveOneFromTotals(locationType, gateLocation);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!IsFilled(gateLocation) &&
|
||||
locationType.IsValidForZoneOrBiome(zone, gateLocation.Biome.Identifier))
|
||||
{
|
||||
AddToFilled(gateLocation);
|
||||
ChangeLocationTypeAndName(campaign, gateLocation, locationType);
|
||||
RemoveOneFromTotals(locationType, gateLocation);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TypeHasRemainingCountForLocation(LocationType countLocationType, Location location)
|
||||
{
|
||||
if (!locationTypeAmountsToAssign.TryGetValue(countLocationType, out List<LocationTypeCount>? locationTypeCounts))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasZoneCount = locationTypeCounts.Any(ltc => ltc.DifficultyZone == map.GetZoneIndex(location.MapPosition.X) && ltc.AmountToAssign > 0);
|
||||
bool hasBiomeCount = locationTypeCounts.Any(ltc => ltc.BiomeId == location.Biome.Identifier && ltc.AmountToAssign > 0);
|
||||
|
||||
return hasZoneCount || hasBiomeCount;
|
||||
}
|
||||
|
||||
private int GetRemainingCount(LocationType locationType, LocationType.AreaSettingData areaSetting)
|
||||
{
|
||||
locationTypeAmountsToAssign.TryGetValue(locationType, out List<LocationTypeCount>? locationTypeCounts);
|
||||
if (locationTypeCounts == null || locationTypeCounts.None()) { return 0; }
|
||||
|
||||
var match = locationTypeCounts.FirstOrDefault(ltc => areaSetting.MatchesRemainingCount(ltc));
|
||||
return match?.AmountToAssign ?? 0;
|
||||
}
|
||||
|
||||
public void RemoveOneFromTotals(LocationType locationType, Location location)
|
||||
{
|
||||
if (!locationTypeAmountsToAssign.TryGetValue(locationType, out List<LocationTypeCount>? locationTypeCounts))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var zoneMatch = locationTypeCounts.FirstOrDefault(ltc => ltc.AmountToAssign > 0 && ltc.DifficultyZone == map.GetZoneIndex(location.MapPosition.X));
|
||||
if (zoneMatch != null)
|
||||
{
|
||||
zoneMatch.AmountToAssign--;
|
||||
}
|
||||
var biomeMatch = locationTypeCounts.FirstOrDefault(ltc => ltc.AmountToAssign > 0 && ltc.BiomeId == location.Biome.Identifier);
|
||||
if (biomeMatch != null)
|
||||
{
|
||||
biomeMatch.AmountToAssign--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assign the location types that should be placed in some specific part of a biome/zone <see cref="LocationType.AreaSettingData.DesiredPosition"/>.
|
||||
/// </summary>
|
||||
public void AssignLocationTypesBasedOnDesiredPosition(IEnumerable<Location> gateLocations)
|
||||
{
|
||||
foreach (LocationType locationType in LocationType.Prefabs)
|
||||
{
|
||||
foreach (var areaSetting in locationType.AreaSettings)
|
||||
{
|
||||
if (!areaSetting.DesiredPosition.HasValue) { continue; }
|
||||
|
||||
int remainingCount = GetRemainingCount(locationType, areaSetting);
|
||||
if (remainingCount == 0) { continue; }
|
||||
|
||||
var locations = map.Locations.Where(location => areaSetting.MatchesLocation(map, location)).Where(location => !gateLocations.Contains(location));
|
||||
if (locations.None()) { continue; }
|
||||
|
||||
FillLocations(locations, areaSetting.DesiredPosition.Value, remainingCount, locationType);
|
||||
}
|
||||
}
|
||||
|
||||
void FillLocations(IEnumerable<Location> locations, float desiredPosition, int locationCount, LocationType locationType)
|
||||
{
|
||||
float areaStart = locations.Min(l => l.MapPosition.X);
|
||||
float areaEnd = locations.Max(l => l.MapPosition.X);
|
||||
|
||||
float desiredMapPosition = MathHelper.Lerp(areaStart, areaEnd, desiredPosition);
|
||||
|
||||
List<Location> sortedLocations = locations.Where(location => !IsFilled(location)).ToList();
|
||||
sortedLocations.Sort((firstLocation, secondLocation) => Math.Abs(firstLocation.MapPosition.X - desiredMapPosition).CompareTo(secondLocation.MapPosition.X - desiredMapPosition));
|
||||
|
||||
for (int i = 0; i < locationCount; i++)
|
||||
{
|
||||
if (sortedLocations.None()) { break; }
|
||||
var closestLocation = sortedLocations.First();
|
||||
ChangeLocationTypeAndName(campaign, closestLocation, locationType);
|
||||
AddToFilled(closestLocation);
|
||||
RemoveOneFromTotals(closestLocation.Type, closestLocation);
|
||||
sortedLocations.Remove(closestLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AssignLocationTypesBasedOnCount(IEnumerable<Location> gateLocations, IEnumerable<Location> locations)
|
||||
{
|
||||
// generate lists of all the instances of location types that we are supposed to try and fit into the available locations
|
||||
List<Location> shuffledLocations = locations.ToList();
|
||||
shuffledLocations.Shuffle(Rand.GetRNG(Rand.RandSync.ServerAndClient));
|
||||
|
||||
foreach (Location location in shuffledLocations)
|
||||
{
|
||||
if (IsFilled(location)) { continue; }
|
||||
|
||||
bool isBiomeGate = gateLocations.Contains(location);
|
||||
if (isBiomeGate && location.Type.BiomeGate == LocationType.BiomeGateSetting.Force)
|
||||
{
|
||||
//forced as a biome gate, let's not touch this location
|
||||
continue;
|
||||
}
|
||||
if (IsEveryLocationTypeAssigned) { break; }
|
||||
|
||||
var suitableLocationType = TryPickSuitableLocationTypeFromTotals(location, isBiomeGate);
|
||||
|
||||
// if we found something suitable, change the location type and name, and add to filled locations
|
||||
// (otherwise we will honor the initial random location type)
|
||||
if (suitableLocationType != null)
|
||||
{
|
||||
ChangeLocationTypeAndName(campaign, location, suitableLocationType);
|
||||
AddToFilled(location);
|
||||
}
|
||||
}
|
||||
|
||||
// warn if we couldn't fill in all the desired counts
|
||||
if (!IsEveryLocationTypeAssigned)
|
||||
{
|
||||
ContentPackage? nonVanillaContentPackage = null;
|
||||
StringBuilder sb = new StringBuilder("Following location types could not be assigned to locations:\n");
|
||||
foreach ((LocationType locationType, List<LocationTypeCount> locationTypeCounts) in locationTypeAmountsToAssign)
|
||||
{
|
||||
foreach (var locationTypeCount in locationTypeCounts)
|
||||
{
|
||||
if (locationTypeCount.AmountToAssign > 0)
|
||||
{
|
||||
if (locationType.ContentPackage != ContentPackageManager.VanillaCorePackage)
|
||||
{
|
||||
nonVanillaContentPackage = locationType.ContentPackage;
|
||||
}
|
||||
sb.AppendLine($"- {locationType.Identifier} - {locationType.Name} ({locationTypeCount.ToDebugString()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
DebugConsole.AddWarning(sb.ToString(),
|
||||
//blame the mod where one of the problematic location types is defined in
|
||||
contentPackage: nonVanillaContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
private LocationType? TryPickSuitableLocationTypeFromTotals(Location location, bool isBiomeGate)
|
||||
{
|
||||
int locationZone = map.GetZoneIndex(location.MapPosition.X);
|
||||
Identifier locationBiomeId = location.Biome.Identifier;
|
||||
LocationType? suitableLocationType = null;
|
||||
|
||||
//find location type counts that haven't been fully assigned yet
|
||||
List<(LocationType LocationType, LocationTypeCount Count)> potentialLocationTypeCounts = [];
|
||||
foreach ((LocationType locationType, List<LocationTypeCount> countList) in locationTypeAmountsToAssign)
|
||||
{
|
||||
//if we're picking a potential new type for a biome gate, it must be a location type that's allowed as a biome gate
|
||||
if (isBiomeGate && locationType.BiomeGate == LocationType.BiomeGateSetting.Deny) { continue; }
|
||||
foreach (var locationTypeCount in countList)
|
||||
{
|
||||
if (locationTypeCount.AmountToAssign > 0)
|
||||
{
|
||||
potentialLocationTypeCounts.Add((locationType, locationTypeCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var zoneMatches = potentialLocationTypeCounts.Where(locationTypeCount => locationTypeCount.Count.DifficultyZone == locationZone);
|
||||
var biomeMatches = potentialLocationTypeCounts.Where(locationTypeCount => locationTypeCount.Count.BiomeId == locationBiomeId);
|
||||
if (zoneMatches.None() && biomeMatches.None())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// if both lists have something, we will try to find a location type that is in both lists
|
||||
if (zoneMatches.Any() && biomeMatches.Any())
|
||||
{
|
||||
var dualMatch = zoneMatches.FirstOrDefault(zoneCount => biomeMatches.Any(biomeCount => biomeCount.LocationType == zoneCount.LocationType));
|
||||
if (dualMatch.LocationType != null)
|
||||
{
|
||||
suitableLocationType = dualMatch.LocationType;
|
||||
}
|
||||
}
|
||||
|
||||
// no dual match, find individual match
|
||||
if (suitableLocationType == null)
|
||||
{
|
||||
suitableLocationType = zoneMatches.Any() ?
|
||||
zoneMatches.First().LocationType : biomeMatches.First().LocationType;
|
||||
}
|
||||
|
||||
if (suitableLocationType != null)
|
||||
{
|
||||
RemoveOneFromTotals(suitableLocationType, location);
|
||||
}
|
||||
|
||||
return suitableLocationType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly List<MapEntity> linkedTo = new List<MapEntity>();
|
||||
|
||||
protected bool flippedX, flippedY;
|
||||
public bool FlippedX { get { return flippedX; } }
|
||||
public bool FlippedY { get { return flippedY; } }
|
||||
public bool FlippedX { get; protected set; }
|
||||
public bool FlippedY { get; protected set; }
|
||||
|
||||
public bool ShouldBeSaved = true;
|
||||
|
||||
@@ -91,6 +90,16 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public virtual float RotationRad { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation taking into account flipping: if the entity is flipped on either axis, the rotation is negated
|
||||
/// (but not if it's flipped on both axes, two flips is essentially double negation).
|
||||
/// </summary>
|
||||
public float RotationRadWithFlipping => FlippedX ^ FlippedY ? -RotationRad : RotationRad;
|
||||
|
||||
public float RotationWithFlipping => MathHelper.ToDegrees(RotationRadWithFlipping);
|
||||
|
||||
public virtual Rectangle Rect
|
||||
{
|
||||
get { return rect; }
|
||||
@@ -697,9 +706,10 @@ namespace Barotrauma
|
||||
/// Flip the entity horizontally
|
||||
/// </summary>
|
||||
/// <param name="relativeToSub">Should the entity be flipped across the y-axis of the sub it's inside</param>
|
||||
public virtual void FlipX(bool relativeToSub)
|
||||
/// <param name="force">Forces the item to be flipped even if it's configured not to be flippable.</param>
|
||||
public virtual void FlipX(bool relativeToSub, bool force = false)
|
||||
{
|
||||
flippedX = !flippedX;
|
||||
FlippedX = !FlippedX;
|
||||
if (!relativeToSub || Submarine == null) { return; }
|
||||
|
||||
Vector2 relative = WorldPosition - Submarine.WorldPosition;
|
||||
@@ -711,9 +721,10 @@ namespace Barotrauma
|
||||
/// Flip the entity vertically
|
||||
/// </summary>
|
||||
/// <param name="relativeToSub">Should the entity be flipped across the x-axis of the sub it's inside</param>
|
||||
public virtual void FlipY(bool relativeToSub)
|
||||
/// <param name="force">Forces the item to be flipped even if it's configured not to be flippable.</param>
|
||||
public virtual void FlipY(bool relativeToSub, bool force = false)
|
||||
{
|
||||
flippedY = !flippedY;
|
||||
FlippedY = !FlippedY;
|
||||
if (!relativeToSub || Submarine == null) { return; }
|
||||
|
||||
Vector2 relative = WorldPosition - Submarine.WorldPosition;
|
||||
|
||||
@@ -23,6 +23,12 @@ namespace Barotrauma
|
||||
get { return allowedLocationTypes; }
|
||||
}
|
||||
|
||||
private readonly HashSet<Identifier> allowedGameModeIdentifiers = new HashSet<Identifier>();
|
||||
|
||||
public IEnumerable<Identifier> AllowedGameModeIdentifiers
|
||||
{
|
||||
get { return allowedGameModeIdentifiers; }
|
||||
}
|
||||
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)]
|
||||
public int ForceToEndLocationIndex
|
||||
@@ -279,6 +285,7 @@ namespace Barotrauma
|
||||
{
|
||||
Name = element.GetAttributeString("name", Identifier.Value);
|
||||
allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()).ToHashSet();
|
||||
allowedGameModeIdentifiers = element.GetAttributeIdentifierArray("allowedgamemodes", Array.Empty<Identifier>()).ToHashSet();
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
if (element.GetAttribute("leveltype") != null)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.RuinGeneration;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -110,6 +111,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var prebuiltOutpostInfo = ChooseOutpost(generationParams);
|
||||
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
|
||||
sub = new Submarine(prebuiltOutpostInfo);
|
||||
sub.Info.OutpostGenerationParams = generationParams;
|
||||
location?.RemoveTakenItems();
|
||||
EnableFactionSpecificEntities(sub, location);
|
||||
return sub;
|
||||
}
|
||||
|
||||
private static SubmarineInfo ChooseOutpost(OutpostGenerationParams generationParams)
|
||||
{
|
||||
var outpostFiles = ContentPackageManager.EnabledPackages.All
|
||||
.SelectMany(p => p.GetFiles<OutpostFile>())
|
||||
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
|
||||
@@ -120,6 +132,50 @@ namespace Barotrauma
|
||||
{
|
||||
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
|
||||
}
|
||||
|
||||
|
||||
//if there's missions selected that allow outpost selection from some specific set of outposts,
|
||||
//choose one of those outposts
|
||||
List<SubmarineInfo> outpostInfosSuitableForMission = new List<SubmarineInfo>();
|
||||
if (GameMain.GameSession?.GameMode is { } gameMode)
|
||||
{
|
||||
foreach (var mission in gameMode.Missions)
|
||||
{
|
||||
if (!mission.Prefab.AllowOutpostSelectionFromTag.IsEmpty)
|
||||
{
|
||||
foreach (var outpostInfo in outpostInfos)
|
||||
{
|
||||
if (outpostInfo.OutpostTags.Contains(mission.Prefab.AllowOutpostSelectionFromTag) &&
|
||||
!outpostInfosSuitableForMission.Contains(outpostInfo))
|
||||
{
|
||||
outpostInfosSuitableForMission.Add(outpostInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if an outpost has been select in the server settings, choose that...
|
||||
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
|
||||
serverSettings.SelectedOutpostName != "Random")
|
||||
{
|
||||
//...but only if the outpost is suitable for the mission (or if the mission has no specific requirements for the outpost)
|
||||
if (outpostInfosSuitableForMission.None() ||
|
||||
outpostInfosSuitableForMission.Any(outpostInfo => outpostInfo.OutpostTags.Contains(serverSettings.SelectedOutpostName)))
|
||||
{
|
||||
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
|
||||
if (matchingOutpost != null)
|
||||
{
|
||||
return matchingOutpost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outpostInfosSuitableForMission.Any())
|
||||
{
|
||||
return outpostInfosSuitableForMission.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
if (generationParams.OutpostTag.IsEmpty)
|
||||
{
|
||||
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None());
|
||||
@@ -139,24 +195,7 @@ namespace Barotrauma
|
||||
{
|
||||
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;
|
||||
return outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
private static Submarine GenerateFromModules(OutpostGenerationParams generationParams, OutpostModuleFile[] outpostModuleFiles, Submarine sub, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
|
||||
@@ -260,13 +299,15 @@ namespace Barotrauma
|
||||
|
||||
if (hasForceOutpostWithInitialFlag)
|
||||
{
|
||||
DebugConsole.NewMessage($"Using Force outpost module as initial in Outpost generation: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}", Color.Yellow);
|
||||
DebugConsole.NewMessage($"Forcing module \"{GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}\" as the initial module...", Color.Yellow);
|
||||
usedForceOutpostModule = GameMain.GameSession.ForceOutpostModule;
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
}
|
||||
|
||||
if (initialModule == null)
|
||||
{
|
||||
//reset the forced outpost module so that it won't be used
|
||||
//if we attempt to generate a new outpost later after this failed attempt
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
throw new Exception("Failed to generate an outpost (no airlock modules found).");
|
||||
}
|
||||
foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags)
|
||||
@@ -297,25 +338,34 @@ namespace Barotrauma
|
||||
remainingOutpostGenerationTries--;
|
||||
continue;
|
||||
}
|
||||
DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
DebugConsole.ThrowError($"Could not force the outpost module \"{GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}\" to the outpost. Loading the module as-is...");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GameMain.GameSession != null)
|
||||
{
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
}
|
||||
|
||||
if (pendingModuleFlags.Any(flag => flag != "none"))
|
||||
{
|
||||
if (!allowInvalidOutpost)
|
||||
{
|
||||
remainingOutpostGenerationTries--;
|
||||
if (remainingOutpostGenerationTries <= 0)
|
||||
if (remainingOutpostGenerationTries > 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
|
||||
//tries left -> don't finish generating the outpost, try generating another layout
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
//out of tries, log an error, but let the method continue into loading the outpost (even if it doesn't have all the required modules)
|
||||
DebugConsole.AddSafeError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
|
||||
DebugConsole.AddSafeError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +404,7 @@ namespace Barotrauma
|
||||
remainingOutpostGenerationTries--;
|
||||
}
|
||||
|
||||
DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
|
||||
DebugConsole.AddSafeError("Failed to generate an outpost with a valid layout and all the required modules. Trying to use a pre-built outpost instead...");
|
||||
return null;
|
||||
|
||||
List<MapEntity> loadEntities(Submarine sub)
|
||||
@@ -463,10 +513,13 @@ namespace Barotrauma
|
||||
int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height)));
|
||||
|
||||
bool overlapsFound = true;
|
||||
PlacedModule overlappingModule1, overlappingModule2, moduleBelowAirlock;
|
||||
int iteration = 0;
|
||||
const int MaxIterations = 20;
|
||||
while (overlapsFound)
|
||||
{
|
||||
overlapsFound = false;
|
||||
overlappingModule1 = overlappingModule2 = moduleBelowAirlock = null;
|
||||
foreach (PlacedModule placedModule in selectedModules)
|
||||
{
|
||||
if (placedModule.PreviousModule == null) { continue; }
|
||||
@@ -478,6 +531,8 @@ namespace Barotrauma
|
||||
int remainingOverlapPreventionTries = 10;
|
||||
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingOverlapPreventionTries > 0)
|
||||
{
|
||||
overlappingModule1 = module1;
|
||||
overlappingModule2 = module2;
|
||||
overlapsFound = true;
|
||||
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, generationParams.MinHallwayLength, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
|
||||
{
|
||||
@@ -492,16 +547,40 @@ namespace Barotrauma
|
||||
}
|
||||
remainingOverlapPreventionTries--;
|
||||
}
|
||||
if (remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
|
||||
|
||||
//check that the module doesn't extend below the airlock and potentially overlap with the sub
|
||||
if (generationParams is not RuinGenerationParams &&
|
||||
//if we've already exhausted half of the retries, accept potential overlaps
|
||||
remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
|
||||
//if the module is horizontally very far, it's ok to expand below the airlock
|
||||
(placedModule.Bounds.X + placedModule.Offset.X < 5000 && placedModule.Bounds.Right + placedModule.Offset.X > -5000) &&
|
||||
ModuleBelowInitialModule(placedModule, selectedModules.First()))
|
||||
{
|
||||
moduleBelowAirlock = placedModule;
|
||||
overlapsFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
iteration++;
|
||||
if (iteration > 10)
|
||||
if (iteration > MaxIterations)
|
||||
{
|
||||
#if DEBUG
|
||||
string warningMsg = "Failed to create an outpost layout with no overlaps.";
|
||||
if (overlappingModule1 != null && overlappingModule2 != null)
|
||||
{
|
||||
warningMsg += $" Overlapping modules: {overlappingModule1.Info.Name}, {overlappingModule2.Info.Name}.";
|
||||
}
|
||||
if (moduleBelowAirlock != null)
|
||||
{
|
||||
warningMsg += $" Module below airlock: {moduleBelowAirlock.Info.Name}.";
|
||||
}
|
||||
if (remainingOutpostGenerationTries > 0)
|
||||
{
|
||||
warningMsg += " Retrying...";
|
||||
}
|
||||
|
||||
DebugConsole.AddWarning(warningMsg);
|
||||
#endif
|
||||
generationFailed = true;
|
||||
break;
|
||||
}
|
||||
@@ -931,7 +1010,7 @@ namespace Barotrauma
|
||||
Rectangle initialModuleBounds = initialModule.Bounds;
|
||||
initialModuleBounds.Location += (initialModule.Offset + initialModule.MoveOffset).ToPoint();
|
||||
|
||||
return bounds.Bottom < initialModuleBounds.Bottom;
|
||||
return bounds.Y < initialModuleBounds.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1053,8 +1132,8 @@ namespace Barotrauma
|
||||
}
|
||||
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
|
||||
|
||||
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
|
||||
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
|
||||
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, requireLocationTypeSpecific: true);
|
||||
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, requireLocationTypeSpecific: 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
|
||||
@@ -1062,12 +1141,12 @@ namespace Barotrauma
|
||||
//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 = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
|
||||
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, requireLocationTypeSpecific: 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 = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
|
||||
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, requireLocationTypeSpecific: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,20 +1188,12 @@ namespace Barotrauma
|
||||
return suitableModule;
|
||||
}
|
||||
|
||||
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
|
||||
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool requireLocationTypeSpecific)
|
||||
{
|
||||
IEnumerable<SubmarineInfo> suitable = modules;
|
||||
if (requireCorrectLocationType)
|
||||
{
|
||||
if (disallowNonLocationTypeSpecific)
|
||||
{
|
||||
//don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type
|
||||
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
|
||||
}
|
||||
else
|
||||
{
|
||||
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
|
||||
}
|
||||
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType, requireLocationTypeSpecific: requireLocationTypeSpecific));
|
||||
}
|
||||
if (requireAllowAttachToPrevious && prevModule != null)
|
||||
{
|
||||
|
||||
@@ -138,10 +138,15 @@ namespace Barotrauma
|
||||
return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier());
|
||||
}
|
||||
|
||||
public bool IsAllowedInLocationType(LocationType locationType)
|
||||
|
||||
/// <param name="requireLocationTypeSpecific">Does the module need to be explicitly configured as suitable for the location type, or is it ok if it's allowed for any location type?</param>
|
||||
public bool IsAllowedInLocationType(LocationType locationType, bool requireLocationTypeSpecific = false)
|
||||
{
|
||||
if (locationType == null || IsAllowedInAnyLocationType()) { return true; }
|
||||
return allowedLocationTypes.Contains(locationType.Identifier);
|
||||
if (locationType == null) { return true; }
|
||||
if (!requireLocationTypeSpecific && IsAllowedInAnyLocationType()) { return true; }
|
||||
return
|
||||
allowedLocationTypes.Contains(locationType.Identifier) ||
|
||||
(!locationType.UseOutpostModulesOfLocationType.IsEmpty && allowedLocationTypes.Contains(locationType.UseOutpostModulesOfLocationType));
|
||||
}
|
||||
|
||||
public void DetermineGapPositions(Submarine sub)
|
||||
|
||||
@@ -251,14 +251,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected float rotationRad = 0f;
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
|
||||
public float Rotation
|
||||
{
|
||||
get => MathHelper.ToDegrees(rotationRad);
|
||||
get => MathHelper.ToDegrees(RotationRad);
|
||||
set
|
||||
{
|
||||
rotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
|
||||
RotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
|
||||
if (StairDirection != Direction.None)
|
||||
{
|
||||
CreateStairBodies();
|
||||
@@ -373,7 +372,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.rotationRad;
|
||||
float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.RotationRad;
|
||||
if (IsHorizontal)
|
||||
{
|
||||
if (FlippedX) { rotation = -MathHelper.Pi - rotation; }
|
||||
@@ -396,9 +395,9 @@ namespace Barotrauma
|
||||
get
|
||||
{
|
||||
Vector2 bodyOffset = Prefab.BodyOffset;
|
||||
if (rotationRad != 0f)
|
||||
if (RotationRad != 0f)
|
||||
{
|
||||
bodyOffset = MathUtils.RotatePoint(bodyOffset, -rotationRad);
|
||||
bodyOffset = MathUtils.RotatePoint(bodyOffset, -RotationRad);
|
||||
}
|
||||
if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
|
||||
if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
|
||||
@@ -627,7 +626,7 @@ namespace Barotrauma
|
||||
|
||||
Body newBody = GameMain.World.CreateRectangle(bodyWidth, bodyHeight, 1.5f);
|
||||
|
||||
var rotationWithFlip = FlippedX ^ FlippedY ? -rotationRad : rotationRad;
|
||||
float rotationWithFlip = RotationRadWithFlipping;
|
||||
|
||||
newBody.BodyType = BodyType.Static;
|
||||
Vector2 stairRectHeightDiff = new Vector2(0f, stairHeight / 2.0f - rect.Height / 2.0f);
|
||||
@@ -764,8 +763,8 @@ namespace Barotrauma
|
||||
public override Quad2D GetTransformedQuad()
|
||||
=> Quad2D.FromSubmarineRectangle(rect).Rotated(
|
||||
FlippedX != FlippedY
|
||||
? rotationRad
|
||||
: -rotationRad);
|
||||
? RotationRad
|
||||
: -RotationRad);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there's a structure items can be attached to at the given position and returns it.
|
||||
@@ -1280,7 +1279,7 @@ namespace Barotrauma
|
||||
gapRect.Width += 20;
|
||||
gapRect.Height += 20;
|
||||
|
||||
bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(rotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
|
||||
bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(RotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
|
||||
if (rotatedEnoughToChangeOrientation)
|
||||
{
|
||||
var center = gapRect.Location + gapRect.Size.FlipY() / new Point(2);
|
||||
@@ -1345,6 +1344,9 @@ namespace Barotrauma
|
||||
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
|
||||
{
|
||||
CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles);
|
||||
#if CLIENT
|
||||
SteamTimelineManager.OnHullBreached(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,7 +1380,7 @@ namespace Barotrauma
|
||||
{
|
||||
const float explosionRange = 500.0f;
|
||||
float explosionStrength = gap.Open;
|
||||
|
||||
|
||||
var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
|
||||
if (linkedHull != null)
|
||||
{
|
||||
@@ -1595,7 +1597,7 @@ namespace Barotrauma
|
||||
|
||||
partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation);
|
||||
|
||||
public override void FlipX(bool relativeToSub)
|
||||
public override void FlipX(bool relativeToSub, bool force = false)
|
||||
{
|
||||
base.FlipX(relativeToSub);
|
||||
|
||||
@@ -1623,7 +1625,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override void FlipY(bool relativeToSub)
|
||||
public override void FlipY(bool relativeToSub, bool force = false)
|
||||
{
|
||||
base.FlipY(relativeToSub);
|
||||
|
||||
|
||||
@@ -1141,6 +1141,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dockingPort in DockingPort.List)
|
||||
{
|
||||
//a little hacky: undock and redock to ensure the hulls and gaps between docking ports are correct
|
||||
//after all the parts of the submarine have been flipped and moved to correct places.
|
||||
if (dockingPort.DockingTarget is { } dockingTarget)
|
||||
{
|
||||
dockingPort.Undock();
|
||||
dockingPort.Dock(dockingTarget);
|
||||
}
|
||||
}
|
||||
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
#if CLIENT
|
||||
@@ -2090,6 +2101,10 @@ namespace Barotrauma
|
||||
foreach (Submarine sub in _loaded)
|
||||
{
|
||||
sub.Remove();
|
||||
if (sub.Info.LazyLoad)
|
||||
{
|
||||
sub.Info.UnloadSubmarineElement();
|
||||
}
|
||||
}
|
||||
|
||||
loaded.Clear();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -194,10 +194,27 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.
|
||||
/// </summary>
|
||||
public readonly bool LazyLoad;
|
||||
|
||||
private XElement submarineElement;
|
||||
|
||||
public XElement SubmarineElement
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
get
|
||||
{
|
||||
if (LazyLoad && submarineElement == null)
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
return submarineElement;
|
||||
}
|
||||
private set
|
||||
{
|
||||
submarineElement = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@@ -263,7 +280,11 @@ namespace Barotrauma
|
||||
RequiredContentPackages = new HashSet<string>();
|
||||
}
|
||||
|
||||
public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true)
|
||||
/// <summary>
|
||||
/// Creates a new SubmarineInfo from a file.
|
||||
/// </summary>
|
||||
/// <param name="lazyLoad">When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.</param>
|
||||
public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true, bool lazyLoad = false)
|
||||
{
|
||||
FilePath = filePath;
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
@@ -296,11 +317,17 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
SubmarineElement = element;
|
||||
}
|
||||
}
|
||||
|
||||
Name = SubmarineElement.GetAttributeString("name", null) ?? Name;
|
||||
|
||||
Init();
|
||||
|
||||
if (lazyLoad)
|
||||
{
|
||||
LazyLoad = true;
|
||||
SubmarineElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
public SubmarineInfo(Submarine sub) : this(sub.Info)
|
||||
@@ -512,6 +539,11 @@ namespace Barotrauma
|
||||
if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); }
|
||||
}
|
||||
|
||||
public void UnloadSubmarineElement()
|
||||
{
|
||||
SubmarineElement = null;
|
||||
}
|
||||
|
||||
public bool IsVanillaSubmarine()
|
||||
{
|
||||
if (FilePath == null) { return false; }
|
||||
@@ -689,7 +721,7 @@ namespace Barotrauma
|
||||
RemoveSavedSub(filePath);
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var subInfo = new SubmarineInfo(filePath);
|
||||
var subInfo = new SubmarineInfo(filePath, lazyLoad: true);
|
||||
if (!subInfo.IsFileCorrupted)
|
||||
{
|
||||
savedSubmarines.Add(subInfo);
|
||||
|
||||
Reference in New Issue
Block a user