Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -3,6 +3,7 @@ using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -62,6 +63,8 @@ namespace Barotrauma
private int nameFormatIndex;
private Identifier nameIdentifier;
public int NameFormatIndex => nameFormatIndex;
/// <summary>
/// For backwards compatibility: a non-localizable name from the old text files.
/// </summary>
@@ -73,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;
@@ -80,7 +94,7 @@ namespace Barotrauma
/// <summary>
/// Is some mission blocking this location from changing its type, or have location type changes been forcibly disabled on the location?
/// </summary>
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges);
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => !m.Completed && m.Prefab.BlockLocationTypeChanges);
public bool DisallowLocationTypeChanges;
@@ -100,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;
@@ -121,8 +140,17 @@ namespace Barotrauma
/// </summary>
public int PriceModifier { get; set; }
public Location Location { get; }
/// <summary>
/// The maximum effect positive reputation can have on store prices (e.g. 0.5 = 50% discount with max reputation).
/// </summary>
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
/// <summary>
/// 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;
private StoreInfo(Location location)
{
Location = location;
@@ -186,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);
}
@@ -198,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;
}
@@ -251,6 +281,7 @@ namespace Barotrauma
}
availableStock.Add(stockItem.ItemPrefab, weight);
}
DailySpecials.Clear();
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
@@ -261,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;
}
@@ -284,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;
@@ -293,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);
@@ -320,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);
@@ -331,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);
@@ -340,7 +378,7 @@ namespace Barotrauma
if (characters.Any())
{
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag)));
}
// Price should never go below 1 mk
@@ -370,7 +408,7 @@ namespace Barotrauma
}
else
{
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
return MathHelper.Lerp(1.0f, 1.0f + MinReputationModifier, reputation.Value / reputation.MinReputation);
}
}
else
@@ -381,7 +419,7 @@ namespace Barotrauma
}
else
{
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
return MathHelper.Lerp(1.0f, 1.0f - MinReputationModifier, reputation.Value / reputation.MinReputation);
}
}
}
@@ -392,12 +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;
@@ -575,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)
{
@@ -684,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);
@@ -780,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)
@@ -1076,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
@@ -1092,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));
@@ -1177,9 +1240,22 @@ namespace Barotrauma
}
}
private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
public static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
{
if (type?.NameFormats == null || !type.NameFormats.Any())
if (LocationType.Prefabs.TryGet(locationTypeIdentifier, out LocationType locationType))
{
return GetName(locationType, nameFormatIndex, nameId);
}
else
{
DebugConsole.ThrowError($"Could not find the location type {locationTypeIdentifier}.\n" + Environment.StackTrace.CleanUpPath());
return new RawLString(nameId.Value);
}
}
public static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
{
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
{
return TextManager.Get(nameId);
}
@@ -1197,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)
@@ -1229,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
@@ -1305,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)
{
@@ -1358,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>();
@@ -1384,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
{
@@ -1470,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; }
@@ -1522,7 +1617,7 @@ namespace Barotrauma
ChangeType(campaign, OriginalType);
PendingLocationTypeChange = null;
}
CreateStores(force: true);
ClearStores();
ClearMissions();
LevelData?.EventHistory?.Clear();
UnlockInitialMissions();
@@ -1543,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())
{
@@ -1700,3 +1796,4 @@ namespace Barotrauma
}
}
}
@@ -33,6 +33,11 @@ namespace Barotrauma
public readonly CharacterTeamType OutpostTeam;
/// <summary>
/// Is this location type considered valid for e.g. events and missions that are should be available in "any outpost"
/// </summary>
public bool IsAnyOutpost;
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public readonly ImmutableArray<Identifier> MissionIdentifiers;
@@ -86,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>
@@ -113,9 +120,12 @@ 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
@@ -155,11 +165,14 @@ 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);
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
IsAnyOutpost = element.GetAttributeBool(nameof(IsAnyOutpost), def: HasOutpost);
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
@@ -179,7 +192,7 @@ namespace Barotrauma
try
{
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
names.AddRange(File.ReadAllLines(path.Value).ToList());
names.AddRange(File.ReadAllLines(path.Value, catchUnauthorizedAccessExceptions: false).ToList());
}
catch (Exception e)
{
@@ -257,9 +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);
}
}
@@ -449,17 +449,6 @@ namespace Barotrauma
}
LocationType forceLocationType = null;
if (!possibleStartOutpostCreated)
{
float zoneWidth = Width / generationParams.DifficultyZones;
float threshold = zoneWidth * 0.1f;
if (position.X < threshold)
{
LocationType.Prefabs.TryGet("outpost", out forceLocationType);
possibleStartOutpostCreated = true;
}
}
if (forceLocationType == null)
{
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
@@ -604,6 +593,10 @@ namespace Barotrauma
{
connectionsBetweenZones[zone1].Add(connection);
}
if (connectionsBetweenZones[zone1].None())
{
DebugConsole.ThrowError($"Potential error during map generation: no connections between zones {zone1} and {zone2} found. Traversing through to the end of the map may be impossible.");
}
}
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
@@ -673,8 +666,9 @@ namespace Barotrauma
connection.Locations[0] :
connection.Locations[1];
//if there's only one connection (= the connection between biomes), create a new connection to the closest location to the right
if (rightMostLocation.Connections.Count == 1)
//if all of the other connected locations are to the left (= if there's no path forwards from the outpost),
//create a new connection to the closest location to the right
if (rightMostLocation.Connections.All(c => c.OtherLocation(rightMostLocation).MapPosition.X < rightMostLocation.MapPosition.X))
{
Location closestLocation = null;
float closestDist = float.PositiveInfinity;
@@ -714,6 +708,13 @@ namespace Barotrauma
}
}
//ensure there's an outpost (a valid starting location) at the very left side of the map
Location startLocation = Locations.MinBy(l => l.MapPosition.X);
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
{
startLocation.ChangeType(campaign, startLocationType, createStores: false);
}
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
@@ -731,7 +732,6 @@ namespace Barotrauma
location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
}
}
location.CreateStores(force: true);
}
foreach (LocationConnection connection in Connections)
@@ -763,7 +763,7 @@ namespace Barotrauma
partial void GenerateLocationConnectionVisuals(LocationConnection connection);
private int GetZoneIndex(float xPos)
public int GetZoneIndex(float xPos)
{
float zoneWidth = Width / generationParams.DifficultyZones;
return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
@@ -1019,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 })
@@ -1211,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 ||
@@ -1223,7 +1235,7 @@ namespace Barotrauma
}
foreach (LocationConnection connection in Connections)
{
connection.LevelData.EventsExhausted = false;
connection.LevelData.ResetExhaustedEventSets();
}
foreach (Location location in Locations)
@@ -1233,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;
}
}
@@ -1337,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)
{
@@ -1408,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);
@@ -1509,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;
}
@@ -1524,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);
@@ -1537,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);
@@ -1562,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;
}
@@ -1579,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;
@@ -1707,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);
}
@@ -10,23 +10,24 @@ namespace Barotrauma
class MapGenerationParams : Prefab, ISerializableEntity
{
public static readonly PrefabSelector<MapGenerationParams> Params = new PrefabSelector<MapGenerationParams>();
public static MapGenerationParams Instance
{
get
{
return Params.ActivePrefab;
}
get { return Params.ActivePrefab; }
}
#if DEBUG
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowLocations { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool ShowLevelTypeNames { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
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;
@@ -49,8 +50,8 @@ namespace Barotrauma
public float LargeLevelConnectionLength { get; set; }
[Serialize("20,20", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " +
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
public Point VoronoiSiteInterval { get; set; }
[Serialize("5,5", IsPropertySaveable.Yes), Editable]
@@ -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)