Unstable v0.1300.0.0 (February 19th 2021)
This commit is contained in:
@@ -33,8 +33,9 @@ namespace Barotrauma
|
||||
{
|
||||
OriginalContainerID = item.OriginalContainerID;
|
||||
}
|
||||
|
||||
OriginalID = item.ID;
|
||||
ModuleIndex = (ushort)item.OriginalModuleIndex;
|
||||
ModuleIndex = (ushort) item.OriginalModuleIndex;
|
||||
Identifier = item.prefab.Identifier;
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ namespace Barotrauma
|
||||
{
|
||||
return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier;
|
||||
}
|
||||
|
||||
public bool Matches(Item item)
|
||||
{
|
||||
if (item.OriginalContainerID != Entity.NullEntityID)
|
||||
@@ -56,13 +58,17 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
|
||||
|
||||
|
||||
private string baseName;
|
||||
private int nameFormatIndex;
|
||||
|
||||
public bool Discovered;
|
||||
|
||||
public int TypeChangeTimer;
|
||||
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
|
||||
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
|
||||
public int LocationTypeChangeCooldown;
|
||||
|
||||
public readonly int ZoneIndex;
|
||||
|
||||
public string BaseName { get => baseName; }
|
||||
|
||||
@@ -80,14 +86,76 @@ namespace Barotrauma
|
||||
|
||||
public Reputation Reputation { get; set; }
|
||||
|
||||
public int[] ProximityTime { get; private set; }
|
||||
public int TurnsInRadiation { get; set; }
|
||||
|
||||
#region Store
|
||||
|
||||
private const float StoreMaxReputationModifier = 0.1f;
|
||||
private const float StoreSellPriceModifier = 0.8f;
|
||||
private const float MechanicalMaxDiscountPercentage = 50.0f;
|
||||
private const float DailySpecialPriceModifier = 0.9f;
|
||||
private const float RequestGoodPriceModifier = 1.5f;
|
||||
public const int StoreInitialBalance = 5000;
|
||||
public int StoreCurrentBalance { get; set; }
|
||||
/// <summary>
|
||||
/// In percentages
|
||||
/// </summary>
|
||||
private const int StorePriceModifierRange = 5;
|
||||
/// <summary>
|
||||
/// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa.
|
||||
/// </summary>
|
||||
public int StorePriceModifier { get; private set; }
|
||||
|
||||
public Color BalanceColor => ActiveStoreBalanceStatus.Color;
|
||||
public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; }
|
||||
private static StoreBalanceStatus DefaultBalanceStatus { get; } = new StoreBalanceStatus(1.0f, 1.0f, Color.White);
|
||||
private static List<StoreBalanceStatus> StoreBalanceStatuses { get; } = new List<StoreBalanceStatus>
|
||||
{
|
||||
new StoreBalanceStatus(0.5f, 0.75f, Color.Orange),
|
||||
new StoreBalanceStatus(0.25f, 0.2f, Color.Red),
|
||||
};
|
||||
|
||||
public struct StoreBalanceStatus
|
||||
{
|
||||
public float PercentageOfInitialBalance { get; }
|
||||
public float SellPriceModifier { get; }
|
||||
public Color Color { get; }
|
||||
|
||||
public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color)
|
||||
{
|
||||
PercentageOfInitialBalance = percentage;
|
||||
SellPriceModifier = sellPriceModifier;
|
||||
Color = color;
|
||||
}
|
||||
}
|
||||
|
||||
private int storeCurrentBalance;
|
||||
public int StoreCurrentBalance
|
||||
{
|
||||
get
|
||||
{
|
||||
return storeCurrentBalance;
|
||||
}
|
||||
set
|
||||
{
|
||||
storeCurrentBalance = value;
|
||||
ActiveStoreBalanceStatus = GetStoreBalanceStatus(value);
|
||||
}
|
||||
}
|
||||
|
||||
public List<PurchasedItem> StoreStock { get; set; }
|
||||
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
|
||||
public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
|
||||
|
||||
/// <summary>
|
||||
/// How many map progress steps it takes before the discounts should be updated.
|
||||
/// </summary>
|
||||
private const int SpecialsUpdateInterval = 3;
|
||||
private const int DailySpecialsCount = 3;
|
||||
private const int RequestedGoodsCount = 3;
|
||||
private int StepsSinceSpecialsUpdated { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
private const float MechanicalMaxDiscountPercentage = 50.0f;
|
||||
|
||||
private readonly List<TakenItem> takenItems = new List<TakenItem>();
|
||||
public IEnumerable<TakenItem> TakenItems
|
||||
@@ -106,12 +174,11 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
availableMissions.RemoveAll(m => m.Completed || m.Failed);
|
||||
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
|
||||
return availableMissions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Mission SelectedMission
|
||||
{
|
||||
get;
|
||||
@@ -152,6 +219,10 @@ namespace Barotrauma
|
||||
|
||||
public string LastTypeChangeMessage;
|
||||
|
||||
public int TimeSinceLastTypeChange;
|
||||
|
||||
public bool IsGateBetweenBiomes;
|
||||
|
||||
private struct LoadedMission
|
||||
{
|
||||
public MissionPrefab MissionPrefab { get; }
|
||||
@@ -175,29 +246,49 @@ namespace Barotrauma
|
||||
return $"Location ({Name ?? "null"})";
|
||||
}
|
||||
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, IEnumerable<Location> existingLocations = null)
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
Type = LocationType.Random(rand, zone, requireOutpost);
|
||||
Type = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
|
||||
Name = RandomName(Type, rand, existingLocations);
|
||||
MapPosition = mapPosition;
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
Connections = new List<LocationConnection>();
|
||||
ProximityTime = new int[Type.CanChangeTo.Count];
|
||||
Connections = new List<LocationConnection>();
|
||||
}
|
||||
|
||||
public Location(XElement element)
|
||||
{
|
||||
string locationType = element.GetAttributeString("type", "");
|
||||
Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase));
|
||||
bool typeNotFound = false;
|
||||
if (Type == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead.");
|
||||
Type = LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase));
|
||||
Type ??= LocationType.List.First();
|
||||
typeNotFound = true;
|
||||
}
|
||||
|
||||
baseName = element.GetAttributeString("basename", "");
|
||||
Name = element.GetAttributeString("name", "");
|
||||
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
|
||||
TypeChangeTimer = element.GetAttributeInt("changetimer", 0);
|
||||
Discovered = element.GetAttributeBool("discovered", false);
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
|
||||
ProximityTime = element.GetAttributeIntArray("proximitytime", new int[Type.CanChangeTo.Count]);
|
||||
if (ProximityTime.Length != Type.CanChangeTo.Count) { ProximityTime = new int[Type.CanChangeTo.Count]; }
|
||||
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
|
||||
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
|
||||
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
|
||||
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
|
||||
|
||||
if (!typeNotFound)
|
||||
{
|
||||
for (int i = 0; i < Type.CanChangeTo.Count; i++)
|
||||
{
|
||||
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
|
||||
{
|
||||
ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0));
|
||||
}
|
||||
}
|
||||
|
||||
LoadLocationTypeChange(element);
|
||||
}
|
||||
|
||||
string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]);
|
||||
foreach (string takenItem in takenItemStr)
|
||||
@@ -237,16 +328,42 @@ namespace Barotrauma
|
||||
LevelData = new LevelData(element.Element("Level"));
|
||||
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
|
||||
if (element.GetChildElement("store") is XElement storeElement)
|
||||
{
|
||||
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
|
||||
StoreStock = LoadStoreStock(storeElement);
|
||||
}
|
||||
|
||||
LoadStore(element);
|
||||
LoadMissions(element);
|
||||
}
|
||||
|
||||
public void LoadLocationTypeChange(XElement locationElement)
|
||||
{
|
||||
TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
|
||||
LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0);
|
||||
foreach (XElement subElement in locationElement.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString())
|
||||
{
|
||||
case "pendinglocationtypechange":
|
||||
int timer = subElement.GetAttributeInt("timer", 0);
|
||||
if (subElement.Attribute("index") != null)
|
||||
{
|
||||
int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
|
||||
PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
string missionIdentifier = subElement.GetAttributeString("missionidentifier", "");
|
||||
var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (mission == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found.");
|
||||
continue;
|
||||
}
|
||||
PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadMissions(XElement locationElement)
|
||||
{
|
||||
if (locationElement.GetChildElement("missions") is XElement missionsElement)
|
||||
@@ -266,9 +383,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, IEnumerable<Location> existingLocations = null)
|
||||
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
return new Location(position, zone, rand, requireOutpost, existingLocations);
|
||||
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
|
||||
}
|
||||
|
||||
public void ChangeType(LocationType newType)
|
||||
@@ -278,15 +395,34 @@ namespace Barotrauma
|
||||
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
|
||||
|
||||
Type = newType;
|
||||
ProximityTime = new int[Type.CanChangeTo.Count];
|
||||
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
|
||||
if (Type.MissionIdentifiers.Any())
|
||||
{
|
||||
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom());
|
||||
}
|
||||
if (Type.MissionTags.Any())
|
||||
{
|
||||
UnlockMissionByTag(Type.MissionTags.GetRandom());
|
||||
}
|
||||
|
||||
CreateStore(force: true);
|
||||
}
|
||||
|
||||
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
|
||||
var mission = InstantiateMission(missionPrefab, ref connection);
|
||||
var mission = InstantiateMission(missionPrefab, connection);
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void UnlockMission(MissionPrefab missionPrefab)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
|
||||
var mission = InstantiateMission(missionPrefab);
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
@@ -304,8 +440,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
LocationConnection connection = null;
|
||||
var mission = InstantiateMission(missionPrefab, ref connection);
|
||||
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
|
||||
//don't allow duplicate missions in the same connection
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
|
||||
{
|
||||
@@ -332,14 +467,13 @@ namespace Barotrauma
|
||||
var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m));
|
||||
if (unusedMissions.Any())
|
||||
{
|
||||
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this))));
|
||||
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this)));
|
||||
if (!suitableMissions.Any())
|
||||
{
|
||||
suitableMissions = unusedMissions;
|
||||
}
|
||||
LocationConnection connection = null;
|
||||
MissionPrefab missionPrefab = suitableMissions.GetRandom();
|
||||
var mission = InstantiateMission(missionPrefab, ref connection);
|
||||
MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced);
|
||||
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
|
||||
//don't allow duplicate missions in the same connection
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
|
||||
{
|
||||
@@ -360,8 +494,14 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private Mission InstantiateMission(MissionPrefab prefab, ref LocationConnection connection)
|
||||
private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
|
||||
{
|
||||
if (prefab.IsAllowed(this, this))
|
||||
{
|
||||
connection = null;
|
||||
return InstantiateMission(prefab);
|
||||
}
|
||||
|
||||
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
|
||||
if (!suitableConnections.Any())
|
||||
{
|
||||
@@ -371,21 +511,33 @@ namespace Barotrauma
|
||||
connection = ToolBox.SelectWeightedRandom(
|
||||
suitableConnections.ToList(),
|
||||
suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(),
|
||||
Rand.RandSync.Unsynced);
|
||||
Rand.RandSync.Unsynced);
|
||||
|
||||
return InstantiateMission(prefab, connection);
|
||||
}
|
||||
|
||||
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
|
||||
{
|
||||
Location destination = connection.OtherLocation(this);
|
||||
var mission = prefab.Instantiate(new Location[] { this, destination });
|
||||
mission.AdjustLevelData(connection.LevelData);
|
||||
return mission;
|
||||
}
|
||||
|
||||
private Mission InstantiateMission(MissionPrefab prefab)
|
||||
{
|
||||
var mission = prefab.Instantiate(new Location[] { this, this });
|
||||
mission.AdjustLevelData(LevelData);
|
||||
return mission;
|
||||
}
|
||||
|
||||
public void InstantiateLoadedMissions(Map map)
|
||||
{
|
||||
availableMissions.Clear();
|
||||
if (loadedMissions == null || loadedMissions.None()) { return; }
|
||||
foreach (LoadedMission loadedMission in loadedMissions)
|
||||
{
|
||||
Location destination = null;
|
||||
Location destination;
|
||||
if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
|
||||
{
|
||||
destination = map.Locations[loadedMission.DestinationIndex];
|
||||
@@ -410,6 +562,23 @@ namespace Barotrauma
|
||||
SelectedMissionIndex = -1;
|
||||
}
|
||||
|
||||
public bool HasOutpost()
|
||||
{
|
||||
if (!Type.HasOutpost) { return false; }
|
||||
|
||||
return !IsCriticallyRadiated();
|
||||
}
|
||||
|
||||
public bool IsCriticallyRadiated()
|
||||
{
|
||||
if (GameMain.GameSession is { Campaign: { Map: { } map } })
|
||||
{
|
||||
return TurnsInRadiation > map.Radiation.Params.CriticalRadiationThreshold;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
|
||||
@@ -456,6 +625,61 @@ namespace Barotrauma
|
||||
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
|
||||
}
|
||||
|
||||
public void LoadStore(XElement locationElement)
|
||||
{
|
||||
StoreStock?.Clear();
|
||||
DailySpecials.Clear();
|
||||
RequestedGoods.Clear();
|
||||
|
||||
if (locationElement.GetChildElement("store") is XElement storeElement)
|
||||
{
|
||||
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
|
||||
StorePriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
|
||||
|
||||
StoreStock ??= new List<PurchasedItem>();
|
||||
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
|
||||
{
|
||||
var id = stockElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
|
||||
if (prefab == null) { continue; }
|
||||
var qty = stockElement.GetAttributeInt("qty", 0);
|
||||
if (qty < 1) { continue; }
|
||||
StoreStock.Add(new PurchasedItem(prefab, qty));
|
||||
}
|
||||
|
||||
StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0);
|
||||
|
||||
if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement)
|
||||
{
|
||||
var loadedDailySpecials = LoadStoreSpecials(specialsElement);
|
||||
DailySpecials.AddRange(loadedDailySpecials);
|
||||
}
|
||||
|
||||
if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement)
|
||||
{
|
||||
var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
|
||||
RequestedGoods.AddRange(loadedRequestedGoods);
|
||||
}
|
||||
|
||||
static List<ItemPrefab> LoadStoreSpecials(XElement element)
|
||||
{
|
||||
List<ItemPrefab> specials = new List<ItemPrefab>();
|
||||
foreach (var childElement in element.GetChildElements("item"))
|
||||
{
|
||||
var id = childElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = ItemPrefab.Find(null, id);
|
||||
if (prefab == null) { continue; }
|
||||
specials.Add(prefab);
|
||||
}
|
||||
return specials;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRadiated() => GameMain.GameSession is { Campaign: { Map: { Radiation: { Enabled: true } radiation } } } && radiation.Contains(this);
|
||||
|
||||
private List<PurchasedItem> CreateStoreStock()
|
||||
{
|
||||
var stock = new List<PurchasedItem>();
|
||||
@@ -463,31 +687,28 @@ namespace Barotrauma
|
||||
{
|
||||
if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo))
|
||||
{
|
||||
var quantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount :
|
||||
(priceInfo.MaxAvailableAmount > 0 ? Math.Min(priceInfo.MaxAvailableAmount, 5) : 5);
|
||||
int quantity = PriceInfo.DefaultAmount;
|
||||
if (priceInfo.MaxAvailableAmount > 0)
|
||||
{
|
||||
if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount)
|
||||
{
|
||||
quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
quantity = priceInfo.MaxAvailableAmount;
|
||||
}
|
||||
}
|
||||
else if (priceInfo.MinAvailableAmount > 0)
|
||||
{
|
||||
quantity = priceInfo.MinAvailableAmount;
|
||||
}
|
||||
stock.Add(new PurchasedItem(prefab, quantity));
|
||||
}
|
||||
}
|
||||
return stock;
|
||||
}
|
||||
|
||||
public static List<PurchasedItem> LoadStoreStock(XElement storeElement)
|
||||
{
|
||||
var stock = new List<PurchasedItem>();
|
||||
if (storeElement == null) { return stock; }
|
||||
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
|
||||
{
|
||||
var id = stockElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
|
||||
if (prefab == null) { continue; }
|
||||
var qty = stockElement.GetAttributeInt("qty", 0);
|
||||
if (qty < 1) { continue; }
|
||||
stock.Add(new PurchasedItem(prefab, qty));
|
||||
}
|
||||
return stock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
|
||||
/// </summary>
|
||||
@@ -526,48 +747,70 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public int GetAdjustedItemBuyPrice(PriceInfo priceInfo)
|
||||
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
|
||||
/// /// <param name="considerDailySpecials">If false, the price won't be affected by <see cref="DailySpecialPriceModifier"/></param>
|
||||
public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true)
|
||||
{
|
||||
// TODO: Check priceInfo.CanBeBought
|
||||
priceInfo ??= item?.GetPriceInfo(this);
|
||||
if (priceInfo == null) { return 0; }
|
||||
var price = priceInfo.Price;
|
||||
float price = priceInfo.Price;
|
||||
|
||||
// Adjust by random price modifier
|
||||
price = ((100 + StorePriceModifier) / 100.0f) * price;
|
||||
|
||||
// Adjust by daily special status
|
||||
if (considerDailySpecials && DailySpecials.Contains(item))
|
||||
{
|
||||
price = DailySpecialPriceModifier * price;
|
||||
}
|
||||
|
||||
// Adjust by current location reputation
|
||||
if (Reputation.Value > 0.0f)
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
|
||||
price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
|
||||
price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price;
|
||||
}
|
||||
// Item price should never go below 1 mk
|
||||
return Math.Max(price, 1);
|
||||
|
||||
// Price should never go below 1 mk
|
||||
return Math.Max((int)price, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If item.GetPriceInfo() returns null, this will return 0
|
||||
/// </summary>
|
||||
public int GetAdjustedItemBuyPrice(ItemPrefab item) => GetAdjustedItemBuyPrice(item?.GetPriceInfo(this));
|
||||
|
||||
public int GetAdjustedItemSellPrice(PriceInfo priceInfo)
|
||||
/// <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>
|
||||
public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
|
||||
{
|
||||
priceInfo ??= item?.GetPriceInfo(this);
|
||||
if (priceInfo == null) { return 0; }
|
||||
var price = (int)(StoreSellPriceModifier * priceInfo.Price);
|
||||
float price = StoreSellPriceModifier * priceInfo.Price;
|
||||
|
||||
// Adjust by random price modifier
|
||||
price = ((100 - StorePriceModifier) / 100.0f) * price;
|
||||
|
||||
// Adjust by current store balance
|
||||
price = ActiveStoreBalanceStatus.SellPriceModifier * price;
|
||||
|
||||
// Adjust by requested good status
|
||||
if (considerRequestedGoods && RequestedGoods.Contains(item))
|
||||
{
|
||||
price = RequestGoodPriceModifier * price;
|
||||
}
|
||||
|
||||
// Adjust by current location reputation
|
||||
if (Reputation.Value > 0.0f)
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
|
||||
price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
|
||||
price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price;
|
||||
}
|
||||
// Item price should never go below 1 mk
|
||||
return Math.Max(price, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If item.GetPriceInfo() returns null, this will return 0
|
||||
/// </summary>
|
||||
public int GetAdjustedItemSellPrice(ItemPrefab item) => GetAdjustedItemSellPrice(item?.GetPriceInfo(this));
|
||||
// Price should never go below 1 mk
|
||||
return Math.Max((int)price, 1);
|
||||
}
|
||||
|
||||
public int GetAdjustedMechanicalCost(int cost)
|
||||
{
|
||||
@@ -575,12 +818,12 @@ namespace Barotrauma
|
||||
return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If 'force' is true, the stock will be recreated even if it has been created previously already.
|
||||
/// This is used when (at least) when the type of the location changes.
|
||||
/// </summary>
|
||||
/// <param name="force">If true, the store will be recreated if it already exists.</param>
|
||||
public void CreateStore(bool force = false)
|
||||
{
|
||||
// In multiplayer, stores should be created by the server and loaded from save data by clients
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
if (!force && StoreStock != null) { return; }
|
||||
|
||||
if (StoreStock != null)
|
||||
@@ -604,10 +847,16 @@ namespace Barotrauma
|
||||
StoreCurrentBalance = StoreInitialBalance;
|
||||
StoreStock = CreateStoreStock();
|
||||
}
|
||||
|
||||
GenerateRandomPriceModifier();
|
||||
CreateStoreSpecials();
|
||||
}
|
||||
|
||||
public void UpdateStore()
|
||||
{
|
||||
// In multiplayer, stores should be updated by the server and loaded from save data by clients
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
if (StoreStock == null)
|
||||
{
|
||||
CreateStore();
|
||||
@@ -619,6 +868,8 @@ namespace Barotrauma
|
||||
StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
|
||||
}
|
||||
|
||||
GenerateRandomPriceModifier();
|
||||
|
||||
var stock = StoreStock;
|
||||
var stockToRemove = new List<PurchasedItem>();
|
||||
foreach (PurchasedItem item in stock)
|
||||
@@ -642,6 +893,56 @@ namespace Barotrauma
|
||||
}
|
||||
stockToRemove.ForEach(i => stock.Remove(i));
|
||||
StoreStock = stock;
|
||||
|
||||
if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval)
|
||||
{
|
||||
CreateStoreSpecials();
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateRandomPriceModifier()
|
||||
{
|
||||
StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange);
|
||||
}
|
||||
|
||||
private void CreateStoreSpecials()
|
||||
{
|
||||
DailySpecials.Clear();
|
||||
var availableStock = new Dictionary<ItemPrefab, float>();
|
||||
foreach (var stockItem in StoreStock)
|
||||
{
|
||||
if (stockItem.Quantity < 1) { continue; }
|
||||
var weight = 1.0f;
|
||||
var priceInfo = stockItem.ItemPrefab.GetPriceInfo(this);
|
||||
if (priceInfo != null)
|
||||
{
|
||||
if (!priceInfo.CanBeSpecial) { continue; }
|
||||
var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount;
|
||||
weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
|
||||
if (weight < 0.0f) { continue; }
|
||||
}
|
||||
availableStock.Add(stockItem.ItemPrefab, weight);
|
||||
}
|
||||
for (int i = 0; i < DailySpecialsCount; i++)
|
||||
{
|
||||
if (availableStock.None()) { break; }
|
||||
var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
|
||||
if (item == null) { break; }
|
||||
DailySpecials.Add(item);
|
||||
availableStock.Remove(item);
|
||||
}
|
||||
|
||||
RequestedGoods.Clear();
|
||||
for (int i = 0; i < RequestedGoodsCount; i++)
|
||||
{
|
||||
var item = ItemPrefab.Prefabs.GetRandom(p =>
|
||||
p.CanBeSold && !RequestedGoods.Contains(p) &&
|
||||
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial);
|
||||
if (item == null) { break; }
|
||||
RequestedGoods.Add(item);
|
||||
}
|
||||
|
||||
StepsSinceSpecialsUpdated = 0;
|
||||
}
|
||||
|
||||
public void AddToStock(List<SoldItem> items)
|
||||
@@ -686,6 +987,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static StoreBalanceStatus GetStoreBalanceStatus(int balance)
|
||||
{
|
||||
StoreBalanceStatus nextStatus = DefaultBalanceStatus;
|
||||
foreach (var balanceStatus in StoreBalanceStatuses)
|
||||
{
|
||||
if (balanceStatus.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance &&
|
||||
((float)balance / StoreInitialBalance) < balanceStatus.PercentageOfInitialBalance)
|
||||
{
|
||||
nextStatus = balanceStatus;
|
||||
}
|
||||
}
|
||||
return nextStatus;
|
||||
}
|
||||
|
||||
public XElement Save(Map map, XElement parentElement)
|
||||
{
|
||||
var locationElement = new XElement("location",
|
||||
@@ -695,17 +1010,42 @@ namespace Barotrauma
|
||||
new XAttribute("discovered", Discovered),
|
||||
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
|
||||
new XAttribute("pricemultiplier", PriceMultiplier),
|
||||
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier));
|
||||
if (ProximityTime.Length > 0 && ProximityTime.Any(t => t > 0))
|
||||
{
|
||||
locationElement.Add(new XAttribute("proximitytime", string.Join(',', ProximityTime.Select(i => i.ToString()))));
|
||||
}
|
||||
new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
|
||||
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
|
||||
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
|
||||
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation));
|
||||
LevelData.Save(locationElement);
|
||||
|
||||
if (TypeChangeTimer > 0)
|
||||
for (int i = 0; i < Type.CanChangeTo.Count; i++)
|
||||
{
|
||||
locationElement.Add(new XAttribute("changetimer", TypeChangeTimer));
|
||||
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
|
||||
{
|
||||
if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j]))
|
||||
{
|
||||
locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PendingLocationTypeChange.HasValue)
|
||||
{
|
||||
var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay));
|
||||
if (PendingLocationTypeChange.Value.parentMission != null)
|
||||
{
|
||||
changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
|
||||
}
|
||||
else
|
||||
{
|
||||
changeElement.Add(new XAttribute("index", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange)));
|
||||
}
|
||||
locationElement.Add(changeElement);
|
||||
}
|
||||
|
||||
if (LocationTypeChangeCooldown > 0)
|
||||
{
|
||||
locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown));
|
||||
}
|
||||
|
||||
if (takenItems.Any())
|
||||
{
|
||||
locationElement.Add(new XAttribute(
|
||||
@@ -719,7 +1059,11 @@ namespace Barotrauma
|
||||
|
||||
if (StoreStock != null)
|
||||
{
|
||||
var storeElement = new XElement("store", new XAttribute("balance", StoreCurrentBalance));
|
||||
var storeElement = new XElement("store",
|
||||
new XAttribute("balance", StoreCurrentBalance),
|
||||
new XAttribute("pricemodifier", StorePriceModifier),
|
||||
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
|
||||
|
||||
foreach (PurchasedItem item in StoreStock)
|
||||
{
|
||||
if (item?.ItemPrefab == null) { continue; }
|
||||
@@ -727,6 +1071,29 @@ namespace Barotrauma
|
||||
new XAttribute("id", item.ItemPrefab.Identifier),
|
||||
new XAttribute("qty", item.Quantity)));
|
||||
}
|
||||
|
||||
if (DailySpecials.Any())
|
||||
{
|
||||
var dailySpecialElement = new XElement("dailyspecials");
|
||||
foreach (var item in DailySpecials)
|
||||
{
|
||||
dailySpecialElement.Add(new XElement("item",
|
||||
new XAttribute("id", item.Identifier)));
|
||||
}
|
||||
storeElement.Add(dailySpecialElement);
|
||||
}
|
||||
|
||||
if (RequestedGoods.Any())
|
||||
{
|
||||
var requestedGoodsElement = new XElement("requestedgoods");
|
||||
foreach (var item in RequestedGoods)
|
||||
{
|
||||
requestedGoodsElement.Add(new XElement("item",
|
||||
new XAttribute("id", item.Identifier)));
|
||||
}
|
||||
storeElement.Add(requestedGoodsElement);
|
||||
}
|
||||
|
||||
locationElement.Add(storeElement);
|
||||
}
|
||||
|
||||
@@ -735,7 +1102,7 @@ namespace Barotrauma
|
||||
var missionsElement = new XElement("missions");
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
var location = mission.Locations.FirstOrDefault(l => l != this);
|
||||
var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
|
||||
var i = map.Locations.IndexOf(location);
|
||||
missionsElement.Add(new XElement("mission",
|
||||
new XAttribute("prefabid", mission.Prefab.Identifier),
|
||||
@@ -750,40 +1117,6 @@ namespace Barotrauma
|
||||
return locationElement;
|
||||
}
|
||||
|
||||
public int Distance(Location other, int maxRecursionDepth, int currRecursionDepth = 0)
|
||||
{
|
||||
if (currRecursionDepth >= maxRecursionDepth) { return -1; }
|
||||
if (other == this) { return 0; }
|
||||
int minDist = -1;
|
||||
foreach (Location connected in Connections.Select(c => c.Locations.First(l => l != this)))
|
||||
{
|
||||
int dist = connected.Distance(other, maxRecursionDepth, currRecursionDepth+1);
|
||||
if (dist >= 0)
|
||||
{
|
||||
if (minDist < 0 || dist < minDist) { minDist = dist; }
|
||||
}
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
public void DetermineProximityTime(Location currentLocation)
|
||||
{
|
||||
int dist = Distance(currentLocation, Type.CanChangeTo.Select(cct => cct.RequiredProximityForProbabilityIncrease).Max());
|
||||
for (int i=0;i<ProximityTime.Length;i++)
|
||||
{
|
||||
if (dist <= Type.CanChangeTo[i].RequiredProximityForProbabilityIncrease)
|
||||
{
|
||||
ProximityTime[i]++;
|
||||
if (ProximityTime[i] > 5) { ProximityTime[i] = 5; }
|
||||
}
|
||||
else
|
||||
{
|
||||
ProximityTime[i]--;
|
||||
if (ProximityTime[i] < 0) { ProximityTime[i] = 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -31,8 +32,31 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private readonly List<Mission> availableMissions = new List<Mission>();
|
||||
public IEnumerable<Mission> AvailableMissions
|
||||
{
|
||||
get
|
||||
{
|
||||
availableMissions.RemoveAll(m => m.Completed || (m.Failed && m.Prefab.AllowRetry));
|
||||
return availableMissions;
|
||||
}
|
||||
}
|
||||
|
||||
public LocationConnection(Location location1, Location location2)
|
||||
{
|
||||
if (location1 == null)
|
||||
{
|
||||
throw new ArgumentException("Invalid location connection: location1 was null");
|
||||
}
|
||||
if (location2 == null)
|
||||
{
|
||||
throw new ArgumentException("Invalid location connection: location2 was null");
|
||||
}
|
||||
if (location1 == location2)
|
||||
{
|
||||
throw new ArgumentException("Invalid location connection: location1 was the same as location2");
|
||||
}
|
||||
|
||||
Locations = new Location[] { location1, location2 };
|
||||
Length = Vector2.Distance(location1.MapPosition, location2.MapPosition);
|
||||
}
|
||||
|
||||
@@ -13,17 +13,12 @@ namespace Barotrauma
|
||||
class LocationType
|
||||
{
|
||||
public static readonly List<LocationType> List = new List<LocationType>();
|
||||
|
||||
private readonly List<string> nameFormats;
|
||||
private readonly List<string> names;
|
||||
|
||||
private readonly Sprite symbolSprite;
|
||||
|
||||
private readonly List<Sprite> portraits = new List<Sprite>();
|
||||
|
||||
//<name, commonness>
|
||||
private List<Tuple<JobPrefab, float>> hireableJobs;
|
||||
private float totalHireableWeight;
|
||||
private readonly List<Tuple<JobPrefab, float>> hireableJobs;
|
||||
private readonly float totalHireableWeight;
|
||||
|
||||
public Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
|
||||
|
||||
@@ -34,16 +29,20 @@ namespace Barotrauma
|
||||
|
||||
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
|
||||
|
||||
public readonly List<string> MissionIdentifiers = new List<string>();
|
||||
public readonly List<string> MissionTags = new List<string>();
|
||||
|
||||
public readonly List<string> HideEntitySubcategories = new List<string>();
|
||||
|
||||
public bool IsEnterable { get; private set; }
|
||||
|
||||
public bool UseInMainMenu
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<string> NameFormats
|
||||
{
|
||||
get { return nameFormats; }
|
||||
}
|
||||
|
||||
public List<string> NameFormats { get; private set; }
|
||||
|
||||
public bool HasHireableCharacters
|
||||
{
|
||||
@@ -56,10 +55,7 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
public Sprite Sprite
|
||||
{
|
||||
get { return symbolSprite; }
|
||||
}
|
||||
public Sprite Sprite { get; private set; }
|
||||
|
||||
public Color SpriteColor
|
||||
{
|
||||
@@ -79,9 +75,15 @@ namespace Barotrauma
|
||||
|
||||
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
|
||||
|
||||
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
|
||||
NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
|
||||
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
|
||||
HasOutpost = element.GetAttributeBool("hasoutpost", true);
|
||||
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
|
||||
|
||||
MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList();
|
||||
MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList();
|
||||
|
||||
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList();
|
||||
|
||||
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
|
||||
try
|
||||
@@ -135,11 +137,11 @@ namespace Barotrauma
|
||||
hireableJobs.Add(hireableJob);
|
||||
break;
|
||||
case "symbol":
|
||||
symbolSprite = new Sprite(subElement, lazyLoad: true);
|
||||
Sprite = new Sprite(subElement, lazyLoad: true);
|
||||
SpriteColor = subElement.GetAttributeColor("color", Color.White);
|
||||
break;
|
||||
case "changeto":
|
||||
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement));
|
||||
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement, requireChangeMessages: true));
|
||||
break;
|
||||
case "portrait":
|
||||
var portrait = new Sprite(subElement, lazyLoad: true);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -6,41 +8,268 @@ namespace Barotrauma
|
||||
{
|
||||
class LocationTypeChange
|
||||
{
|
||||
public class Requirement
|
||||
{
|
||||
public enum FunctionType
|
||||
{
|
||||
Add,
|
||||
Multiply
|
||||
}
|
||||
|
||||
public readonly FunctionType Function;
|
||||
|
||||
/// <summary>
|
||||
/// The change can only happen if there's at least one of the given types of locations near this one
|
||||
/// </summary>
|
||||
public readonly List<string> RequiredLocations;
|
||||
|
||||
/// <summary>
|
||||
/// How close the location needs to be to one of the RequiredLocations for the change to occur
|
||||
/// </summary>
|
||||
public readonly int RequiredProximity;
|
||||
|
||||
/// <summary>
|
||||
/// Base probability per turn for the location to change if near one of the RequiredLocations
|
||||
/// </summary>
|
||||
public readonly float Probability;
|
||||
|
||||
/// <summary>
|
||||
/// How close the location needs to be to one of the RequiredLocations for the probability to increase
|
||||
/// </summary>
|
||||
public readonly int RequiredProximityForProbabilityIncrease;
|
||||
|
||||
/// <summary>
|
||||
/// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations
|
||||
/// </summary>
|
||||
public readonly float ProximityProbabilityIncrease;
|
||||
|
||||
/// <summary>
|
||||
/// Does there need to be a beacon station within RequiredProximity
|
||||
/// </summary>
|
||||
public readonly bool RequireBeaconStation;
|
||||
|
||||
/// <summary>
|
||||
/// Does there need to be hunting grounds within RequiredProximity
|
||||
/// </summary>
|
||||
public readonly bool RequireHuntingGrounds;
|
||||
|
||||
public Requirement(XElement element, LocationTypeChange change)
|
||||
{
|
||||
RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList();
|
||||
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1);
|
||||
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
|
||||
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
|
||||
RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false);
|
||||
RequireHuntingGrounds = element.GetAttributeBool("requirehuntinggrounds", false);
|
||||
|
||||
string functionStr = element.GetAttributeString("function", "Add");
|
||||
if (!Enum.TryParse(functionStr, ignoreCase: true, out Function))
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Invalid location type change in location type \"{change.CurrentType}\". " +
|
||||
$"\"{functionStr}\" is not a valid function.");
|
||||
}
|
||||
|
||||
Probability = element.GetAttributeFloat("probability", 1.0f);
|
||||
|
||||
if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f)
|
||||
{
|
||||
if (!RequiredLocations.Any() && !RequireBeaconStation && !RequireHuntingGrounds)
|
||||
{
|
||||
DebugConsole.AddWarning(
|
||||
$"Invalid location type change in location type \"{change.CurrentType}\". " +
|
||||
"Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set.");
|
||||
}
|
||||
if (Probability >= 1.0f)
|
||||
{
|
||||
DebugConsole.AddWarning(
|
||||
$"Invalid location type change in location type \"{change.CurrentType}\". " +
|
||||
"Probability is configured to increase when near some other type of location, but the base probability is already 100%");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesLocation(Location location)
|
||||
{
|
||||
return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated();
|
||||
}
|
||||
|
||||
public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet<Location> checkedLocations = null)
|
||||
{
|
||||
if (currentDistance > maxDistance) { return false; }
|
||||
if (currentDistance > 0 && MatchesLocation(location)) { return true; }
|
||||
|
||||
checkedLocations ??= new HashSet<Location>();
|
||||
checkedLocations.Add(location);
|
||||
|
||||
foreach (var connection in location.Connections)
|
||||
{
|
||||
if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var otherLocation = connection.OtherLocation(location);
|
||||
if (!checkedLocations.Contains(otherLocation))
|
||||
{
|
||||
if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly string CurrentType;
|
||||
|
||||
public readonly string ChangeToType;
|
||||
|
||||
/// <summary>
|
||||
/// Base probability per turn for the location to change if near one of the RequiredLocations
|
||||
/// </summary>
|
||||
public readonly float Probability;
|
||||
public readonly int RequiredDuration;
|
||||
|
||||
public readonly float ProximityProbabilityIncrease;
|
||||
public readonly int RequiredProximityForProbabilityIncrease;
|
||||
public readonly bool RequireDiscovered;
|
||||
|
||||
public List<Requirement> Requirements = new List<Requirement>();
|
||||
|
||||
public List<string> Messages = new List<string>();
|
||||
|
||||
//the change can't happen if there's a location of the given type next to this one
|
||||
/// <summary>
|
||||
/// The change can't happen if there's one or more of the given types of locations near this one
|
||||
/// </summary>
|
||||
public readonly List<string> DisallowedAdjacentLocations;
|
||||
|
||||
//the change can only happen if there's at least one of the given types of locations next to this one
|
||||
public readonly List<string> RequiredAdjacentLocations;
|
||||
/// <summary>
|
||||
/// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled
|
||||
/// </summary>
|
||||
public readonly int DisallowedProximity;
|
||||
|
||||
public LocationTypeChange(string currentType, XElement element)
|
||||
/// <summary>
|
||||
/// The location can't change it's type for this many turns after this location type changes occurs
|
||||
/// </summary>
|
||||
public readonly int CooldownAfterChange;
|
||||
|
||||
public readonly Point RequiredDurationRange;
|
||||
|
||||
public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f)
|
||||
{
|
||||
ChangeToType = element.GetAttributeString("type", "");
|
||||
Probability = element.GetAttributeFloat("probability", 1.0f);
|
||||
RequiredDuration = element.GetAttributeInt("requiredduration", 0);
|
||||
CurrentType = currentType;
|
||||
ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", ""));
|
||||
|
||||
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
|
||||
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", 0);
|
||||
RequireDiscovered = element.GetAttributeBool("requirediscovered", false);
|
||||
|
||||
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
|
||||
RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList();
|
||||
DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1);
|
||||
|
||||
RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero);
|
||||
|
||||
Probability = element.GetAttributeFloat("probability", defaultProbability);
|
||||
|
||||
CooldownAfterChange = Math.Max(element.GetAttributeInt("cooldownafterchange", 0), 0);
|
||||
|
||||
//backwards compatibility
|
||||
if (element.Attribute("requiredlocations") != null)
|
||||
{
|
||||
Requirements.Add(new Requirement(element, this));
|
||||
}
|
||||
|
||||
//backwards compatibility
|
||||
if (element.Attribute("requiredduration") != null)
|
||||
{
|
||||
RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0));
|
||||
}
|
||||
|
||||
string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
|
||||
|
||||
Messages = TextManager.GetAll(messageTag);
|
||||
if (Messages == null)
|
||||
{
|
||||
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
|
||||
if (requireChangeMessages)
|
||||
{
|
||||
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
|
||||
}
|
||||
Messages = new List<string>();
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Requirements.Add(new Requirement(subElement, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float DetermineProbability(Location location)
|
||||
{
|
||||
if (RequireDiscovered && !location.Discovered) { return 0.0f; }
|
||||
if (location.IsCriticallyRadiated()) { return 0.0f; }
|
||||
if (location.LocationTypeChangeCooldown > 0) { return 0.0f; }
|
||||
if (location.IsGateBetweenBiomes) { return 0.0f; }
|
||||
|
||||
if (DisallowedAdjacentLocations.Any() &&
|
||||
AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float probability = Probability;
|
||||
foreach (Requirement requirement in Requirements)
|
||||
{
|
||||
if (requirement.AnyWithinDistance(location, requirement.RequiredProximity))
|
||||
{
|
||||
if (requirement.Function == Requirement.FunctionType.Add)
|
||||
{
|
||||
probability += requirement.Probability;
|
||||
}
|
||||
else
|
||||
{
|
||||
probability *= requirement.Probability;
|
||||
}
|
||||
}
|
||||
|
||||
if (location.ProximityTimer.ContainsKey(requirement))
|
||||
{
|
||||
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
|
||||
{
|
||||
if (requirement.Function == Requirement.FunctionType.Add)
|
||||
{
|
||||
probability += requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement];
|
||||
}
|
||||
else
|
||||
{
|
||||
probability *= requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return probability;
|
||||
}
|
||||
|
||||
private bool AnyWithinDistance(Location location, int maxDistance, Func<Location, bool> predicate, int currentDistance = 0, HashSet<Location> checkedLocations = null)
|
||||
{
|
||||
if (currentDistance > maxDistance) { return false; }
|
||||
if (currentDistance > 0 && predicate(location)) { return true; }
|
||||
|
||||
checkedLocations ??= new HashSet<Location>();
|
||||
checkedLocations.Add(location);
|
||||
|
||||
foreach (var connection in location.Connections)
|
||||
{
|
||||
var otherLocation = connection.OtherLocation(location);
|
||||
if (!checkedLocations.Contains(otherLocation))
|
||||
{
|
||||
if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -15,8 +16,8 @@ namespace Barotrauma
|
||||
|
||||
private Location furthestDiscoveredLocation;
|
||||
|
||||
private int Width => generationParams.Width;
|
||||
private int Height => generationParams.Height;
|
||||
public int Width => generationParams.Width;
|
||||
public int Height => generationParams.Height;
|
||||
|
||||
public Action<Location, LocationConnection> OnLocationSelected;
|
||||
/// <summary>
|
||||
@@ -56,11 +57,14 @@ namespace Barotrauma
|
||||
|
||||
public List<LocationConnection> Connections { get; private set; }
|
||||
|
||||
public Radiation Radiation;
|
||||
|
||||
public Map()
|
||||
{
|
||||
generationParams = MapGenerationParams.Instance;
|
||||
Locations = new List<Location>();
|
||||
Connections = new List<LocationConnection>();
|
||||
Radiation = new Radiation(this, generationParams.RadiationParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,6 +73,7 @@ namespace Barotrauma
|
||||
private Map(CampaignMode campaign, XElement element) : this()
|
||||
{
|
||||
Seed = element.GetAttributeString("seed", "a");
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -80,11 +85,17 @@ namespace Barotrauma
|
||||
Locations.Add(null);
|
||||
}
|
||||
Locations[i] = new Location(subElement);
|
||||
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
|
||||
break;
|
||||
case "radiation":
|
||||
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
|
||||
for (int i = 0; i < Locations.Count; i++)
|
||||
{
|
||||
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
@@ -92,6 +103,7 @@ namespace Barotrauma
|
||||
{
|
||||
case "connection":
|
||||
Point locationIndices = subElement.GetAttributePoint("locations", new Point(0, 1));
|
||||
if (locationIndices.X == locationIndices.Y) { continue; }
|
||||
var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
|
||||
{
|
||||
Passed = subElement.GetAttributeBool("passed", false),
|
||||
@@ -181,8 +193,8 @@ namespace Barotrauma
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
|
||||
|
||||
CurrentLocation.CreateStore();
|
||||
CurrentLocation.Discovered = true;
|
||||
CurrentLocation.CreateStore();
|
||||
|
||||
InitProjectSpecific();
|
||||
}
|
||||
@@ -243,21 +255,21 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (newLocations[i] != null) continue;
|
||||
if (newLocations[i] != null) { continue; }
|
||||
|
||||
Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
|
||||
|
||||
int positionIndex = Rand.Int(1, Rand.RandSync.Server);
|
||||
|
||||
Vector2 position = points[positionIndex];
|
||||
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) position = points[1 - positionIndex];
|
||||
int zone = MathHelper.Clamp((int)Math.Floor(position.X / zoneWidth) + 1, 1, generationParams.DifficultyZones);
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, Locations);
|
||||
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
|
||||
int zone = GetZoneIndex(position.X);
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations);
|
||||
Locations.Add(newLocations[i]);
|
||||
}
|
||||
|
||||
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
|
||||
Connections.Add(newConnection);
|
||||
Connections.Add(newConnection);
|
||||
}
|
||||
|
||||
//remove connections that are too short
|
||||
@@ -316,7 +328,15 @@ namespace Barotrauma
|
||||
{
|
||||
connection.Locations[1] = Locations[i];
|
||||
}
|
||||
Locations[i].Connections.Add(connection);
|
||||
|
||||
if (connection.Locations[0] != connection.Locations[1])
|
||||
{
|
||||
Locations[i].Connections.Add(connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
Connections.Remove(connection);
|
||||
}
|
||||
}
|
||||
Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]);
|
||||
Locations.RemoveAt(j);
|
||||
@@ -337,6 +357,56 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
LocationConnection[] connectionsBetweenZones = new LocationConnection[generationParams.DifficultyZones];
|
||||
foreach (var connection in Connections)
|
||||
{
|
||||
int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
|
||||
int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X);
|
||||
if (zone1 == zone2) { continue; }
|
||||
if (zone1 > zone2)
|
||||
{
|
||||
int temp = zone2;
|
||||
zone2 = zone1;
|
||||
zone1 = temp;
|
||||
}
|
||||
|
||||
if (connectionsBetweenZones[zone1] == null)
|
||||
{
|
||||
connectionsBetweenZones[zone1] = connection;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].CenterPos.Y - Height / 2))
|
||||
{
|
||||
connectionsBetweenZones[zone1] = connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = Connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X);
|
||||
int zone2 = GetZoneIndex(Connections[i].Locations[1].MapPosition.X);
|
||||
if (zone1 == zone2) { continue; }
|
||||
|
||||
if (!connectionsBetweenZones.Contains(Connections[i]))
|
||||
{
|
||||
Connections.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
var leftMostLocation =
|
||||
Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
|
||||
Connections[i].Locations[0] :
|
||||
Connections[i].Locations[1];
|
||||
if (!leftMostLocation.Type.HasOutpost)
|
||||
{
|
||||
leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost));
|
||||
}
|
||||
leftMostLocation.IsGateBetweenBiomes = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
for (int i = location.Connections.Count - 1; i >= 0; i--)
|
||||
@@ -359,6 +429,14 @@ namespace Barotrauma
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location);
|
||||
if (location.Type.MissionIdentifiers.Any())
|
||||
{
|
||||
location.UnlockMissionByIdentifier(location.Type.MissionIdentifiers.GetRandom());
|
||||
}
|
||||
if (location.Type.MissionTags.Any())
|
||||
{
|
||||
location.UnlockMissionByTag(location.Type.MissionTags.GetRandom());
|
||||
}
|
||||
}
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
@@ -368,6 +446,12 @@ namespace Barotrauma
|
||||
|
||||
partial void GenerateLocationConnectionVisuals();
|
||||
|
||||
private int GetZoneIndex(float xPos)
|
||||
{
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
|
||||
}
|
||||
|
||||
public Biome GetBiome(Vector2 mapPos)
|
||||
{
|
||||
return GetBiome(mapPos.X);
|
||||
@@ -544,6 +628,12 @@ namespace Barotrauma
|
||||
|
||||
CurrentLocation.CreateStore();
|
||||
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
|
||||
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.CampaignMetadata is { } metadata)
|
||||
{
|
||||
metadata.SetValue("campaign.location.id", CurrentLocationIndex);
|
||||
metadata.SetValue("campaign.location.name", CurrentLocation.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLocation(int index)
|
||||
@@ -618,7 +708,6 @@ namespace Barotrauma
|
||||
|
||||
public void SelectMission(int missionIndex)
|
||||
{
|
||||
if (SelectedConnection == null) { return; }
|
||||
if (CurrentLocation == null)
|
||||
{
|
||||
string errorMsg = "Failed to select a mission (current location not set).";
|
||||
@@ -628,11 +717,18 @@ namespace Barotrauma
|
||||
}
|
||||
CurrentLocation.SelectedMissionIndex = missionIndex;
|
||||
|
||||
//the destination must be the same as the destination of the mission
|
||||
if (CurrentLocation.SelectedMission != null &&
|
||||
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
|
||||
if (CurrentLocation.SelectedMission == null) { return; }
|
||||
|
||||
if (CurrentLocation.SelectedMission.Locations[0] != CurrentLocation ||
|
||||
CurrentLocation.SelectedMission.Locations[1] != CurrentLocation)
|
||||
{
|
||||
CurrentLocation.SelectedMissionIndex = -1;
|
||||
if (SelectedConnection == null) { return; }
|
||||
//the destination must be the same as the destination of the mission
|
||||
if (CurrentLocation.SelectedMission != null &&
|
||||
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
|
||||
{
|
||||
CurrentLocation.SelectedMissionIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission);
|
||||
@@ -668,89 +764,116 @@ namespace Barotrauma
|
||||
{
|
||||
ProgressWorld();
|
||||
}
|
||||
|
||||
Radiation.OnStep(steps);
|
||||
}
|
||||
|
||||
private void ProgressWorld()
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Discovered) { continue; }
|
||||
|
||||
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
|
||||
if (location.Discovered)
|
||||
{
|
||||
furthestDiscoveredLocation = location;
|
||||
if (furthestDiscoveredLocation == null ||
|
||||
location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
|
||||
{
|
||||
furthestDiscoveredLocation = location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (location == CurrentLocation || location == SelectedLocation) { continue; }
|
||||
|
||||
//find which types of locations this one can change to
|
||||
var cct = location.Type.CanChangeTo;
|
||||
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
|
||||
List<int> readyTypeChanges = new List<int>();
|
||||
for (int i = 0; i < cct.Count; i++)
|
||||
ProgressLocationTypeChanges(location);
|
||||
|
||||
if (location.Discovered)
|
||||
{
|
||||
LocationTypeChange typeChange = cct[i];
|
||||
//check if there are any adjacent locations that would prevent the change
|
||||
bool disallowedFound = false;
|
||||
foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations)
|
||||
{
|
||||
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
disallowedFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (disallowedFound) { continue; }
|
||||
|
||||
//check that there's a required adjacent location present
|
||||
bool requiredFound = false;
|
||||
foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations)
|
||||
{
|
||||
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
requiredFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) { continue; }
|
||||
|
||||
allowedTypeChanges.Add(typeChange);
|
||||
|
||||
if (location.TypeChangeTimer >= typeChange.RequiredDuration)
|
||||
{
|
||||
readyTypeChanges.Add(i);
|
||||
}
|
||||
location.UpdateStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//select a random type change
|
||||
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i])))
|
||||
private void ProgressLocationTypeChanges(Location location)
|
||||
{
|
||||
location.TimeSinceLastTypeChange++;
|
||||
location.LocationTypeChangeCooldown--;
|
||||
|
||||
if (location.PendingLocationTypeChange != null)
|
||||
{
|
||||
if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
|
||||
{
|
||||
var selectedTypeChangeIndex =
|
||||
ToolBox.SelectWeightedRandom(
|
||||
readyTypeChanges,
|
||||
readyTypeChanges.Select(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i])).ToList(),
|
||||
Rand.RandSync.Unsynced);
|
||||
var selectedTypeChange = cct[selectedTypeChangeIndex];
|
||||
if (selectedTypeChange != null)
|
||||
{
|
||||
string prevName = location.Name;
|
||||
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase)));
|
||||
ChangeLocationType(location, prevName, selectedTypeChange);
|
||||
location.TypeChangeTimer = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowedTypeChanges.Count > 0)
|
||||
{
|
||||
location.TypeChangeTimer++;
|
||||
//remove pending type change if it's no longer allowed
|
||||
location.PendingLocationTypeChange = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
location.TypeChangeTimer = 0;
|
||||
location.PendingLocationTypeChange =
|
||||
(location.PendingLocationTypeChange.Value.typeChange,
|
||||
location.PendingLocationTypeChange.Value.delay - 1,
|
||||
location.PendingLocationTypeChange.Value.parentMission);
|
||||
if (location.PendingLocationTypeChange.Value.delay <= 0)
|
||||
{
|
||||
ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
location.UpdateStore();
|
||||
//find which types of locations this one can change to
|
||||
Dictionary<LocationTypeChange, float> allowedTypeChanges = new Dictionary<LocationTypeChange, float>();
|
||||
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
|
||||
{
|
||||
float probability = typeChange.DetermineProbability(location);
|
||||
if (probability <= 0.0f) { continue; }
|
||||
allowedTypeChanges.Add(typeChange, probability);
|
||||
}
|
||||
|
||||
//select a random type change
|
||||
if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
|
||||
{
|
||||
var selectedTypeChange =
|
||||
ToolBox.SelectWeightedRandom(
|
||||
allowedTypeChanges.Keys.ToList(),
|
||||
allowedTypeChanges.Values.ToList(),
|
||||
Rand.RandSync.Unsynced);
|
||||
if (selectedTypeChange != null)
|
||||
{
|
||||
if (selectedTypeChange.RequiredDurationRange.X > 0)
|
||||
{
|
||||
location.PendingLocationTypeChange =
|
||||
(selectedTypeChange,
|
||||
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
|
||||
null);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeLocationType(location, selectedTypeChange);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
|
||||
{
|
||||
foreach (var requirement in typeChange.Requirements)
|
||||
{
|
||||
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
|
||||
{
|
||||
if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
|
||||
location.ProximityTimer[requirement] += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
location.ProximityTimer.Remove(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,7 +922,22 @@ namespace Barotrauma
|
||||
return distance;
|
||||
}
|
||||
|
||||
partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change);
|
||||
private void ChangeLocationType(Location location, LocationTypeChange change)
|
||||
{
|
||||
string prevName = location.Name;
|
||||
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)));
|
||||
ChangeLocationTypeProjSpecific(location, prevName, change);
|
||||
foreach (var requirement in change.Requirements)
|
||||
{
|
||||
location.ProximityTimer.Remove(requirement);
|
||||
}
|
||||
location.TimeSinceLastTypeChange = 0;
|
||||
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
|
||||
location.PendingLocationTypeChange = null;
|
||||
}
|
||||
|
||||
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
|
||||
|
||||
partial void ClearAnimQueue();
|
||||
|
||||
/// <summary>
|
||||
@@ -835,8 +973,15 @@ namespace Barotrauma
|
||||
{
|
||||
case "location":
|
||||
Location location = Locations[subElement.GetAttributeInt("i", 0)];
|
||||
|
||||
location.TypeChangeTimer = subElement.GetAttributeInt("changetimer", 0);
|
||||
location.ProximityTimer.Clear();
|
||||
for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
|
||||
{
|
||||
for (int j = 0; j < location.Type.CanChangeTo[i].Requirements.Count; j++)
|
||||
{
|
||||
location.ProximityTimer.Add(location.Type.CanChangeTo[i].Requirements[j], subElement.GetAttributeInt("changetimer" + i + "-" + j, 0));
|
||||
}
|
||||
}
|
||||
location.LoadLocationTypeChange(subElement);
|
||||
location.Discovered = subElement.GetAttributeBool("discovered", false);
|
||||
if (location.Discovered)
|
||||
{
|
||||
@@ -849,7 +994,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string locationType = subElement.GetAttributeString("type", "");
|
||||
string prevLocationName = location.Name;
|
||||
LocationType prevLocationType = location.Type;
|
||||
@@ -860,15 +1004,22 @@ namespace Barotrauma
|
||||
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (change != null)
|
||||
{
|
||||
ChangeLocationType(location, prevLocationName, change);
|
||||
ChangeLocationTypeProjSpecific(location, prevLocationName, change);
|
||||
location.TimeSinceLastTypeChange = 0;
|
||||
}
|
||||
}
|
||||
|
||||
location.LoadStore(subElement);
|
||||
location.LoadMissions(subElement);
|
||||
|
||||
break;
|
||||
case "connection":
|
||||
int connectionIndex = subElement.GetAttributeInt("i", 0);
|
||||
Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
|
||||
break;
|
||||
case "radiation":
|
||||
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,6 +1086,8 @@ namespace Barotrauma
|
||||
mapElement.Add(connectionElement);
|
||||
}
|
||||
|
||||
mapElement.Add(Radiation.Save());
|
||||
|
||||
element.Add(mapElement);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,8 @@ namespace Barotrauma
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public RadiationParams RadiationParams;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
|
||||
@@ -238,6 +240,9 @@ namespace Barotrauma
|
||||
TypeChangeIcon = new Sprite(subElement);
|
||||
break;
|
||||
#endif
|
||||
case "radiationparams":
|
||||
RadiationParams = new RadiationParams(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal partial class Radiation : ISerializableEntity
|
||||
{
|
||||
public string Name => nameof(Radiation);
|
||||
|
||||
[Serialize(defaultValue: 0f, isSaveable: true)]
|
||||
public float Amount { get; set; }
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
|
||||
|
||||
public readonly Map Map;
|
||||
public readonly RadiationParams Params;
|
||||
|
||||
private float radiationTimer;
|
||||
|
||||
private float increasedAmount;
|
||||
private float lastIncrease;
|
||||
|
||||
public bool Enabled = true;
|
||||
|
||||
public Radiation(Map map, RadiationParams radiationParams, XElement? element = null)
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
Map = map;
|
||||
Params = radiationParams;
|
||||
radiationTimer = Params.RadiationDamageDelay;
|
||||
if (element == null)
|
||||
{
|
||||
Amount = Params.StartingRadiation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the progress of the radiation.
|
||||
/// </summary>
|
||||
/// <param name="steps"></param>
|
||||
public void OnStep(float steps = 1)
|
||||
{
|
||||
if (!Enabled) { return; }
|
||||
if (steps <= 0) { return; }
|
||||
|
||||
IncreaseRadiation(Params.RadiationStep * steps);
|
||||
|
||||
int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated());
|
||||
|
||||
foreach (Location location in Map.Locations.Where(Contains))
|
||||
{
|
||||
if (amountOfOutposts <= Params.MinimumOutpostAmount) { break; }
|
||||
|
||||
if (Map.CurrentLocation is { } currLocation)
|
||||
{
|
||||
// Don't advance on nearby locations to avoid buggy behavior
|
||||
if (currLocation == location || currLocation.Connections.Any(lc => lc.OtherLocation(currLocation) == location)) { continue; }
|
||||
}
|
||||
|
||||
bool wasCritical = location.IsCriticallyRadiated();
|
||||
|
||||
location.TurnsInRadiation++;
|
||||
|
||||
if (location.Type.HasOutpost && !wasCritical && location.IsCriticallyRadiated())
|
||||
{
|
||||
amountOfOutposts--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void IncreaseRadiation(float amount)
|
||||
{
|
||||
Amount += amount;
|
||||
increasedAmount = lastIncrease = amount;
|
||||
}
|
||||
|
||||
public void UpdateRadiation(float deltaTime)
|
||||
{
|
||||
if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; }
|
||||
|
||||
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
|
||||
if (radiationTimer > 0)
|
||||
{
|
||||
radiationTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
radiationTimer = Params.RadiationDamageDelay;
|
||||
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsDead || character.Removed || !(character.CharacterHealth is { } health)) { continue; }
|
||||
|
||||
if (IsEntityRadiated(character))
|
||||
{
|
||||
health.ApplyAffliction(null, new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(Location location)
|
||||
{
|
||||
return Contains(location.MapPosition);
|
||||
}
|
||||
|
||||
public bool Contains(Vector2 pos)
|
||||
{
|
||||
return pos.X < Amount;
|
||||
}
|
||||
|
||||
public bool IsEntityRadiated(Entity entity)
|
||||
{
|
||||
if (!Enabled) { return false; }
|
||||
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);
|
||||
var (startX, startY) = startLocation.MapPosition;
|
||||
var (endX, endY) = endLocation.MapPosition;
|
||||
Vector2 mapPos = new Vector2(startX + (endX - startX), startY + (endY - startY)) * distance;
|
||||
|
||||
return Contains(mapPos);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
{
|
||||
XElement element = new XElement(nameof(Radiation));
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal class RadiationParams: ISerializableEntity
|
||||
{
|
||||
public string Name => nameof(RadiationParams);
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
|
||||
|
||||
[Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")]
|
||||
public float StartingRadiation { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")]
|
||||
public float RadiationStep { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")]
|
||||
public int CriticalRadiationThreshold { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")]
|
||||
public int MinimumOutpostAmount { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")]
|
||||
public float AnimationSpeed { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")]
|
||||
public float RadiationDamageDelay { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")]
|
||||
public float RadiationDamageAmount { get; set; }
|
||||
|
||||
public RadiationParams(XElement element)
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user