Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2025-04-10 10:37:09 -03:00
296 changed files with 8420 additions and 2945 deletions
@@ -11,7 +11,6 @@ namespace Barotrauma
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true);
public readonly struct AttackEventData
{
public readonly ISpatialEntity Attacker;
@@ -93,7 +93,7 @@ namespace Barotrauma
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
{
AddDamage(attack.StructureDamage, worldPosition);
AddDamage(attack.LevelWallDamage, worldPosition);
return new AttackResult(attack.StructureDamage);
}
@@ -207,11 +207,17 @@ namespace Barotrauma
private set;
}
/// <summary>
/// The top of the abyss area (the y-coordinate at which the abyss starts)
/// </summary>
public int AbyssStart
{
get { return AbyssArea.Y + AbyssArea.Height; }
}
/// <summary>
/// The bottom of the abyss area (the y-coordinate at which the abyss ends, below which there's nothing but the ocean floor)
/// </summary>
public int AbyssEnd
{
get { return AbyssArea.Y; }
@@ -547,11 +553,7 @@ namespace Barotrauma
Mirrored = mirror;
#if CLIENT
if (backgroundCreatureManager == null)
{
var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<BackgroundCreaturePrefabsFile>()).ToArray();
backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
}
backgroundCreatureManager ??= new BackgroundCreatureManager();
#endif
Stopwatch sw = new Stopwatch();
sw.Start();
@@ -2170,7 +2172,7 @@ namespace Barotrauma
}
}
if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
if (!MathUtils.GetLineWorldRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
{
caveStartPosVector = caveArea.Location.ToVector2();
}
@@ -3361,7 +3363,7 @@ namespace Barotrauma
if (r.Contains(e.Point2)) { return true; }
if (r.Contains(eCenter)) { return true; }
if (MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, r, out _))
if (MathUtils.GetLineWorldRectangleIntersection(e.Point1, e.Point2, r, out _))
{
return true;
}
@@ -3588,7 +3590,7 @@ namespace Barotrauma
public void Update(float deltaTime, Camera cam)
{
LevelObjectManager.Update(deltaTime);
LevelObjectManager.Update(deltaTime, cam);
foreach (LevelWall wall in ExtraWalls) { wall.Update(deltaTime); }
for (int i = UnsyncedExtraWalls.Count - 1; i >= 0; i--)
@@ -5083,6 +5085,11 @@ namespace Barotrauma
return Loaded != null && worldPosition.Y > Loaded.Size.Y;
}
public static bool IsPositionInAbyss(Vector2 worldPosition)
{
return Loaded != null && worldPosition.Y < loaded.AbyssStart && worldPosition.Y > loaded.AbyssEnd;
}
public void DebugSetStartLocation(Location newStartLocation)
{
StartLocation = newStartLocation;
@@ -11,10 +11,11 @@ namespace Barotrauma
{
class LevelData
{
[Flags]
public enum LevelType
{
LocationConnection,
Outpost
LocationConnection = 1,
Outpost = 2
}
public readonly LevelType Type;
@@ -87,10 +88,15 @@ namespace Barotrauma
public readonly Dictionary<EventSet, int> FinishedEvents = new Dictionary<EventSet, int>();
/// <summary>
/// For backwards compatibility (previously "exhausting" one event set exhausted all of them (now we use <see cref="exhaustedEventSets"/> instead).
/// </summary>
private bool allEventsExhausted;
/// <summary>
/// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
/// </summary>
public bool EventsExhausted { get; set; }
private HashSet<Identifier> exhaustedEventSets = new HashSet<Identifier>();
/// <summary>
/// The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the top of the level!
@@ -221,7 +227,9 @@ namespace Barotrauma
}
}
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
exhaustedEventSets = element.GetAttributeIdentifierArray(nameof(exhaustedEventSets), Array.Empty<Identifier>()).ToHashSet();
//backwards compatibility: previously we didn't track which individual event sets have been exhausted
allEventsExhausted = element.GetAttributeBool("EventsExhausted", false);
}
/// <summary>
@@ -328,6 +336,32 @@ namespace Barotrauma
return levelData;
}
/// <summary>
/// Marks the event set as "exhausted". Exhausted sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
/// </summary>
public void ExhaustEventSet(EventSet eventSet)
{
exhaustedEventSets.Add(eventSet.Identifier);
}
/// <summary>
/// Has the event set been "exhausted"? Exhausted sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
/// </summary>
public bool IsEventSetExhausted(EventSet eventSet)
{
if (allEventsExhausted) { return true; }
return exhaustedEventSets.Contains(eventSet.Identifier);
}
/// <summary>
/// Resets all "exhausted" event sets, allowing them to appear in the level again.
/// </summary>
public void ResetExhaustedEventSets()
{
allEventsExhausted = false;
exhaustedEventSets.Clear();
}
public void ReassignGenerationParams(string seed)
{
GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier);
@@ -363,7 +397,10 @@ namespace Barotrauma
new XAttribute("size", XMLExtensions.PointToString(Size)),
new XAttribute("generationparams", GenerationParams.Identifier),
new XAttribute("initialdepth", InitialDepth),
new XAttribute(nameof(EventsExhausted).ToLower(), EventsExhausted));
new XAttribute("exhaustedeventsets", allEventsExhausted));
newElement.Add(
new XAttribute(nameof(exhaustedEventSets), string.Join(',', exhaustedEventSets.Select(e => e.Value))));
if (HasBeaconStation)
{
@@ -20,7 +20,7 @@ namespace Barotrauma
private List<LevelObject> updateableObjects;
private List<LevelObject>[,] objectGrid;
const float ParallaxStrength = 0.0001f;
public const float ParallaxStrength = 0.0001f;
public float GlobalForceDecreaseTimer
{
@@ -416,11 +416,11 @@ namespace Barotrauma
}
}
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z * ParallaxStrength;
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z * ParallaxStrength;
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z;
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z;
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z * ParallaxStrength - level.BottomPos;
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z * ParallaxStrength - level.BottomPos;
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos;
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos;
if (newObject.Triggers != null)
{
@@ -455,6 +455,8 @@ namespace Barotrauma
#endif
objects.Add(newObject);
if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); }
//add some variance to the Z position to prevent z-fighting
//(based on the x and y position of the object, scaled to be visually insignificant)
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
int xStart = (int)Math.Floor(minX / GridSize);
@@ -566,7 +568,7 @@ namespace Barotrauma
return availableSpawnPositions;
}
public void Update(float deltaTime)
public void Update(float deltaTime, Camera cam)
{
GlobalForceDecreaseTimer += deltaTime;
if (GlobalForceDecreaseTimer > 1000000.0f)
@@ -612,10 +614,10 @@ namespace Barotrauma
}
}
UpdateProjSpecific(deltaTime);
UpdateProjSpecific(deltaTime, cam);
}
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateProjSpecific(float deltaTime, Camera cam);
private void OnObjectTriggered(LevelObject triggeredObject, LevelTrigger trigger, Entity triggerer)
{
@@ -1,7 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -136,6 +135,14 @@ namespace Barotrauma
private set;
}
[Serialize(3000.0f, IsPropertySaveable.Yes, description: "Objects fade out to the background color of the level the further they are from the camera. This value is the depth at which the object becomes \"maximally\" faded out."), Editable]
public float FadeOutDepth
{
get;
private set;
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f),
Serialize(0.0f, IsPropertySaveable.Yes, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")]
/// <summary>
@@ -76,6 +76,17 @@ namespace Barotrauma
public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
/// <summary>
/// How many "world steps" (<see cref="Map.ProgressWorld(CampaignMode)"/> must pass for the stores to be reset in the location?
/// Mainly an optimization
/// </summary>
public const int ClearStoresDelay = 10;
/// <summary>
/// How many "world steps" (<see cref="Map.ProgressWorld(CampaignMode)"/> have passed since this location was last visited?
/// </summary>
public int WorldStepsSinceVisited;
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
public int LocationTypeChangeCooldown;
@@ -103,6 +114,11 @@ namespace Barotrauma
public Faction SecondaryFaction { get; set; }
/// <summary>
/// Not used by the vanilla game. Can be used by code mods to change the color of the location icon on the campaign map.
/// </summary>
public Color? OverrideIconColor;
public Reputation Reputation => Faction?.Reputation;
public bool IsFactionHostile => Faction?.Reputation.NormalizedValue < Reputation.HostileThreshold;
@@ -131,7 +147,7 @@ namespace Barotrauma
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
/// <summary>
/// The maximum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation).
/// The minimum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation).
/// </summary>
private float MinReputationModifier => Location.StoreMinReputationModifier;
@@ -198,9 +214,11 @@ namespace Barotrauma
}
}
public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
public static PurchasedItem CreateInitialStockItem(Location location, ItemPrefab itemPrefab, PriceInfo priceInfo)
{
int quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1);
//simulate stores stocking up if the location hasn't been visited in a while
quantity = Math.Min(quantity + location.WorldStepsSinceVisited, priceInfo.MaxAvailableAmount);
return new PurchasedItem(itemPrefab, quantity, buyer: null);
}
@@ -210,7 +228,7 @@ namespace Barotrauma
foreach (var prefab in ItemPrefab.Prefabs)
{
if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; }
stock.Add(CreateInitialStockItem(prefab, priceInfo));
stock.Add(CreateInitialStockItem(Location, prefab, priceInfo));
}
return stock;
}
@@ -263,6 +281,7 @@ namespace Barotrauma
}
availableStock.Add(stockItem.ItemPrefab, weight);
}
DailySpecials.Clear();
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
@@ -273,14 +292,16 @@ namespace Barotrauma
DailySpecials.Add(item);
availableStock.Remove(item);
}
RequestedGoods.Clear();
for (int i = 0; i < Location.RequestedGoodsCount; i++)
{
var item = ItemPrefab.Prefabs.GetRandom(p =>
p.CanBeSold && !RequestedGoods.Contains(p) &&
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
if (item == null) { break; }
RequestedGoods.Add(item);
var selectedPrefab = ItemPrefab.Prefabs.GetRandom(prefab =>
prefab.CanBeSold && !RequestedGoods.Contains(prefab) &&
prefab.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
if (selectedPrefab == null) { break; }
RequestedGoods.Add(selectedPrefab);
}
Location.StepsSinceSpecialsUpdated = 0;
}
@@ -296,7 +317,7 @@ namespace Barotrauma
{
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
float price = priceInfo.Price;
float price = Location.StoreBuyPriceModifier * priceInfo.Price;
// Adjust by random price modifier
price = (100 + PriceModifier) / 100.0f * price;
price *= priceInfo.BuyingPriceMultiplier;
@@ -305,6 +326,11 @@ namespace Barotrauma
{
price = Location.DailySpecialPriceModifier * price;
}
// Adjust by requested good status (to avoid the store selling items that it requests potentially for less than it pays for them)
if (RequestedGoods.Contains(item))
{
price = Location.RequestGoodBuyPriceModifier * price;
}
// Adjust by current reputation
price *= GetReputationModifier(true);
@@ -332,7 +358,7 @@ namespace Barotrauma
}
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// <param name="considerRequestedGoods">If false, the price won't be affected by <see cref="RequestGoodPriceModifier"/></param>
/// <param name="considerRequestedGoods">If false, the price won't be affected by <see cref="Barotrauma.Location.RequestGoodSellPriceModifier"/></param>
public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
{
priceInfo ??= item?.GetPriceInfo(this);
@@ -343,7 +369,7 @@ namespace Barotrauma
// Adjust by requested good status
if (considerRequestedGoods && RequestedGoods.Contains(item))
{
price = Location.RequestGoodPriceModifier * price;
price = Location.RequestGoodSellPriceModifier * price;
}
// Adjust by location reputation
price *= GetReputationModifier(false);
@@ -404,13 +430,15 @@ namespace Barotrauma
}
}
public Dictionary<Identifier, StoreInfo> Stores { get; set; }
public Dictionary<Identifier, StoreInfo> Stores { get; private set; }
private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
private float StoreMinReputationModifier => Type.StoreMinReputationModifier;
private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
private float StoreBuyPriceModifier => Type.StoreBuyPriceModifier;
private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
private float RequestGoodBuyPriceModifier => Type.RequestGoodBuyPriceModifier;
private float RequestGoodSellPriceModifier => Type.RequestGoodPriceModifier;
public int StoreInitialBalance => Type.StoreInitialBalance;
private int StorePriceModifierRange => Type.StorePriceModifierRange;
@@ -588,24 +616,11 @@ namespace Barotrauma
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
LoadChangingProperties(element, campaign);
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0);
var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
if (!factionIdentifier.IsEmpty)
{
Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
}
var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
if (!secondaryFactionIdentifier.IsEmpty)
{
SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
}
Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty);
if (biomeId != Identifier.Empty)
{
@@ -697,6 +712,24 @@ namespace Barotrauma
}
}
/// <summary>
/// Load the values of properties that can change mid-campaign and need to be updated when a client receives a new campaign save from the server
/// </summary>
public void LoadChangingProperties(XElement element, CampaignMode campaign)
{
PriceMultiplier = element.GetAttributeFloat(nameof(PriceMultiplier), 1.0f);
MechanicalPriceMultiplier = element.GetAttributeFloat(nameof(MechanicalPriceMultiplier), 1.0f);
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
StepsSinceSpecialsUpdated = element.GetAttributeInt(nameof(StepsSinceSpecialsUpdated), 0);
WorldStepsSinceVisited = element.GetAttributeInt(nameof(WorldStepsSinceVisited), 0);
var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
}
public void LoadLocationTypeChange(XElement locationElement)
{
TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
@@ -793,13 +826,20 @@ namespace Barotrauma
if (Type.Faction == Identifier.Empty) { Faction = null; }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
}
UnlockInitialMissions(Rand.RandSync.Unsynced);
if (!IsCriticallyRadiated())
{
UnlockInitialMissions(Rand.RandSync.Unsynced);
}
if (createStores)
{
CreateStores(force: true);
}
else
{
ClearStores();
}
}
public void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
@@ -1089,12 +1129,17 @@ namespace Barotrauma
return false;
}
public LocationType GetLocationType()
public LocationType GetLocationTypeToDisplay(out Identifier overrideDescriptionIdentifier)
{
overrideDescriptionIdentifier = Identifier.Empty;
if (IsCriticallyRadiated() && !Type.ReplaceInRadiation.IsEmpty)
{
if (LocationType.Prefabs.TryGet(Type.ReplaceInRadiation, out LocationType newLocationType))
{
if (!newLocationType.DescriptionInRadiation.IsEmpty)
{
overrideDescriptionIdentifier = newLocationType.DescriptionInRadiation;
}
return newLocationType;
}
else
@@ -1105,6 +1150,11 @@ namespace Barotrauma
return Type;
}
public LocationType GetLocationTypeToDisplay()
{
return GetLocationTypeToDisplay(out _);
}
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
{
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
@@ -1223,8 +1273,11 @@ namespace Barotrauma
{
UpdateStoreIdentifiers();
Stores?.Clear();
bool hasStores = false;
foreach (var storeElement in locationElement.GetChildElements("store"))
{
hasStores = true;
Stores ??= new Dictionary<Identifier, StoreInfo>();
var identifier = storeElement.GetAttributeIdentifier("identifier", "");
if (identifier.IsEmpty)
@@ -1255,13 +1308,16 @@ namespace Barotrauma
}
}
// Backwards compatibility: create new stores for any identifiers not present in the save data
foreach (var id in StoreIdentifiers)
if (hasStores)
{
AddNewStore(id);
foreach (var id in StoreIdentifiers)
{
AddNewStore(id);
}
}
}
public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this);
public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.DepthInRadiation(this) > 0;
/// <summary>
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
@@ -1331,6 +1387,9 @@ namespace Barotrauma
return null;
}
/// <summary>
/// Create stores and stocks for the location. If the location already has stores, the method will not do anything unless the "force" argument is true. />
/// </summary>
/// <param name="force">If true, the stores will be recreated if they already exists.</param>
public void CreateStores(bool force = false)
{
@@ -1384,13 +1443,13 @@ namespace Barotrauma
}
}
public void UpdateStores()
public void UpdateStores(bool createStoresIfNotCreated = true)
{
// In multiplayer, stores should be updated by the server and loaded from save data by clients
if (GameMain.NetworkMember is { IsClient: true }) { return; }
if (Stores == null)
{
CreateStores();
if (createStoresIfNotCreated) { CreateStores(); }
return;
}
var storesToRemove = new HashSet<Identifier>();
@@ -1410,13 +1469,13 @@ namespace Barotrauma
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab);
var existingStock = stock.FirstOrDefault(s => s.ItemPrefabIdentifier == itemPrefab.Identifier);
if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo))
{
if (existingStock == null)
{
//can be bought from the location, but not in stock - some new item added by an update or mod?
stock.Add(StoreInfo.CreateInitialStockItem(itemPrefab, priceInfo));
stock.Add(StoreInfo.CreateInitialStockItem(this, itemPrefab, priceInfo));
}
else
{
@@ -1496,6 +1555,16 @@ namespace Barotrauma
}
}
/// <summary>
/// Removes all information about stores from the location (can be used to avoid storing unnecessary
/// store info about locations that haven't been visited in a long time). The stores are automatically
/// recreated when the player enters the location.
/// </summary>
public void ClearStores()
{
Stores = null;
}
public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
{
if (items == null) { return; }
@@ -1548,7 +1617,7 @@ namespace Barotrauma
ChangeType(campaign, OriginalType);
PendingLocationTypeChange = null;
}
CreateStores(force: true);
ClearStores();
ClearMissions();
LevelData?.EventHistory?.Clear();
UnlockInitialMissions();
@@ -1569,7 +1638,8 @@ namespace Barotrauma
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
new XAttribute(nameof(StepsSinceSpecialsUpdated), StepsSinceSpecialsUpdated),
new XAttribute(nameof(WorldStepsSinceVisited), WorldStepsSinceVisited));
if (!rawName.IsNullOrEmpty())
{
@@ -1726,3 +1796,4 @@ namespace Barotrauma
}
}
}
@@ -91,6 +91,8 @@ namespace Barotrauma
public Identifier ReplaceInRadiation { get; }
public Identifier DescriptionInRadiation { get; }
/// <summary>
/// If set, forces the location to be assigned to this faction. Set to "None" if you don't want the location to be assigned to any faction.
/// </summary>
@@ -120,8 +122,10 @@ namespace Barotrauma
public float StoreMaxReputationModifier { get; } = 0.1f;
public float StoreMinReputationModifier { get; } = 1.0f;
public float StoreSellPriceModifier { get; } = 0.3f;
public float StoreBuyPriceModifier { get; } = 1f;
public float DailySpecialPriceModifier { get; } = 0.5f;
public float RequestGoodPriceModifier { get; } = 2f;
public float RequestGoodBuyPriceModifier { get; } = 5f;
public int StoreInitialBalance { get; } = 5000;
/// <summary>
/// In percentages
@@ -161,6 +165,7 @@ namespace Barotrauma
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", Array.Empty<string>()).ToList();
ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty);
DescriptionInRadiation = element.GetAttributeIdentifier(nameof(DescriptionInRadiation), "locationdescription.abandonedirradiated");
forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty);
@@ -265,10 +270,12 @@ namespace Barotrauma
break;
case "store":
StoreMaxReputationModifier = subElement.GetAttributeFloat("maxreputationmodifier", StoreMaxReputationModifier);
StoreBuyPriceModifier = subElement.GetAttributeFloat("buypricemodifier", StoreBuyPriceModifier);
StoreMinReputationModifier = subElement.GetAttributeFloat("minreputationmodifier", StoreMaxReputationModifier);
StoreSellPriceModifier = subElement.GetAttributeFloat("sellpricemodifier", StoreSellPriceModifier);
DailySpecialPriceModifier = subElement.GetAttributeFloat("dailyspecialpricemodifier", DailySpecialPriceModifier);
RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier);
RequestGoodBuyPriceModifier = subElement.GetAttributeFloat("requestgoodbuypricemodifier", RequestGoodBuyPriceModifier);
StoreInitialBalance = subElement.GetAttributeInt("initialbalance", StoreInitialBalance);
StorePriceModifierRange = subElement.GetAttributeInt("pricemodifierrange", StorePriceModifierRange);
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
@@ -328,7 +328,7 @@ namespace Barotrauma
{
if (LocationType.Prefabs.TryGet("outpost".ToIdentifier(), out LocationType outpostLocationType))
{
otherLocation.ChangeType(campaign, outpostLocationType);
otherLocation.ChangeType(campaign, outpostLocationType, createStores: false);
}
}
@@ -732,7 +732,6 @@ namespace Barotrauma
location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
}
}
location.CreateStores(force: true);
}
foreach (LocationConnection connection in Connections)
@@ -1020,11 +1019,11 @@ namespace Barotrauma
}
CurrentLocation = SelectedLocation;
CurrentLocation.CreateStores();
Discover(CurrentLocation);
Visit(CurrentLocation);
SelectedLocation = null;
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
@@ -1212,7 +1211,19 @@ namespace Barotrauma
{
foreach (Location location in Locations)
{
location.LevelData.EventsExhausted = false;
if (location.Visited)
{
location.WorldStepsSinceVisited++;
if (location.WorldStepsSinceVisited > Location.ClearStoresDelay)
{
location.ClearStores();
}
}
else
{
location.ClearStores();
}
location.LevelData.ResetExhaustedEventSets();
if (location.Discovered)
{
if (furthestDiscoveredLocation == null ||
@@ -1224,7 +1235,7 @@ namespace Barotrauma
}
foreach (LocationConnection connection in Connections)
{
connection.LevelData.EventsExhausted = false;
connection.LevelData.ResetExhaustedEventSets();
}
foreach (Location location in Locations)
@@ -1234,12 +1245,27 @@ namespace Barotrauma
continue;
}
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
bool shouldUpdateStores = location.Discovered;
//don't allow the type of the current location or the destination to change (it'd be weird to arrive at a different type of location than the one you were travelling to)
//biome gates should also remain unchanged
bool shouldProcessLocationTypeChanges = location != CurrentLocation && location != SelectedLocation && !location.IsGateBetweenBiomes;
if (shouldProcessLocationTypeChanges &&
ProgressLocationTypeChanges(campaign, location))
{
location.UpdateStores();
//don't update stores if the location type changed (that recreates the stores anyway)
shouldUpdateStores = false;
}
if (shouldUpdateStores)
{
location.UpdateStores(createStoresIfNotCreated: false);
}
}
if (CurrentLocation != null)
{
CurrentLocation.UpdateStores(createStoresIfNotCreated: true);
CurrentLocation.WorldStepsSinceVisited = 0;
}
}
@@ -1338,7 +1364,7 @@ namespace Barotrauma
{
location.ClearMissions();
}
location.ChangeType(campaign, newType);
location.ChangeType(campaign, newType, createStores: false);
ChangeLocationTypeProjSpecific(location, prevName, change);
foreach (var requirement in change.Requirements)
{
@@ -1409,9 +1435,13 @@ namespace Barotrauma
}
}
public void Visit(Location location)
public void Visit(Location location, bool resetTimeSinceVisited = true)
{
if (location is null) { return; }
if (resetTimeSinceVisited)
{
location.WorldStepsSinceVisited = 0;
}
if (locationsVisited.Contains(location)) { return; }
locationsVisited.Add(location);
RemoveFogOfWarProjSpecific(location);
@@ -1510,12 +1540,14 @@ namespace Barotrauma
}
location.LoadLocationTypeChange(subElement);
location.LoadChangingProperties(subElement, campaign);
// Backwards compatibility: if the discovery status is defined in the location element,
// the game was saved using when the discovery order still wasn't being tracked
if (subElement.GetAttributeBool("discovered", false))
{
Discover(location);
Visit(location);
Visit(location, resetTimeSinceVisited: false);
trackedLocationDiscoveryAndVisitOrder = false;
}
@@ -1525,9 +1557,6 @@ namespace Barotrauma
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
location.ChangeType(campaign, newLocationType);
var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
if (showNotifications && prevLocationType != location.Type)
{
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
@@ -1538,9 +1567,6 @@ namespace Barotrauma
}
}
var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
location.LoadStores(subElement);
location.LoadMissions(subElement);
@@ -1563,16 +1589,24 @@ namespace Barotrauma
break;
case "discovered":
bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false);
int[] discoveredIndices = subElement.GetAttributeIntArray("indices", Array.Empty<int>());
foreach (int discoveredIndex in discoveredIndices)
{
Discover(Locations[discoveredIndex]);
}
//backwards compatibility
foreach (var childElement in subElement.GetChildElements("location"))
{
if (GetLocation(childElement) is Location l)
{
Discover(l);
//even older backwards compatibility: previously we didn't track whether you've "visited" empty locations,
//nor the order in which locations are visited - we need to handle that here
if (!trackedVisitedEmptyLocations)
{
if (!l.HasOutpost())
{
Visit(l);
Visit(l, resetTimeSinceVisited: false);
}
trackedLocationDiscoveryAndVisitOrder = false;
}
@@ -1580,11 +1614,17 @@ namespace Barotrauma
}
break;
case "visited":
int[] visitedIndices = subElement.GetAttributeIntArray("indices", Array.Empty<int>());
foreach (int visitedIndex in visitedIndices)
{
Visit(Locations[visitedIndex], resetTimeSinceVisited: false);
}
//backwards compatibility
foreach (var childElement in subElement.GetChildElements("location"))
{
if (GetLocation(childElement) is Location l)
{
Visit(l);
Visit(l, resetTimeSinceVisited: false);
}
}
break;
@@ -1708,25 +1748,15 @@ namespace Barotrauma
if (locationsDiscovered.Any())
{
var discoveryElement = new XElement("discovered",
new XAttribute("trackedvisitedemptylocations", true));
foreach (Location location in locationsDiscovered)
{
int index = Locations.IndexOf(location);
var locationElement = new XElement("location", new XAttribute("i", index));
discoveryElement.Add(locationElement);
}
new XAttribute("trackedvisitedemptylocations", true),
new XAttribute("indices", string.Join(',', locationsDiscovered.Select(l => Locations.IndexOf(l)))));
mapElement.Add(discoveryElement);
}
if (locationsVisited.Any())
{
var visitElement = new XElement("visited");
foreach (Location location in locationsVisited)
{
int index = Locations.IndexOf(location);
var locationElement = new XElement("location", new XAttribute("i", index));
visitElement.Add(locationElement);
}
var visitElement = new XElement("visited",
new XAttribute("indices", string.Join(',', locationsVisited.Select(l => Locations.IndexOf(l)))));
mapElement.Add(visitElement);
}
@@ -25,6 +25,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowOverlay { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "When enabled, locations that have an active store (= a store whose stocks are kept track of in the save file) are displayed in green, locations with no active store (due to not having been visited in a long time, or not having a store in the first place) are displayed in blue, and everything else in yellow."), Editable]
public bool ShowStoreInfo { get; set; }
#else
public readonly bool ShowLocations = true;
public readonly bool ShowLevelTypeNames = false;
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -23,8 +23,6 @@ namespace Barotrauma
public readonly Map Map;
public readonly RadiationParams Params;
private Affliction? radiationAffliction;
private float radiationTimer;
private float increasedAmount;
@@ -62,7 +60,7 @@ namespace Barotrauma
int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated());
foreach (Location location in Map.Locations.Where(Contains))
foreach (Location location in Map.Locations.Where(l => DepthInRadiation(l) > 0))
{
if (location.IsGateBetweenBiomes)
{
@@ -96,8 +94,6 @@ namespace Barotrauma
increasedAmount = lastIncrease = amount;
}
public void UpdateRadiation(float deltaTime)
{
if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; }
@@ -110,55 +106,66 @@ namespace Barotrauma
return;
}
if (radiationAffliction == null)
{
float radiationStrengthChange = AfflictionPrefab.RadiationSickness.Effects.FirstOrDefault()?.StrengthChange ?? 0.0f;
radiationAffliction = new Affliction(
AfflictionPrefab.RadiationSickness,
(Params.RadiationDamageAmount - radiationStrengthChange) * Params.RadiationDamageDelay);
}
radiationTimer = Params.RadiationDamageDelay;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.Removed || !(character.CharacterHealth is { } health)) { continue; }
if (IsEntityRadiated(character))
float depthInRadiation = DepthInRadiation(character);
if (depthInRadiation > 0)
{
var limb = character.AnimController.MainLimb;
AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false);
character.CharacterHealth.ApplyDamage(limb, attackResult);
AfflictionPrefab afflictionPrefab;
// Get the related affliction (if necessary, fall back to the traditional radiation sickness for slightly better backwards compatibility)
afflictionPrefab = AfflictionPrefab.JovianRadiation ?? AfflictionPrefab.RadiationSickness;
float currentAfflictionStrength = character.CharacterHealth.GetAfflictionStrengthByIdentifier(afflictionPrefab.Identifier);
// Get Jovian radiation strength, and cancel out the affliction's strength change (meant for decaying it)
// (for simplicity, let's assume each Effect of the Affliction has the same strengthchange)
float addedStrength = Params.RadiationDamageAmount - afflictionPrefab.Effects.FirstOrDefault()?.StrengthChange ?? 0.0f;
// Damage is applied periodically, so we must apply the total damage for the full period at once (after deducting strengthchange)
addedStrength *= Params.RadiationDamageDelay;
// The JovianRadiation affliction has brackets of 25 strength determined by the multiplier (1x = 0-25, 2x = 25-50 etc.)
int multiplier = (int)Math.Ceiling(depthInRadiation / Params.RadiationEffectMultipliedPerPixelDistance);
float growthPotentialInBracket = (multiplier * 25) - currentAfflictionStrength;
if (growthPotentialInBracket > 0)
{
addedStrength = Math.Min(addedStrength, growthPotentialInBracket);
character.CharacterHealth.ApplyAffliction(
character.AnimController?.MainLimb,
afflictionPrefab.Instantiate(addedStrength));
}
}
}
}
public bool Contains(Location location)
public float DepthInRadiation(Location location)
{
return Contains(location.MapPosition);
return DepthInRadiation(location.MapPosition);
}
private float DepthInRadiation(Vector2 pos)
{
return Amount - pos.X;
}
public bool Contains(Vector2 pos)
public float DepthInRadiation(Entity entity)
{
return pos.X < Amount;
}
public bool IsEntityRadiated(Entity entity)
{
if (!Enabled) { return false; }
if (!Enabled) { return 0; }
if (Level.Loaded is { Type: LevelData.LevelType.LocationConnection, StartLocation: { } startLocation, EndLocation: { } endLocation } level)
{
if (Contains(startLocation) && Contains(endLocation)) { return true; }
float distance = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
// Approximate how far between the level start and end points the entity is on the map
float distanceNormalized = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
var (startX, startY) = startLocation.MapPosition;
var (endX, endY) = endLocation.MapPosition;
Vector2 mapPos = new Vector2(startX + (endX - startX), startY + (endY - startY)) * distance;
Vector2 mapPos = new Vector2(startX, startY) + (new Vector2(endX - startX, endY - startY) * distanceNormalized);
return Contains(mapPos);
return DepthInRadiation(mapPos);
}
return false;
return 0;
}
public XElement Save()
@@ -9,37 +9,40 @@ namespace Barotrauma
public string Name => nameof(RadiationParams);
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
[Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much radiation the world starts with.")]
[Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much Jovian radiation the world starts with.")]
public float StartingRadiation { get; set; }
[Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much radiation is added on each step.")]
[Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much Jovian radiation is added on each step.")]
public float RadiationStep { get; set; }
[Serialize(defaultValue: 250f, isSaveable: IsPropertySaveable.No, "The interval at which Jovian radiation's effect multiplies in intensity, measured in map pixels. For example if this is 200, then 400 map pixels into the radiation its effect will be doubled.")]
public float RadiationEffectMultipliedPerPixelDistance { get; set; }
[Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in radiation does it take for an outpost to be removed from the map.")]
[Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in Jovian radiation does it take for an outpost to be removed from the map.")]
public int CriticalRadiationThreshold { get; set; }
[Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to radiation.")]
[Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to Jovian radiation.")]
public int MinimumOutpostAmount { get; set; }
[Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the radiation increase animation goes.")]
public float AnimationSpeed { get; set; }
[Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more radiation damage while in a radiated zone.")]
[Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more of the Jovian radiation's effect while in the radiated zone.")]
public float RadiationDamageDelay { get; set; }
[Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the radiation affliction increased by while in a radiated zone.")]
[Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the Jovian radiation affliction increased by while in a radiated zone.")]
public float RadiationDamageAmount { get; set; }
[Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of radiation.")]
[Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of Jovian radiation.")]
public float MaxRadiation { get; set; }
[Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area.")]
[Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the Jovian radiation increase animation goes in the map view.")]
public float AnimationSpeed { get; set; }
[Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area in the map view.")]
public Color RadiationAreaColor { get; set; }
[Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the radiation border sprites.")]
[Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the Jovian radiation border sprites in the map view.")]
public Color RadiationBorderTint { get; set; }
[Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation.")]
[Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation in the map view.")]
public float BorderAnimationSpeed { get; set; }
public RadiationParams(XElement element)
@@ -549,7 +549,24 @@ namespace Barotrauma
return;
}
//sort damageable walls by sprite depth:
//necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise
int i = 0;
if (this is Structure { DrawDamageEffect: true } structure)
{
//insertion sort according to draw depth
float drawDepth = structure.SpriteDepth;
while (i < MapEntityList.Count)
{
float otherDrawDepth = (MapEntityList[i] as Structure)?.SpriteDepth ?? 1.0f;
if (otherDrawDepth < drawDepth) { break; }
i++;
}
MapEntityList.Insert(i, this);
return;
}
i = 0;
while (i < MapEntityList.Count)
{
i++;
@@ -8,7 +8,7 @@ namespace Barotrauma
{
public readonly static PrefabCollection<NPCSet> Sets = new PrefabCollection<NPCSet>();
private readonly ImmutableArray<HumanPrefab> Humans;
public readonly ImmutableArray<HumanPrefab> Humans;
public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
@@ -59,6 +59,13 @@ namespace Barotrauma
set;
}
[Serialize(true, IsPropertySaveable.Yes, description: "Should hallways of the minimum hallway length be always generated between modules, even if they could be placed directly against each other with no overlaps?"), Editable]
public bool AlwaysGenerateHallways
{
get;
set;
}
[Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be destructible, regardless if damaging outposts is allowed by the server?"), Editable]
public bool AlwaysDestructible
{
@@ -56,6 +56,11 @@ namespace Barotrauma
}
}
/// <summary>
/// How many times the generator retries generating an outpost with a different seed if it fails to generate a valid outpost with no overlaps.
/// </summary>
const int MaxOutpostGenerationRetries = 6;
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false)
{
return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost);
@@ -86,7 +91,7 @@ namespace Barotrauma
generationParams = newParams;
}
locationType = location.GetLocationType();
locationType = location.Type;
}
Submarine sub = null;
@@ -182,8 +187,8 @@ namespace Barotrauma
List<PlacedModule> selectedModules = new List<PlacedModule>();
bool generationFailed = false;
int remainingTries = 5;
while (remainingTries > -1 && outpostModules.Any())
int remainingOutpostGenerationTries = MaxOutpostGenerationRetries;
while (remainingOutpostGenerationTries > -1 && outpostModules.Any())
{
if (sub != null)
{
@@ -208,7 +213,7 @@ namespace Barotrauma
GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveRange(uniqueEventCount, GameMain.Server.EntityEventManager.UniqueEvents.Count - uniqueEventCount);
#endif
if (remainingTries <= 0)
if (remainingOutpostGenerationTries <= 0)
{
generationFailed = true;
break;
@@ -269,7 +274,7 @@ namespace Barotrauma
if (pendingModuleFlags.Contains("initialFlag".ToIdentifier())) { pendingModuleFlags.Remove(initialFlag); }
}
if (remainingTries == 1)
if (remainingOutpostGenerationTries == 1)
{
//generation has failed and only one attempt left, try removing duplicate modules
pendingModuleFlags = pendingModuleFlags.Distinct().ToList();
@@ -283,13 +288,13 @@ namespace Barotrauma
selectedModules,
locationType,
allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams,
allowDifferentLocationType: remainingTries == 1);
allowDifferentLocationType: remainingOutpostGenerationTries == 1);
if (GameMain.GameSession?.ForceOutpostModule != null)
{
if (remainingTries > 0)
if (remainingOutpostGenerationTries > 0)
{
remainingTries--;
remainingOutpostGenerationTries--;
continue;
}
DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
@@ -301,8 +306,8 @@ namespace Barotrauma
{
if (!allowInvalidOutpost)
{
remainingTries--;
if (remainingTries <= 0)
remainingOutpostGenerationTries--;
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));
}
@@ -346,7 +351,7 @@ namespace Barotrauma
EnableFactionSpecificEntities(sub, location);
return sub;
}
remainingTries--;
remainingOutpostGenerationTries--;
}
DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
@@ -444,9 +449,12 @@ namespace Barotrauma
selectedModule.Offset =
(selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) -
selectedModule.ThisGap.WorldPosition;
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
if (generationParams.AlwaysGenerateHallways)
{
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
{
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
}
}
}
entities[selectedModule] = moduleEntities;
@@ -467,24 +475,30 @@ namespace Barotrauma
GetSubsequentModules(placedModule, selectedModules, ref subsequentModules);
List<PlacedModule> otherModules = selectedModules.Except(subsequentModules).ToList();
int remainingTries = 10;
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0)
int remainingOverlapPreventionTries = 10;
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingOverlapPreventionTries > 0)
{
overlapsFound = true;
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, generationParams.MinHallwayLength, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
{
foreach (KeyValuePair<PlacedModule, Vector2> kvp in solution)
{
kvp.Key.Offset += kvp.Value;
kvp.Key.Offset += kvp.Value;
}
}
else
{
break;
}
remainingTries--;
remainingOverlapPreventionTries--;
}
if (remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
ModuleBelowInitialModule(placedModule, selectedModules.First()))
{
overlapsFound = true;
}
}
iteration++;
if (iteration > 10)
{
@@ -647,7 +661,8 @@ namespace Barotrauma
/// <param name="selectedModules">The modules we've already selected to be used in the outpost.</param>
/// <param name="locationType">The type of the location we're generating the outpost for.</param>
/// <param name="tryReplacingCurrentModule">If we fail to append to the current module, should we try replacing it with something else and see if we can append to it then?</param>
/// <param name="allowExtendBelowInitialModule">Is the module allowed to be placed further down than the initial module (usually the airlock module)?</param>
/// <param name="allowExtendBelowInitialModule">Is the module allowed to be placed further down than the initial module (usually the airlock module)?
/// Note that at this point we only determine which module to attach to which, but not the actual positions or bounds of the modules, so it's possible for a module to attach to the side of the airlock but still extend below the airlock if it's very tall for example.</param>
/// <param name="allowDifferentLocationType">If we fail to find a module suitable for the location type, should we use a module that's meant for a different location type instead?</param>
private static bool AppendToModule(PlacedModule currentModule,
List<SubmarineInfo> availableModules,
@@ -886,7 +901,7 @@ namespace Barotrauma
Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset;
if (Submarine.RectContains(rect, gapPos1) ||
Submarine.RectContains(rect, gapPos2) ||
MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _))
MathUtils.GetLineWorldRectangleIntersection(gapPos1, gapPos2, rect, out _))
{
return true;
}
@@ -904,6 +919,21 @@ namespace Barotrauma
return false;
}
/// <summary>
/// Check if the lowest point of the module is below the lowest point of the initial (docking) module.
/// This shouldn't happen, because it can cause modules to overlap with the docked sub.
/// </summary>
private static bool ModuleBelowInitialModule(PlacedModule module, PlacedModule initialModule)
{
Rectangle bounds = module.Bounds;
bounds.Location += (module.Offset + module.MoveOffset).ToPoint();
Rectangle initialModuleBounds = initialModule.Bounds;
initialModuleBounds.Location += (initialModule.Offset + initialModule.MoveOffset).ToPoint();
return bounds.Bottom < initialModuleBounds.Bottom;
}
/// <summary>
/// Attempt to find a way to move the modules in a way that stops the 2 specific modules from overlapping.
/// Done by iterating through the modules and testing how much the subsequent modules (i.e. modules that are further from the initial outpost)
@@ -919,6 +949,7 @@ namespace Barotrauma
IEnumerable<PlacedModule> movableModules,
PlacedModule module1, PlacedModule module2,
IEnumerable<PlacedModule> allmodules,
float minMoveAmount,
int maxMoveAmount,
out Dictionary<PlacedModule, Vector2> solution)
{
@@ -934,14 +965,13 @@ namespace Barotrauma
{
if (module.ThisGap.ConnectedDoor == null && module.PreviousGap.ConnectedDoor == null) { continue; }
Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
Vector2 moveStep = moveDir * 50.0f;
Vector2 currentMove = Vector2.Zero;
const float moveStep = 50.0f;
Vector2 currentMove = moveDir * Math.Max(minMoveAmount, moveStep);
List<PlacedModule> subsequentModules2 = new List<PlacedModule>();
GetSubsequentModules(module, movableModules, ref subsequentModules2);
while (currentMove.LengthSquared() < maxMoveAmount * maxMoveAmount)
{
currentMove += moveStep;
foreach (PlacedModule movedModule in subsequentModules2)
{
movedModule.MoveOffset = currentMove;
@@ -958,6 +988,7 @@ namespace Barotrauma
}
break;
}
currentMove += moveDir * moveStep;
}
foreach (PlacedModule movedModule in allmodules)
{
@@ -98,10 +98,19 @@ namespace Barotrauma
{
get { return base.Prefab.Name.Value; }
}
public bool HasBody
{
get { return Prefab.Body; }
get { return Prefab.Body && !DisableCollision; }
}
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBodyByDefault)]
/// <summary>
/// Note that changing the value mid-round will not have an effect: this is only intended for disabling the collisions on a structure in the sub editor.
/// </summary>
public bool DisableCollision
{
get;
set;
}
public List<Body> Bodies { get; private set; }
@@ -254,7 +263,7 @@ namespace Barotrauma
{
CreateStairBodies();
}
else if (Prefab.Body)
else if (HasBody)
{
CreateSections();
UpdateSections();
@@ -325,7 +334,7 @@ namespace Barotrauma
{
Rectangle oldRect = Rect;
base.Rect = value;
if (Prefab.Body)
if (HasBody)
{
CreateSections();
UpdateSections();
@@ -506,10 +515,16 @@ namespace Barotrauma
Indestructible = Prefab.ConfigElement.GetAttributeBool(nameof(Indestructible), false);
}
//if the prefab normally has a body, but it has been disabled by DisableCollision,
//we still want the item in the wall list to render it correctly
if (Prefab.Body)
{
Bodies = new List<Body>();
WallList.Add(this);
}
if (HasBody)
{
Bodies = new List<Body>();
CreateSections();
UpdateSections();
}
@@ -969,7 +984,7 @@ namespace Barotrauma
public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true, bool createWallDamageProjectiles = false)
{
if (!Prefab.Body || Prefab.Platform || Indestructible) { return; }
if (!HasBody || Prefab.Platform || Indestructible) { return; }
if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; }
@@ -1115,7 +1130,7 @@ namespace Barotrauma
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = false)
{
if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); }
if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); }
if (!HasBody || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); }
Vector2 transformedPos = worldPosition;
if (Submarine != null) { transformedPos -= Submarine.Position; }
@@ -1177,7 +1192,7 @@ namespace Barotrauma
bool createWallDamageProjectiles = false)
{
if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; }
if (!Prefab.Body) { return; }
if (!HasBody) { return; }
if (!MathUtils.IsValid(damage)) { return; }
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
@@ -1219,7 +1234,9 @@ namespace Barotrauma
Sections[sectionIndex].gap = null;
}
}
else
//do not create gaps on damaged walls in editors,
//they're created at the start of a round and "pre-creating" them in the editors causes issues (see #12998)
else if (Screen.Selected is not { IsEditor: true })
{
float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f;
if (Sections[sectionIndex].gap == null)
@@ -1727,7 +1744,7 @@ namespace Barotrauma
//structures with a body drop a shadow by default
if (element.GetAttribute(nameof(UseDropShadow)) == null)
{
s.UseDropShadow = prefab.Body;
s.UseDropShadow = s.HasBody;
}
if (element.GetAttribute(nameof(NoAITarget)) == null)
@@ -1917,6 +1917,7 @@ namespace Barotrauma
}
element.Add(new XAttribute("tags", Info.Tags.ToString()));
element.Add(new XAttribute("outposttags", Info.OutpostTags.ConvertToString()));
element.Add(new XAttribute("triggeroutpostmissionevents", Info.TriggerOutpostMissionEvents.ConvertToString()));
element.Add(new XAttribute("gameversion", GameMain.Version.ToString()));
Rectangle dimensions = VisibleBorders;
@@ -515,7 +515,7 @@ namespace Barotrauma
//cast a line from the position of the character to the same direction as the translation of the sub
//and see where it intersects with the bounding box
if (!MathUtils.GetLineRectangleIntersection(limb.WorldPosition,
if (!MathUtils.GetLineWorldRectangleIntersection(limb.WorldPosition,
limb.WorldPosition + translateDir * 100000.0f, worldBorders, out Vector2 intersection))
{
//should never happen when casting a line out from inside the bounding box
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -129,24 +129,23 @@ namespace Barotrauma
public ImmutableHashSet<Identifier> OutpostTags { get; set; } = ImmutableHashSet<Identifier>.Empty;
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
public ImmutableHashSet<Identifier> TriggerOutpostMissionEvents { get; set; } = ImmutableHashSet<Identifier>.Empty;
public bool IsOutpost => Type is SubmarineType.Outpost or SubmarineType.OutpostModule;
public bool IsWreck => Type == SubmarineType.Wreck;
public bool IsBeacon => Type == SubmarineType.BeaconStation;
public bool IsEnemySubmarine => Type == SubmarineType.EnemySubmarine;
public bool IsPlayer => Type == SubmarineType.Player;
public bool IsRuin => Type == SubmarineType.Ruin;
/// <summary>
/// Ruin modules are of type SubmarineType.OutpostModule, until the ruin generator (or the test game mode) sets them as ruins.
/// This is a helper workaround check intended to be used only in the context of the sub editor and the test game mode, where ruins aren't generated.
/// </summary>
public bool ShouldBeRuin => Type is SubmarineType.Ruin or SubmarineType.OutpostModule &&
(OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinentrance".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinvault".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinworkshop".ToIdentifier()) ||
OutpostModuleInfo.ModuleFlags.Contains("ruinshrine".ToIdentifier()));
public bool ShouldBeRuin =>
Type is SubmarineType.Ruin or SubmarineType.OutpostModule &&
OutpostModuleInfo.ModuleFlags.Any(f => f.StartsWith("ruin"));
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);
@@ -341,6 +340,7 @@ namespace Barotrauma
OutpostGenerationParams = original.OutpostGenerationParams;
LayersHiddenByDefault = original.LayersHiddenByDefault;
OutpostTags = original.OutpostTags;
TriggerOutpostMissionEvents = original.TriggerOutpostMissionEvents;
if (original.OutpostModuleInfo != null)
{
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
@@ -438,6 +438,14 @@ namespace Barotrauma
OutpostTags = SubmarineElement.GetAttributeIdentifierImmutableHashSet(nameof(OutpostTags), ImmutableHashSet<Identifier>.Empty);
TriggerOutpostMissionEvents = SubmarineElement.GetAttributeIdentifierImmutableHashSet(nameof(TriggerOutpostMissionEvents), ImmutableHashSet<Identifier>.Empty);
//backwards compatibility: previously the outpost deathmatch mission always triggered an event with the tag "deathmatchweapondrop"
//now that's configured in the outpost itself, so let's make older outposts trigger it automatically
if (GameVersion < new Version(1, 8, 0, 0) && OutpostTags.Contains("PvPOutpost"))
{
TriggerOutpostMissionEvents = TriggerOutpostMissionEvents.Add("deathmatchweapondrop".ToIdentifier());
}
if (SubmarineElement?.Attribute("type") != null)
{
if (Enum.TryParse(SubmarineElement.GetAttributeString("type", ""), out SubmarineType type))
@@ -1011,26 +1011,52 @@ namespace Barotrauma
return assignedWayPoints;
}
public static List<WayPoint> GetOutpostSpawnPoints(CharacterTeamType teamID)
/// <summary>
/// Find appropriate spawnpoints in the outpost for the player crew (preferring job-specific spawnpoints in the initial/airlock module normally, and job and team -specific spawnpoints in the PvP mode).
/// </summary>
public static WayPoint[] SelectOutpostSpawnPoints(List<CharacterInfo> crew, CharacterTeamType teamID)
{
List<WayPoint> spawnWaypoints = WayPointList.FindAll(wp =>
List<WayPoint> potentialSpawnPoints = WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost);
if (GameMain.GameSession.GameMode is PvPMode)
{
Identifier teamSpawnTag = ("deathmatch" + teamID).ToIdentifier();
if (spawnWaypoints.Any(wp => wp.Tags.Contains(teamSpawnTag)))
if (potentialSpawnPoints.Any(wp => wp.Tags.Contains(teamSpawnTag)))
{
spawnWaypoints = spawnWaypoints.FindAll(wp => wp.Tags.Contains(teamSpawnTag));
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.Tags.Contains(teamSpawnTag));
}
}
else
{
spawnWaypoints = spawnWaypoints.FindAll(wp =>
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp =>
wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull.OutpostModuleTags.Contains(Barotrauma.Tags.Airlock));
}
return spawnWaypoints;
if (potentialSpawnPoints.None()) { return potentialSpawnPoints.ToArray(); }
List<WayPoint> spawnPoints = new List<WayPoint>();
for (int i = 0; i < crew.Count; i++)
{
var spawnPointsForJob = potentialSpawnPoints.Where(wp => wp.AssignedJob == crew[i].Job.Prefab);
var spawnPointsForAnyJob = potentialSpawnPoints.Where(wp => wp.AssignedJob == null);
if (spawnPointsForJob.Any())
{
//prefer job-specific spawnpoints
spawnPoints.Add(spawnPointsForJob.GetRandomUnsynced());
}
else if (spawnPointsForAnyJob.Any())
{
//2nd option: a non-job-specific spawnpoint
spawnPoints.Add(spawnPointsForAnyJob.GetRandomUnsynced());
}
else
{
//last option: whatever spawnpoint (no matter if it's not for this job)
spawnPoints.Add(potentialSpawnPoints.GetRandomUnsynced());
}
}
return spawnPoints.ToArray();
}
public void FindHull()