Faction Test v1.0.1.0

This commit is contained in:
Regalis11
2023-02-16 15:01:28 +02:00
parent caa5a2f762
commit 2c5a7923b0
309 changed files with 7502 additions and 4335 deletions
@@ -18,9 +18,9 @@ namespace Barotrauma
public const ushort ReservedIDStart = ushort.MaxValue - 3;
public const ushort MaxEntityCount = ushort.MaxValue - 2; //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values
public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
private static readonly Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static IReadOnlyCollection<Entity> GetEntities()
{
return dictionary.Values;
@@ -85,6 +85,28 @@ namespace Barotrauma
this.Submarine = submarine;
spawnTime = Timing.TotalTime;
if (dictionary.Count >= MaxEntityCount)
{
Dictionary<Identifier, int> entityCounts = new Dictionary<Identifier, int>();
foreach (var entity in dictionary)
{
if (entity.Value is MapEntity me)
{
if (entityCounts.ContainsKey(me.Prefab.Identifier))
{
entityCounts[me.Prefab.Identifier]++;
}
else
{
entityCounts[me.Prefab.Identifier] = 1;
}
}
}
string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " +
string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}"));
throw new Exception(errorMsg);
}
//give a unique ID
ID = DetermineID(id, submarine);
@@ -127,6 +127,7 @@ namespace Barotrauma
hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false);
}
Attack.DamageMultiplier = 1.0f;
float displayRange = Attack.Range;
if (damageSource is Item sourceItem)
{
@@ -192,6 +193,12 @@ namespace Barotrauma
item.Condition -= item.MaxCondition * EmpStrength * distFactor;
}
var lightComponent = item.GetComponent<LightComponent>();
if (lightComponent != null)
{
lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f);
}
//discharge batteries
var powerContainer = item.GetComponent<PowerContainer>();
if (powerContainer != null)
@@ -264,7 +271,7 @@ namespace Barotrauma
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
{
float distFactor = 1.0f - dist / displayRange;
float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
Vector2 explosionPos = worldPosition;
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
@@ -352,7 +359,7 @@ namespace Barotrauma
if (affliction.DivideByLimbCount)
{
float limbCountFactor = distFactors.Count;
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage")
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType)
{
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
@@ -4,6 +4,7 @@ using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -51,7 +52,6 @@ namespace Barotrauma
//can ambient light get through the gap even if it's not open
public bool PassAmbientLight;
//a collider outside the gap (for example an ice wall next to the sub)
//used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub
private Body outsideCollisionBlocker;
@@ -63,8 +63,43 @@ namespace Barotrauma
set
{
if (float.IsNaN(value)) { return; }
if (value > open) { openedTimer = 1.0f; }
if (value > open)
{
openedTimer = 1.0f;
}
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
{
if (value > open && value >= 1.0f)
{
InformWaypointsAboutGapState(this, open: true);
}
else if (value < open && open >= 1.0f)
{
InformWaypointsAboutGapState(this, open: false);
}
}
open = MathHelper.Clamp(value, 0.0f, 1.0f);
static void InformWaypointsAboutGapState(Gap gap, bool open)
{
foreach (var wp in WayPoint.WayPointList)
{
if (IsWaypointRightAboveGap(gap, wp))
{
wp.OnGapStateChanged(open, gap);
}
}
}
static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp)
{
if (wp.SpawnType != SpawnType.Path) { return false; }
if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; }
if (wp.Position.Y < gap.Rect.Top) { return false; }
if (wp.Position.X > gap.Rect.Right) { return false; }
if (wp.Position.X < gap.Rect.Left) { return false; }
return true;
}
}
}
@@ -1083,7 +1083,7 @@ namespace Barotrauma
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
{
//gap blocked if the door is not open or the predicted state is not open
if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
{
if (g.ConnectedDoor.OpenState < 0.1f)
{
@@ -9,7 +9,7 @@ namespace Barotrauma
Vector2 WorldPosition { get; }
float Health { get; }
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true);
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true);
public readonly struct AttackEventData
@@ -50,6 +50,12 @@ namespace Barotrauma
Description = TextManager.Get($"EntityDescription.{Identifier}");
Tags = Enumerable.Empty<Identifier>().ToImmutableHashSet();
string description = element.GetAttributeString("description", string.Empty);
if (!description.IsNullOrEmpty())
{
Description = Description.Fallback(description);
}
List<ushort> containedItemIDs = new List<ushort>();
foreach (XElement entityElement in element.Elements())
{
@@ -1700,14 +1700,22 @@ namespace Barotrauma
foreach (VoronoiCell cell in closeCells)
{
bool tooClose = false;
foreach (GraphEdge edge in cell.Edges)
{
if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
if (cell.IsPointInsideAABB(position, margin: minDistance))
{
tooClose = true;
}
else
{
foreach (GraphEdge edge in cell.Edges)
{
tooClose = true;
break;
if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
{
tooClose = true;
break;
}
}
}
if (tooClose) { tooCloseCells.Add(cell); }
@@ -3247,7 +3255,8 @@ namespace Barotrauma
{
suitablePositions.RemoveAll(p => !filter(p));
}
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) ||
positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave))
{
suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2()));
}
@@ -3412,8 +3421,7 @@ namespace Barotrauma
bool closeEnough = false;
foreach (VoronoiCell cell in wall.Cells)
{
if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize &&
Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize)
if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2))
{
closeEnough = true;
break;
@@ -112,10 +112,9 @@ namespace Barotrauma
(int)MathUtils.Round(generationParams.Height, Level.GridCellSize));
}
public LevelData(XElement element, float? forceDifficulty = null)
public LevelData(XElement element, float? forceDifficulty = null, bool clampDifficultyToBiome = false)
{
Seed = element.GetAttributeString("seed", "");
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
Size = element.GetAttributePoint("size", new Point(1000));
Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type);
@@ -131,10 +130,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\".");
GenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(l => l.Type == Type);
if (GenerationParams == null)
{
GenerationParams = LevelGenerationParams.LevelParams.First();
}
GenerationParams ??= LevelGenerationParams.LevelParams.First();
}
InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin);
@@ -147,6 +143,12 @@ namespace Barotrauma
Biome = Biome.Prefabs.First();
}
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
if (clampDifficultyToBiome)
{
Difficulty = MathHelper.Clamp(Difficulty, Biome.MinDifficulty, Biome.AdjustedMaxDifficulty);
}
string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty<string>());
EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier));
@@ -67,7 +67,7 @@ namespace Barotrauma
set;
}
[Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(100.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float Commonness
{
get;
@@ -3,7 +3,6 @@ using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -63,10 +62,17 @@ namespace Barotrauma
public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false;
public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
public int LocationTypeChangeCooldown;
/// <summary>
/// Is some mission blocking this location from changing its type?
/// </summary>
public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges);
public string BaseName { get => baseName; }
public string Name { get; private set; }
@@ -96,6 +102,7 @@ namespace Barotrauma
public class StoreInfo
{
public Identifier Identifier { get; }
public Identifier MerchantFaction { get; private set; }
public int Balance { get; set; }
public List<PurchasedItem> Stock { get; } = new List<PurchasedItem>();
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
@@ -105,6 +112,7 @@ namespace Barotrauma
/// </summary>
public int PriceModifier { get; set; }
public Location Location { get; }
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
private StoreInfo(Location location)
{
@@ -129,6 +137,7 @@ namespace Barotrauma
public StoreInfo(Location location, XElement storeElement) : this(location)
{
Identifier = storeElement.GetAttributeIdentifier("identifier", "");
MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), "");
Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance);
PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
// Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute
@@ -285,13 +294,14 @@ namespace Barotrauma
{
price = Location.DailySpecialPriceModifier * price;
}
// Adjust by current location reputation
price *= Location.GetStoreReputationModifier(true);
// Adjust by current reputation
price *= GetReputationModifier(true);
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
{
if (Location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
var faction = GetMerchantOrLocationFactionIdentifier();
if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
@@ -317,8 +327,8 @@ namespace Barotrauma
{
price = Location.RequestGoodPriceModifier * price;
}
// Adjust by current location reputation
price *= Location.GetStoreReputationModifier(false);
// Adjust by location reputation
price *= GetReputationModifier(false);
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
@@ -331,6 +341,45 @@ namespace Barotrauma
return Math.Max((int)price, 1);
}
public void SetMerchantFaction(Identifier factionIdentifier)
{
MerchantFaction = factionIdentifier;
}
public Identifier GetMerchantOrLocationFactionIdentifier()
{
return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty);
}
public float GetReputationModifier(bool buying)
{
var factionIdentifier = GetMerchantOrLocationFactionIdentifier();
var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation;
if (reputation == null) { return 1.0f; }
if (buying)
{
if (reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
}
}
else
{
if (reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
}
}
}
public override string ToString()
{
return Identifier.Value;
@@ -392,6 +441,8 @@ namespace Barotrauma
}
}
public void SelectMission(Mission mission)
{
if (!SelectedMissions.Contains(mission) && mission != null)
@@ -454,19 +505,22 @@ namespace Barotrauma
public bool IsGateBetweenBiomes;
private struct LoadedMission
private readonly struct LoadedMission
{
public MissionPrefab MissionPrefab { get; }
public int OriginLocationIndex { get; }
public int DestinationIndex { get; }
public bool SelectedMission { get; }
public readonly MissionPrefab MissionPrefab;
public readonly int TimesAttempted;
public readonly int OriginLocationIndex;
public readonly int DestinationIndex;
public readonly bool SelectedMission;
public LoadedMission(MissionPrefab prefab, int originLocationIndex, int destinationIndex, bool selectedMission)
public LoadedMission(XElement element)
{
MissionPrefab = prefab;
OriginLocationIndex = originLocationIndex;
DestinationIndex = destinationIndex;
SelectedMission = selectedMission;
var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty);
MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null;
TimesAttempted = element.GetAttributeInt("timesattempted", 0);
OriginLocationIndex = element.GetAttributeInt("origin", -1);
DestinationIndex = element.GetAttributeInt("destinationindex", -1);
SelectedMission = element.GetAttributeBool("selected", false);
}
}
@@ -577,12 +631,9 @@ namespace Barotrauma
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!");
if (Type == null)
{
Type = LocationType.Prefabs.First();
}
Type ??= LocationType.Prefabs.First();
LevelData = new LevelData(element.Element("Level"));
LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true);
PortraitId = ToolBox.StringToInt(Name);
@@ -661,14 +712,11 @@ namespace Barotrauma
loadedMissions = new List<LoadedMission>();
foreach (XElement childElement in missionsElement.GetChildElements("mission"))
{
var id = childElement.GetAttributeString("prefabid", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var origin = childElement.GetAttributeInt("origin", -1);
var destination = childElement.GetAttributeInt("destinationindex", -1);
var selected = childElement.GetAttributeBool("selected", false);
loadedMissions.Add(new LoadedMission(prefab, origin, destination, selected));
var loadedMission = new LoadedMission(childElement);
if (loadedMission.MissionPrefab != null)
{
loadedMissions.Add(loadedMission);
}
}
}
}
@@ -693,7 +741,7 @@ namespace Barotrauma
Type = newType;
Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
if (Type.HasOutpost)
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
if (Faction == null)
{
@@ -734,30 +782,14 @@ namespace Barotrauma
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
var mission = InstantiateMission(missionPrefab, connection);
if (!mission.Prefab.AllowOtherMissionsInLevel)
{
availableMissions.Clear();
}
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#endif
AddMission(InstantiateMission(missionPrefab, connection));
}
public void UnlockMission(MissionPrefab missionPrefab)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
var mission = InstantiateMission(missionPrefab);
if (!mission.Prefab.AllowOtherMissionsInLevel)
{
availableMissions.Clear();
}
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#endif
AddMission(InstantiateMission(missionPrefab));
}
public Mission UnlockMissionByIdentifier(Identifier identifier)
@@ -778,14 +810,7 @@ namespace Barotrauma
{
return null;
}
if (!mission.Prefab.AllowOtherMissionsInLevel)
{
availableMissions.Clear();
}
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#endif
AddMission(mission);
return mission;
}
return null;
@@ -816,14 +841,7 @@ namespace Barotrauma
{
return null;
}
if (!mission.Prefab.AllowOtherMissionsInLevel)
{
availableMissions.Clear();
}
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#endif
AddMission(mission);
return mission;
}
else
@@ -835,6 +853,20 @@ namespace Barotrauma
return null;
}
private void AddMission(Mission mission)
{
if (!mission.Prefab.AllowOtherMissionsInLevel)
{
availableMissions.Clear();
}
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#else
(GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
#endif
}
private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
{
if (prefab.IsAllowed(this, this))
@@ -933,6 +965,7 @@ namespace Barotrauma
{
mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
}
mission.TimesAttempted = loadedMission.TimesAttempted;
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
}
@@ -1332,33 +1365,6 @@ namespace Barotrauma
}
}
public float GetStoreReputationModifier(bool buying)
{
if (Reputation == null) { return 1.0f; }
if (buying)
{
if (Reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation);
}
}
else
{
if (Reputation.Value > 0.0f)
{
return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation);
}
else
{
return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation);
}
}
}
public static int GetExtraSpecialSalesCount()
{
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
@@ -1488,6 +1494,7 @@ namespace Barotrauma
{
var storeElement = new XElement("store",
new XAttribute("identifier", store.Identifier.Value),
new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction),
new XAttribute("balance", store.Balance),
new XAttribute("pricemodifier", store.PriceModifier));
foreach (PurchasedItem item in store.Stock)
@@ -1532,6 +1539,7 @@ namespace Barotrauma
missionsElement.Add(new XElement("mission",
new XAttribute("prefabid", mission.Prefab.Identifier),
new XAttribute("destinationindex", destinationIndex),
new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted),
new XAttribute("origin", originIndex),
new XAttribute("selected", selectedMissions.Contains(mission))));
}
@@ -41,6 +41,11 @@ namespace Barotrauma
public bool IsEnterable { get; private set; }
/// <summary>
/// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone
/// </summary>
public bool AllowInRandomLevels { get; private set; }
public bool UsePortraitInRandomLoadingScreens
{
get;
@@ -115,6 +120,7 @@ namespace Barotrauma
UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true);
HasOutpost = element.GetAttributeBool("hasoutpost", true);
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true);
ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true);
@@ -263,14 +269,31 @@ namespace Barotrauma
return names[rand.Next() % names.Length];
}
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false)
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
{
Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
LocationType[] allowedLocationTypes =
Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost))
Prefabs.Where(lt =>
(predicate == null || predicate(lt)) && IsValid(lt))
.OrderBy(p => p.UintIdentifier).ToArray();
bool IsValid(LocationType lt)
{
if (requireOutpost && !lt.HasOutpost) { return false; }
if (zone.HasValue)
{
if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; }
}
//if zone is not defined, this is a "random" (non-campaign) level
//-> don't choose location types that aren't allowed in those
else if (!lt.AllowInRandomLevels)
{
return false;
}
return true;
}
if (allowedLocationTypes.Length == 0)
{
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using static Barotrauma.LocationTypeChange;
namespace Barotrauma
{
@@ -58,7 +58,7 @@ namespace Barotrauma
public Requirement(XElement element, LocationTypeChange change)
{
RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty<Identifier>())).ToImmutableArray();
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1);
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0);
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false);
@@ -91,37 +91,30 @@ namespace Barotrauma
}
}
public bool AnyWithinDistance(Location startLocation, int distance)
{
return Map.LocationOrConnectionWithinDistance(
startLocation,
maxDistance: distance,
criteria: MatchesLocation,
connectionCriteria: MatchesConnection);
}
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)
public bool MatchesConnection(LocationConnection connection)
{
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)
{
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 true;
}
if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds)
{
return true;
}
return false;
}
}
@@ -227,8 +220,9 @@ namespace Barotrauma
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); }))
if (DisallowedAdjacentLocations.Any() &&
Map.LocationOrConnectionWithinDistance(location, DisallowedProximity,
(otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
{
return 0.0f;
}
@@ -247,7 +241,6 @@ namespace Barotrauma
probability *= requirement.Probability;
}
}
if (location.ProximityTimer.ContainsKey(requirement))
{
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
@@ -266,25 +259,5 @@ namespace Barotrauma
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;
}
}
}
@@ -70,13 +70,13 @@ namespace Barotrauma
public List<Location> Locations { get; private set; }
private readonly List<Location> locationsDiscovered = new List<Location>();
private readonly List<Location> outpostsVisited = new List<Location>();
private readonly List<Location> locationsVisited = new List<Location>();
public List<LocationConnection> Connections { get; private set; }
public Radiation Radiation;
private bool wasLocationDiscoveryOrderTracked = true;
private bool trackedLocationDiscoveryAndVisitOrder = true;
public Map(CampaignSettings settings)
{
@@ -939,6 +939,12 @@ namespace Barotrauma
public void MoveToNextLocation()
{
if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
{
//force the location at the end of the level to be selected, even if it's been deselect on the map
//(e.g. due to returning to an empty location the beginning of the level during the round)
SelectLocation(Level.Loaded.EndLocation);
}
if (SelectedConnection == null)
{
if (!endLocations.Contains(CurrentLocation))
@@ -1043,12 +1049,24 @@ namespace Barotrauma
Location prevSelected = SelectedLocation;
SelectedLocation = Locations[index];
var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
SelectedConnection =
Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
if (currentDisplayLocation == SelectedLocation)
{
SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
}
else
{
SelectedConnection =
Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
}
if (SelectedConnection?.Locked ?? false)
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
string errorMsg =
$"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." +
$" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError(errorMsg);
}
if (prevSelected != SelectedLocation)
{
@@ -1266,51 +1284,6 @@ namespace Barotrauma
return false;
}
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
{
if (startingLocation.Type.HasOutpost)
{
endingLocation = startingLocation;
return 0;
}
int iterations = 0;
int distance = 0;
endingLocation = null;
List<Location> testedLocations = new List<Location>();
List<Location> locationsToTest = new List<Location> { startingLocation };
while (endingLocation == null && iterations < 100)
{
List<Location> nextTestingBatch = new List<Location>();
for (int i = 0; i < locationsToTest.Count; i++)
{
Location testLocation = locationsToTest[i];
for (int j = 0; j < testLocation.Connections.Count; j++)
{
Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation);
if (potentialOutpost.Type.HasOutpost)
{
distance = iterations + 1;
endingLocation = potentialOutpost;
}
else if (!testedLocations.Contains(potentialOutpost))
{
nextTestingBatch.Add(potentialOutpost);
}
}
testedLocations.Add(testLocation);
}
locationsToTest = nextTestingBatch;
iterations++;
}
return distance;
}
private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
{
string prevName = location.Name;
@@ -1321,6 +1294,8 @@ namespace Barotrauma
return false;
}
if (location.LocationTypeChangesBlocked) { return false; }
if (newType.OutpostTeam != location.Type.OutpostTeam ||
newType.HasOutpost != location.Type.HasOutpost)
{
@@ -1338,6 +1313,50 @@ namespace Barotrauma
return true;
}
public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
{
return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance;
}
/// <summary>
/// Get the shortest distance from the start location to another location that satisfies the specified criteria.
/// </summary>
/// <returns>The distance to a matching location, or int.MaxValue if none are found.</returns>
public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
{
int distance = 0;
var locationsToTest = new List<Location>() { startLocation };
var nextBatchToTest = new HashSet<Location>();
var checkedLocations = new HashSet<Location>();
while (locationsToTest.Any())
{
foreach (var location in locationsToTest)
{
checkedLocations.Add(location);
if (criteria(location)) { return distance; }
foreach (var connection in location.Connections)
{
if (connectionCriteria != null && connectionCriteria(connection))
{
return distance;
}
var otherLocation = connection.OtherLocation(location);
if (!checkedLocations.Contains(otherLocation))
{
nextBatchToTest.Add(otherLocation);
}
}
if (distance > maxDistance) { return int.MaxValue; }
}
distance++;
locationsToTest.Clear();
locationsToTest.AddRange(nextBatchToTest);
nextBatchToTest.Clear();
}
return int.MaxValue;
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
partial void ClearAnimQueue();
@@ -1356,29 +1375,38 @@ namespace Barotrauma
public void Visit(Location location)
{
if (location is null) { return; }
if (!location.HasOutpost()) { return; }
if (outpostsVisited.Contains(location)) { return; }
outpostsVisited.Add(location);
if (locationsVisited.Contains(location)) { return; }
locationsVisited.Add(location);
RemoveFogOfWarProjSpecific(location);
}
public void ClearLocationHistory()
{
locationsDiscovered.Clear();
outpostsVisited.Clear();
locationsVisited.Clear();
}
public int? GetDiscoveryIndex(Location location)
{
if (!wasLocationDiscoveryOrderTracked) { return null; }
if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
if (location is null) { return -1; }
return locationsDiscovered.IndexOf(location);
}
public int? GetVisitIndex(Location location)
public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false)
{
if (!wasLocationDiscoveryOrderTracked) { return null; }
if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
if (location is null) { return -1; }
return outpostsVisited.IndexOf(location);
int index = locationsVisited.IndexOf(location);
if (includeLocationsWithoutOutpost) { return index; }
int noOutpostLocations = 0;
for (int i = 0; i < index; i++)
{
if (locationsVisited[i] is not Location l) { continue; }
if (l.HasOutpost()) { continue; }
noOutpostLocations++;
}
return index - noOutpostLocations;
}
public bool IsDiscovered(Location location)
@@ -1387,6 +1415,14 @@ namespace Barotrauma
return locationsDiscovered.Contains(location);
}
public bool IsVisited(Location location)
{
if (location is null) { return false; }
return locationsVisited.Contains(location);
}
partial void RemoveFogOfWarProjSpecific(Location location);
/// <summary>
/// Load a previously saved map from an xml element
/// </summary>
@@ -1431,11 +1467,13 @@ namespace Barotrauma
}
location.LoadLocationTypeChange(subElement);
// Backwards compatibility
// 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);
wasLocationDiscoveryOrderTracked = false;
Visit(location);
trackedLocationDiscoveryAndVisitOrder = false;
}
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
@@ -1472,32 +1510,45 @@ namespace Barotrauma
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
break;
case "discovered":
bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false);
foreach (var childElement in subElement.GetChildElements("location"))
{
int index = childElement.GetAttributeInt("i", -1);
if (index < 0) { continue; }
if (Locations[index] is not Location l) { continue; }
Discover(l);
if (GetLocation(childElement) is Location l)
{
Discover(l);
if (!trackedVisitedEmptyLocations)
{
if (!l.HasOutpost())
{
Visit(l);
}
trackedLocationDiscoveryAndVisitOrder = false;
}
}
}
break;
case "visited":
foreach (var childElement in subElement.GetChildElements("location"))
{
int index = childElement.GetAttributeInt("i", -1);
if (index < 0) { continue; }
if (Locations[index] is not Location l) { continue; }
Visit(l);
if (GetLocation(childElement) is Location l)
{
Visit(l);
}
}
break;
}
Location GetLocation(XElement element)
{
int index = element.GetAttributeInt("i", -1);
if (index < 0) { return null; }
return Locations[index];
}
}
void Discover(Location location)
{
this.Discover(location, checkTalents: false);
#if CLIENT
RemoveFogOfWar(location);
#endif
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
@@ -1509,9 +1560,6 @@ namespace Barotrauma
location?.InstantiateLoadedMissions(this);
}
#if RELEASE
TODO: MAKE SURE THE VERSION NUMBER BELOW IS CORRECT FOR THE FULL RELEASE (OR WHICHEVER UPDATE WE ADD THE FACTIONS IN)
#endif
//backwards compatibility:
//if the save is from a version prior to the addition of faction-specific outposts, assign factions
if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null))
@@ -1599,7 +1647,8 @@ namespace Barotrauma
if (locationsDiscovered.Any())
{
var discoveryElement = new XElement("discovered");
var discoveryElement = new XElement("discovered",
new XAttribute("trackedvisitedemptylocations", true));
foreach (Location location in locationsDiscovered)
{
int index = Locations.IndexOf(location);
@@ -1609,10 +1658,10 @@ namespace Barotrauma
mapElement.Add(discoveryElement);
}
if (outpostsVisited.Any())
if (locationsVisited.Any())
{
var visitElement = new XElement("visited");
foreach (Location location in outpostsVisited)
foreach (Location location in locationsVisited)
{
int index = Locations.IndexOf(location);
var locationElement = new XElement("location", new XAttribute("i", index));
@@ -59,7 +59,24 @@ namespace Barotrauma
//the position and dimensions of the entity
protected Rectangle rect;
public bool ExternalHighlight = false;
protected static readonly HashSet<MapEntity> highlightedEntities = new HashSet<MapEntity>();
public static IEnumerable<MapEntity> HighlightedEntities => highlightedEntities;
private bool externalHighlight = false;
public bool ExternalHighlight
{
get { return externalHighlight; }
set
{
if (value != externalHighlight)
{
externalHighlight = value;
CheckIsHighlighted();
}
}
}
//is the mouse inside the rect
private bool isHighlighted;
@@ -67,7 +84,14 @@ namespace Barotrauma
public bool IsHighlighted
{
get { return isHighlighted || ExternalHighlight; }
set { isHighlighted = value; }
set
{
if (value != IsHighlighted)
{
isHighlighted = value;
CheckIsHighlighted();
}
}
}
public virtual Rectangle Rect
@@ -362,6 +386,30 @@ namespace Barotrauma
return true;
}
protected virtual void CheckIsHighlighted()
{
if (IsHighlighted || ExternalHighlight)
{
highlightedEntities.Add(this);
}
else
{
highlightedEntities.Remove(this);
}
}
private static readonly List<MapEntity> tempHighlightedEntities = new List<MapEntity>();
public static void ClearHighlightedEntities()
{
tempHighlightedEntities.Clear();
tempHighlightedEntities.AddRange(highlightedEntities);
foreach (var entity in tempHighlightedEntities)
{
entity.IsHighlighted = false;
}
}
public abstract MapEntity Clone();
public static List<MapEntity> Clone(List<MapEntity> entitiesToClone)
@@ -649,6 +697,9 @@ namespace Barotrauma
List<MapEntity> entities = new List<MapEntity>();
foreach (var element in parentElement.Elements())
{
#if CLIENT
GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested();
#endif
string typeName = element.Name.ToString();
Type t;
@@ -174,6 +174,9 @@ namespace Barotrauma
public abstract Sprite Sprite { get; }
public virtual bool CanSpriteFlipX { get; } = false;
public virtual bool CanSpriteFlipY { get; } = false;
public abstract string OriginalName { get; }
public abstract LocalizedString Name { get; }
@@ -150,10 +150,13 @@ namespace Barotrauma
selectedModules.Clear();
//select which module types the outpost should consist of
List<Identifier> pendingModuleFlags =
onlyEntrance ?
(generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty<Identifier>()).ToList() :
SelectModules(outpostModules, location, generationParams);
List<Identifier> pendingModuleFlags = new List<Identifier>();
if (generationParams.ModuleCounts.Any())
{
pendingModuleFlags = onlyEntrance ?
generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() :
SelectModules(outpostModules, location, generationParams);
}
foreach (Identifier flag in pendingModuleFlags)
{
@@ -1446,6 +1449,16 @@ namespace Barotrauma
{
me.HiddenInGame =
location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier];
#if CLIENT
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
if (me.HiddenInGame && me is Item item)
{
foreach (var lightComponent in item.GetComponents<LightComponent>())
{
lightComponent.Light.Enabled = false;
}
}
#endif
}
}
}
@@ -20,8 +20,8 @@ namespace Barotrauma
public readonly ContentXElement ConfigElement;
public readonly bool CanSpriteFlipX;
public readonly bool CanSpriteFlipY;
public override bool CanSpriteFlipX { get; }
public override bool CanSpriteFlipY { get; }
/// <summary>
/// If null, the orientation is determined automatically based on the dimensions of the structure instances
@@ -1379,19 +1379,6 @@ namespace Barotrauma
Info = new SubmarineInfo(info);
ConnectedDockingPorts = new Dictionary<Submarine, DockingPort>();
//place the sub above the top of the level
HiddenSubPosition = HiddenSubStartPosition;
if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null)
{
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
}
foreach (Submarine sub in loaded)
{
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
}
IdOffset = IdRemap.DetermineNewOffset();
List<MapEntity> newEntities = new List<MapEntity>();
@@ -1419,7 +1406,6 @@ namespace Barotrauma
Vector2 center = Vector2.Zero;
var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this);
if (matchingHulls.Any())
{
Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
@@ -1441,6 +1427,17 @@ namespace Barotrauma
}
subBody = new SubmarineBody(this, showErrorMessages);
//place the sub above the top of the level
HiddenSubPosition = HiddenSubStartPosition;
if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null)
{
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
}
foreach (Submarine sub in loaded)
{
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
}
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
@@ -1745,57 +1742,66 @@ namespace Barotrauma
public static void Unload()
{
if (Unloading)
{
DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading.");
return;
}
Unloading = true;
try
{
#if CLIENT
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
#endif
var _loaded = new List<Submarine>(loaded);
foreach (Submarine sub in _loaded)
{
sub.Remove();
}
loaded.Clear();
visibleEntities = null;
if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; }
RemoveAll();
if (Item.ItemList.Count > 0)
{
List<Item> items = new List<Item>(Item.ItemList);
foreach (Item item in items)
var _loaded = new List<Submarine>(loaded);
foreach (Submarine sub in _loaded)
{
DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed");
try
{
item.Remove();
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e);
}
sub.Remove();
}
Item.ItemList.Clear();
loaded.Clear();
visibleEntities = null;
if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; }
RemoveAll();
if (Item.ItemList.Count > 0)
{
List<Item> items = new List<Item>(Item.ItemList);
foreach (Item item in items)
{
DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed");
try
{
item.Remove();
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e);
}
}
Item.ItemList.Clear();
}
Ragdoll.RemoveAll();
PhysicsBody.RemoveAll();
GameMain.World = null;
Powered.Grids.Clear();
GC.Collect();
}
finally
{
Unloading = false;
}
Ragdoll.RemoveAll();
PhysicsBody.RemoveAll();
GameMain.World?.Clear();
GameMain.World = null;
Powered.Grids.Clear();
GC.Collect();
Unloading = false;
}
public override void Remove()
@@ -1862,18 +1868,18 @@ namespace Barotrauma
{
if (node == null || node.Waypoint == null) { continue; }
var wp = node.Waypoint;
if (wp.isObstructed) { continue; }
if (wp.IsObstructed) { continue; }
foreach (var connection in node.connections)
{
var connectedWp = connection.Waypoint;
if (connectedWp.isObstructed) { continue; }
if (connectedWp.IsObstructed) { continue; }
Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition);
var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false);
if (body != null)
{
connectedWp.isObstructed = true;
wp.isObstructed = true;
connectedWp.IsObstructed = true;
wp.IsObstructed = true;
break;
}
}
@@ -1892,11 +1898,11 @@ namespace Barotrauma
{
if (node == null || node.Waypoint == null) { continue; }
var wp = node.Waypoint;
if (wp.isObstructed) { continue; }
if (wp.IsObstructed) { continue; }
foreach (var connection in node.connections)
{
var connectedWp = connection.Waypoint;
if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; }
if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; }
Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition;
Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition;
var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true);
@@ -1904,8 +1910,8 @@ namespace Barotrauma
{
if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
{
connectedWp.isObstructed = true;
wp.isObstructed = true;
connectedWp.IsObstructed = true;
wp.IsObstructed = true;
if (!obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
{
nodes = new HashSet<PathNode>();
@@ -1927,7 +1933,7 @@ namespace Barotrauma
{
if (obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
{
nodes.ForEach(n => n.Waypoint.isObstructed = false);
nodes.ForEach(n => n.Waypoint.IsObstructed = false);
nodes.Clear();
obstructedNodes.Remove(otherSub);
}
@@ -46,6 +46,7 @@ namespace Barotrauma
}
private float depthDamageTimer = 10.0f;
private float damageSoundTimer = 10.0f;
private readonly Submarine submarine;
@@ -507,36 +508,58 @@ namespace Barotrauma
if (Level.Loaded == null) { return; }
//camera shake and sounds start playing 500 meters before crush depth
float depthEffectThreshold = 500.0f;
if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold)
const float CosmeticEffectThreshold = -500.0f;
//breaches won't get any more severe 500 meters below crush depth
const float MaxEffectThreshold = 500.0f;
const float MinWallDamageProbability = 0.1f;
const float MaxWallDamageProbability = 1.0f;
const float MinWallDamage = 50f;
const float MaxWallDamage = 500.0f;
const float MinCameraShake = 5f;
const float MaxCameraShake = 50.0f;
if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)
{
return;
}
depthDamageTimer -= deltaTime;
if (depthDamageTimer > 0.0f) { return; }
#if CLIENT
SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f);
#endif
foreach (Structure wall in Structure.WallList)
damageSoundTimer -= deltaTime;
if (damageSoundTimer <= 0.0f)
{
if (wall.Submarine != submarine) { continue; }
float wallCrushDepth = wall.CrushDepth;
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
if (pastCrushDepth > 0)
{
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f);
}
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f));
}
#if CLIENT
SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f);
#endif
damageSoundTimer = Rand.Range(5.0f, 10.0f);
}
depthDamageTimer = 10.0f;
depthDamageTimer -= deltaTime;
if (depthDamageTimer <= 0.0f)
{
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != submarine) { continue; }
float wallCrushDepth = wall.CrushDepth;
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f);
if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; }
float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio);
if (pastCrushDepth > 0)
{
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f);
#if CLIENT
SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f);
#endif
}
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio));
}
}
depthDamageTimer = Rand.Range(5.0f, 10.0f);
}
}
public void FlipX()
@@ -479,7 +479,6 @@ namespace Barotrauma
hashTask = new Task(() =>
{
hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace);
Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow);
});
hashTask.Start();
}
@@ -532,11 +531,15 @@ namespace Barotrauma
/// <summary>
/// Calculated from <see cref="SubmarineElement"/>. Can be used when the sub hasn't been loaded and we can't access <see cref="Submarine.RealWorldCrushDepth"/>.
/// </summary>
public float GetRealWorldCrushDepth()
public bool IsCrushDepthDefinedInStructures(out float realWorldCrushDepth)
{
if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; }
if (SubmarineElement == null)
{
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
return false;
}
bool structureCrushDepthsDefined = false;
float realWorldCrushDepth = float.PositiveInfinity;
realWorldCrushDepth = float.PositiveInfinity;
foreach (var structureElement in SubmarineElement.GetChildElements("structure"))
{
string name = structureElement.Attribute("name")?.Value ?? "";
@@ -554,7 +557,7 @@ namespace Barotrauma
{
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
}
return realWorldCrushDepth;
return structureCrushDepthsDefined;
}
public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag)
{
@@ -595,7 +598,6 @@ namespace Barotrauma
}
SaveUtil.CompressStringToFile(filePath, doc.ToString());
Md5Hash.Cache.Remove(filePath);
}
public static void AddToSavedSubs(SubmarineInfo subInfo)
@@ -776,7 +778,7 @@ namespace Barotrauma
float price = Price;
if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction, characterList) is FactionAffiliation.Positive)
if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
{
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated));
}
@@ -29,7 +29,32 @@ namespace Barotrauma
private HashSet<Identifier> tags;
public bool isObstructed;
public bool IsObstructed;
public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y;
// Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open.
// The only way to add the open gaps should be by calling OnGapStateSchanged.
public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater);
private HashSet<Gap> openGaps;
/// <summary>
/// Only called by a Gap when the state changes.
/// So in practice used like an event callback, although technically just a method
/// (It would be cleaner to use an actual event in Gap.cs, but event registering and unregistering might cause an extra hassle)
/// </summary>
public void OnGapStateChanged(bool open, Gap gap)
{
openGaps ??= new HashSet<Gap>();
if (open)
{
openGaps.Add(gap);
}
else
{
openGaps.Remove(gap);
}
}
private ushort gapId;
public Gap ConnectedGap
@@ -989,6 +1014,12 @@ namespace Barotrauma
public override void OnMapLoaded()
{
if (Submarine == null)
{
// Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc.
// Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost.
return;
}
InitializeLinks();
FindHull();
FindStairs();