v0.13.0.11

This commit is contained in:
Joonas Rikkonen
2021-04-22 17:33:08 +03:00
parent 0697d7fc64
commit 8bb31f2893
391 changed files with 17271 additions and 5949 deletions
@@ -87,7 +87,7 @@ namespace Barotrauma.MapCreatures.Behavior
internal partial class BallastFloraBehavior : ISerializableEntity
{
#if DEBUG || UNSTABLE
#if DEBUG
public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
#endif
@@ -60,7 +60,7 @@ namespace Barotrauma.MapCreatures.Behavior
protected virtual void Grow()
{
List<BallastFloraBranch> newTiles = GrowRandomly();
#if DEBUG || UNSTABLE
#if DEBUG
Behavior.debugSearchLines.Clear();
#endif
if (newTiles.Any(TryScanTargets)) { return; }
@@ -135,7 +135,7 @@ namespace Barotrauma.MapCreatures.Behavior
Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position);
#if DEBUG || UNSTABLE
#if DEBUG
Tuple<Vector2, Vector2> debugLine1 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(topLeft), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos - diameter));
Tuple<Vector2, Vector2> debugLine2 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(bottomRight), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos + diameter));
Behavior.debugSearchLines.Add(debugLine2);
@@ -15,7 +15,7 @@ namespace Barotrauma
{
private static readonly List<Triplet<Explosion, Vector2, float>> prevExplosions = new List<Triplet<Explosion, Vector2, float>>();
private readonly Attack attack;
public readonly Attack Attack;
private readonly float force;
@@ -27,6 +27,10 @@ namespace Barotrauma
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
private bool playTinnitus;
private bool applyFireEffects;
private string[] ignoreFireEffectsForTags;
private bool ignoreCover;
private bool onlyInside;
private bool onlyOutside;
private readonly float flashDuration;
private readonly float? flashRange;
private readonly string decal;
@@ -38,7 +42,7 @@ namespace Barotrauma
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f)
{
attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range)
Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range)
{
SeverLimbsProbability = 1.0f
};
@@ -50,11 +54,12 @@ namespace Barotrauma
smoke = true;
flames = true;
underwaterBubble = true;
ignoreFireEffectsForTags = new string[0];
}
public Explosion(XElement element, string parentDebugName)
{
attack = new Attack(element, parentDebugName + ", Explosion");
Attack = new Attack(element, parentDebugName + ", Explosion");
force = element.GetAttributeFloat("force", 0.0f);
@@ -67,6 +72,11 @@ namespace Barotrauma
playTinnitus = element.GetAttributeBool("playtinnitus", true);
applyFireEffects = element.GetAttributeBool("applyfireeffects", flames);
ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true);
ignoreCover = element.GetAttributeBool("ignorecover", false);
onlyInside = element.GetAttributeBool("onlyinside", false);
onlyOutside = element.GetAttributeBool("onlyoutside", false);
flash = element.GetAttributeBool("flash", true);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
@@ -78,10 +88,10 @@ namespace Barotrauma
decal = element.GetAttributeString("decal", "");
decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize");
cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f);
cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range);
cameraShake = element.GetAttributeFloat("camerashake", Attack.Range * 0.1f);
cameraShakeRange = element.GetAttributeFloat("camerashakerange", Attack.Range);
screenColorRange = element.GetAttributeFloat("screencolorrange", attack.Range * 0.1f);
screenColorRange = element.GetAttributeFloat("screencolorrange", Attack.Range * 0.1f);
screenColor = element.GetAttributeColor("screencolor", Color.Transparent);
screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f);
}
@@ -117,7 +127,7 @@ namespace Barotrauma
hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false);
}
float displayRange = attack.Range;
float displayRange = Attack.Range;
Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position;
float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f;
@@ -132,9 +142,9 @@ namespace Barotrauma
if (displayRange < 0.1f) { return; }
if (attack.GetStructureDamage(1.0f) > 0.0f)
if (Attack.GetStructureDamage(1.0f) > 0.0f || Attack.GetLevelWallDamage(1.0f) > 0.0f)
{
RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attack.GetLevelWallDamage(1.0f), attacker);
RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker);
}
if (BallastFloraDamage > 0.0f)
@@ -169,12 +179,12 @@ namespace Barotrauma
}
}
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f))
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f))
{
return;
}
DamageCharacters(worldPosition, attack, force, damageSource, attacker);
DamageCharacters(worldPosition, Attack, force, damageSource, attacker);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -184,9 +194,9 @@ namespace Barotrauma
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius));
if (dist > attack.Range) { continue; }
if (dist > Attack.Range) { continue; }
if (dist < attack.Range * 0.5f && applyFireEffects && !item.FireProof)
if (dist < Attack.Range * 0.5f && applyFireEffects && !item.FireProof && ignoreFireEffectsForTags.None(t => item.HasTag(t)))
{
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
@@ -213,8 +223,8 @@ namespace Barotrauma
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
{
float distFactor = 1.0f - dist / attack.Range;
float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
float distFactor = 1.0f - dist / Attack.Range;
float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
Vector2 explosionPos = worldPosition;
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
@@ -243,6 +253,8 @@ namespace Barotrauma
{
continue;
}
if (onlyInside && c.Submarine == null) { continue; }
else if (onlyOutside && c.Submarine != null) { continue; }
Vector2 explosionPos = worldPosition;
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
@@ -271,7 +283,10 @@ namespace Barotrauma
float distFactor = 1.0f - dist / attack.Range;
//solid obstacles between the explosion and the limb reduce the effect of the explosion
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition);
if (!ignoreCover)
{
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition);
}
distFactors.Add(limb, distFactor);
modifiedAfflictions.Clear();
@@ -203,9 +203,9 @@ namespace Barotrauma
public void AutoOrient()
{
Vector2 searchPosLeft = new Vector2(rect.X, rect.Y - rect.Height / 2);
Hull hullLeft = Hull.FindHullOld(searchPosLeft, null, false);
Hull hullLeft = Hull.FindHullUnoptimized(searchPosLeft, null, false);
Vector2 searchPosRight = new Vector2(rect.Right, rect.Y - rect.Height / 2);
Hull hullRight = Hull.FindHullOld(searchPosRight, null, false);
Hull hullRight = Hull.FindHullUnoptimized(searchPosRight, null, false);
if (hullLeft != null && hullRight != null && hullLeft != hullRight)
{
@@ -214,9 +214,9 @@ namespace Barotrauma
}
Vector2 searchPosTop = new Vector2(rect.Center.X, rect.Y);
Hull hullTop = Hull.FindHullOld(searchPosTop, null, false);
Hull hullTop = Hull.FindHullUnoptimized(searchPosTop, null, false);
Vector2 searchPosBottom = new Vector2(rect.Center.X, rect.Y - rect.Height);
Hull hullBottom = Hull.FindHullOld(searchPosBottom, null, false);
Hull hullBottom = Hull.FindHullUnoptimized(searchPosBottom, null, false);
if (hullTop != null && hullBottom != null && hullTop != hullBottom)
{
@@ -261,8 +261,8 @@ namespace Barotrauma
for (int i = 0; i < 2; i++)
{
hulls[i] = Hull.FindHullOld(searchPos[i], null, false);
if (hulls[i] == null) hulls[i] = Hull.FindHullOld(searchPos[i], null, false, true);
hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false);
if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true);
}
if (hulls[0] == null && hulls[1] == null) { return; }
@@ -939,14 +939,14 @@ namespace Barotrauma
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
/// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found.
/// </summary>
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance)
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0)
{
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance);
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor);
}
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance)
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0)
{
if (distance >= maxDistance) return float.MaxValue;
if (distance >= maxDistance) { return float.MaxValue; }
if (this == target)
{
return distance + Vector2.Distance(startPos, endPos);
@@ -956,12 +956,17 @@ namespace Barotrauma
foreach (Gap g in ConnectedGaps)
{
float distanceMultiplier = 1;
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.OpenState < 0.1f) continue;
if (g.ConnectedDoor.OpenState < 0.1f)
{
if (distanceMultiplierFromDoors <= 0) { continue; }
distanceMultiplier *= distanceMultiplierFromDoors;
}
}
}
else if (g.Open <= 0.0f)
@@ -973,8 +978,11 @@ namespace Barotrauma
{
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
{
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance);
if (dist < float.MaxValue) { return dist; }
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance);
if (dist < float.MaxValue)
{
return dist;
}
}
}
}
@@ -982,7 +990,13 @@ namespace Barotrauma
return float.MaxValue;
}
//returns the water block which contains the point (or null if it isn't inside any)
/// <summary>
/// Returns the hull which contains the point (or null if it isn't inside any)
/// </summary>
/// <param name="position">The position to check</param>
/// <param name="guess">This hull is checked first: if the current hull is known, this can be used as an optimization</param>
/// <param name="useWorldCoordinates">Should world coordinates or the sub's local coordinates be used?</param>
/// <param name="inclusive">Does being exactly at the edge of the hull count as being inside?</param>
public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
if (EntityGrids == null) return null;
@@ -1030,20 +1044,19 @@ namespace Barotrauma
return null;
}
//returns the water block which contains the point (or null if it isn't inside any)
public static Hull FindHullOld(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
/// <summary>
/// Returns the hull which contains the point (or null if it isn't inside any). The difference to FindHull is that this method goes through all hulls without trying
/// to first find the sub the point is inside and checking the hulls in that sub.
/// = This is slower, use with caution in situations where the sub's extents or hulls may have changed after it was loaded.
/// </summary>
public static Hull FindHullUnoptimized(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
return FindHullOld(position, hullList, guess, useWorldCoordinates, inclusive);
}
public static Hull FindHullOld(Vector2 position, List<Hull> hulls, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
if (guess != null && hulls.Contains(guess))
if (guess != null && hullList.Contains(guess))
{
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
}
foreach (Hull hull in hulls)
foreach (Hull hull in hullList)
{
if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull;
}
@@ -135,13 +135,18 @@ namespace Barotrauma
}
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false)
{
return PasteEntities(position, sub, configElement, FilePath, selectInstance);
}
public static List<MapEntity> PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false)
{
int idOffset = Entity.FindFreeID(1);
if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); }
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, FilePath, idOffset);
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, filePath, idOffset);
if (entities.Count == 0) { return entities; }
Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition;
Vector2 offset = sub?.HiddenSubPosition ?? Vector2.Zero;
foreach (MapEntity me in entities)
{
@@ -168,9 +173,8 @@ namespace Barotrauma
MapEntity.SelectedList.Clear();
entities.ForEach(MapEntity.AddSelection);
}
#endif
#endif
return entities;
}
public void Delete()
@@ -101,19 +101,19 @@ namespace Barotrauma
public Sprite WallSprite { get; private set; }
public Sprite WallEdgeSprite { get; private set; }
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, Rand.RandSync rand)
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand)
{
if (CaveParams.All(p => p.GetCommonness(generationParams) <= 0.0f))
if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f))
{
return CaveParams.First();
}
return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams)).ToList(), rand);
return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand);
}
public float GetCommonness(LevelGenerationParams generationParams)
public float GetCommonness(LevelGenerationParams generationParams, bool abyss)
{
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness))
{
return commonness;
}
@@ -141,29 +141,21 @@ namespace Barotrauma
return cells;
}
public static void GeneratePath(Level.Tunnel tunnel, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, Rectangle limits)
public static void GeneratePath(Level.Tunnel tunnel, Level level)
{
var targetCells = new List<VoronoiCell>();
for (int i = 0; i < tunnel.Nodes.Count; i++)
{
//a search depth of 2 is large enough to find a cell in almost all maps, but in case it fails, we increase the depth
int searchDepth = 2;
while (searchDepth < 5)
var closestCell = level.GetClosestCell(tunnel.Nodes[i].ToVector2());
if (closestCell != null && !targetCells.Contains(closestCell))
{
int cellIndex = FindCellIndex(tunnel.Nodes[i], cells, cellGrid, gridCellSize, searchDepth);
if (cellIndex > -1)
{
targetCells.Add(cells[cellIndex]);
break;
}
searchDepth++;
targetCells.Add(closestCell);
}
}
tunnel.Cells.AddRange(GeneratePath(targetCells, cells, limits));
tunnel.Cells.AddRange(GeneratePath(targetCells, level.GetAllCells()));
}
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells, Rectangle limits)
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
@@ -460,10 +452,15 @@ namespace Barotrauma
return cellBody;
}
public static List<Vector2> CreateRandomChunk(float radius, int vertexCount, float radiusVariance)
{
Debug.Assert(radiusVariance < radius);
return CreateRandomChunk(radius * 2, radius * 2, vertexCount, radiusVariance);
}
public static List<Vector2> CreateRandomChunk(float width, float height, int vertexCount, float radiusVariance)
{
Debug.Assert(radiusVariance < Math.Min(width, height));
Debug.Assert(vertexCount >= 3);
List<Vector2> verts = new List<Vector2>();
@@ -471,72 +468,12 @@ namespace Barotrauma
float angle = 0.0f;
for (int i = 0; i < vertexCount; i++)
{
verts.Add(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) *
(radius + Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)));
Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server));
angle += angleStep;
}
return verts;
}
/// <summary>
/// find the index of the cell which the point is inside
/// (actually finds the cell whose center is closest, but it's always the correct cell assuming the point is inside the borders of the diagram)
/// </summary>
public static int FindCellIndex(Vector2 position,List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1, Vector2? offset = null)
{
float closestDist = float.PositiveInfinity;
VoronoiCell closestCell = null;
Vector2 gridOffset = offset == null ? Vector2.Zero : (Vector2)offset;
position -= gridOffset;
int gridPosX = (int)Math.Floor(position.X / gridCellSize);
int gridPosY = (int)Math.Floor(position.Y / gridCellSize);
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
{
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
{
for (int i = 0; i < cellGrid[x, y].Count; i++)
{
float dist = Vector2.DistanceSquared(cellGrid[x, y][i].Center, position);
if (dist > closestDist) continue;
closestDist = dist;
closestCell = cellGrid[x, y][i];
}
}
}
return cells.IndexOf(closestCell);
}
public static int FindCellIndex(Point position, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1)
{
int closestDist = int.MaxValue;
VoronoiCell closestCell = null;
int gridPosX = position.X / gridCellSize;
int gridPosY = position.Y / gridCellSize;
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
{
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
{
for (int i = 0; i < cellGrid[x, y].Count; i++)
{
int dist = MathUtils.DistanceSquared(
(int)cellGrid[x, y][i].Site.Coord.X, (int)cellGrid[x, y][i].Site.Coord.Y,
position.X, position.Y);
if (dist > closestDist) continue;
closestDist = dist;
closestCell = cellGrid[x, y][i];
}
}
}
return cells.IndexOf(closestCell);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -29,6 +29,8 @@ namespace Barotrauma
public bool HasBeaconStation;
public bool IsBeaconActive;
public bool HasHuntingGrounds, OriginallyHadHuntingGrounds;
public OutpostGenerationParams ForceOutpostGenerationParams;
public readonly Point Size;
@@ -86,6 +88,9 @@ namespace Barotrauma
HasBeaconStation = element.GetAttributeBool("hasbeaconstation", false);
IsBeaconActive = element.GetAttributeBool("isbeaconactive", false);
HasHuntingGrounds = element.GetAttributeBool("hashuntinggrounds", false);
OriginallyHadHuntingGrounds = element.GetAttributeBool("originallyhadhuntinggrounds", HasHuntingGrounds);
string generationParamsId = element.GetAttributeString("generationparams", "");
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId);
if (GenerationParams == null)
@@ -112,8 +117,7 @@ namespace Barotrauma
EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase))));
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase))));
NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase))));
}
@@ -140,7 +144,13 @@ namespace Barotrauma
var rand = new MTRandom(ToolBox.StringToInt(Seed));
InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble());
HasBeaconStation = rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
//minimum difficulty of the level before hunting grounds can appear
float huntingGroundsDifficultyThreshold = 25;
//probability of hunting grounds appearing in 100% difficulty levels
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
IsBeaconActive = false;
}
@@ -163,7 +173,7 @@ namespace Barotrauma
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
}
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null)
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, bool requireOutpost = false)
{
if (string.IsNullOrEmpty(seed))
{
@@ -172,7 +182,9 @@ namespace Barotrauma
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type;
LevelType type = generationParams == null ?
(requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) :
generationParams.Type;
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
var biome =
@@ -191,7 +203,13 @@ namespace Barotrauma
levelData.HasBeaconStation = beaconRng < 0.5f;
levelData.IsBeaconActive = beaconRng > 0.25f;
}
GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData);
if (GameMain.GameSession?.GameMode != null)
{
foreach (Mission mission in GameMain.GameSession.GameMode.Missions)
{
mission.AdjustLevelData(levelData);
}
}
return levelData;
}
@@ -213,6 +231,17 @@ namespace Barotrauma
new XAttribute("isbeaconactive", IsBeaconActive.ToString()));
}
if (HasHuntingGrounds)
{
newElement.Add(
new XAttribute("hashuntinggrounds", true));
}
if (HasHuntingGrounds || OriginallyHadHuntingGrounds)
{
newElement.Add(
new XAttribute("originallyhadhuntinggrounds", true));
}
if (Type == LevelType.Outpost)
{
if (EventHistory.Any())
@@ -181,6 +181,13 @@ namespace Barotrauma
set;
}
[Serialize(true, true, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable]
public bool CreateHoleToAbyss
{
get;
set;
}
[Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)]
public int LevelObjectAmount
{
@@ -404,7 +411,35 @@ namespace Barotrauma
set;
}
[Serialize(300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
[Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int AbyssIslandCount
{
get;
set;
}
[Serialize("4000,7000", true), Editable]
public Point AbyssIslandSizeMin
{
get;
set;
}
[Serialize("8000,10000", true), Editable]
public Point AbyssIslandSizeMax
{
get;
set;
}
[Serialize(0.5f, true), Editable()]
public float AbyssIslandCaveProbability
{
get;
set;
}
[Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
public int SeaFloorDepth
{
get { return seaFloorBaseDepth; }
@@ -554,7 +589,7 @@ namespace Barotrauma
var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any());
if (biome == null)
{
matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.Any(b => b.IsEndBiome));
matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome));
}
else
{
@@ -101,6 +101,7 @@ namespace Barotrauma
sl.filePath = filePath;
sl.saveElement = doc.Root;
sl.saveElement.Name = "LinkedSubmarine";
sl.saveElement.SetAttributeValue("filepath", filePath);
return sl;
}
@@ -183,10 +184,10 @@ namespace Barotrauma
else
{
string levelSeed = element.GetAttributeString("location", "");
LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.LevelData;
LevelData levelData = GameMain.GameSession?.Campaign?.NextLevel ?? GameMain.GameSession?.LevelData;
linkedSub = new LinkedSubmarine(submarine, idRemap.AssignMaxId())
{
purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles,
purchasedLostShuttles = GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles,
saveElement = element
};
@@ -236,6 +237,11 @@ namespace Barotrauma
DebugConsole.ThrowError("Failed to load a linked submarine (empty XML element). The save file may be corrupted.");
return;
}
if (!info.SubmarineElement.Elements().Any(e => e.Name.ToString().Equals("hull", StringComparison.OrdinalIgnoreCase)))
{
DebugConsole.ThrowError("Failed to load a linked submarine (the submarine contains no hulls).");
return;
}
IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset);
sub = Submarine.Load(info, false, parentRemap);
@@ -297,7 +303,7 @@ namespace Barotrauma
{
originalMyPortID = myPort.Item.ID;
myPort.Undock();
myPort.Undock(applyEffects: false);
myPort.DockingDir = 0;
//something else is already docked to the port this sub should be docked to
@@ -321,8 +327,8 @@ namespace Barotrauma
sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);
myPort.Dock(linkedPort);
myPort.Lock(true);
myPort.Dock(linkedPort);
myPort.Lock(isNetworkMessage: true, applyEffects: false);
}
}
@@ -366,6 +372,11 @@ namespace Barotrauma
}
saveElement.Name = "LinkedSubmarine";
if (saveElement.Attribute("previewimage") != null)
{
saveElement.Attribute("previewimage").Remove();
}
if (saveElement.Attribute("pos") != null) { saveElement.Attribute("pos").Remove(); }
saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)));
@@ -392,14 +403,14 @@ namespace Barotrauma
bool leaveBehind = false;
if (!sub.DockedTo.Contains(Submarine.MainSub))
{
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndPosition || Submarine.MainSub.AtStartPosition);
if (Submarine.MainSub.AtEndPosition)
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit);
if (Submarine.MainSub.AtEndExit)
{
leaveBehind = sub.AtEndPosition != Submarine.MainSub.AtEndPosition;
leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit;
}
else
{
leaveBehind = sub.AtStartPosition != Submarine.MainSub.AtStartPosition;
leaveBehind = sub.AtStartExit != Submarine.MainSub.AtStartExit;
}
}
@@ -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,15 +58,17 @@ namespace Barotrauma
}
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
private string baseName;
private int nameFormatIndex;
private LocationType addInitialMissionsForType;
public bool Discovered;
public readonly Dictionary<LocationTypeChange, int> ProximityTimer = new Dictionary<LocationTypeChange, int>();
public Pair<LocationTypeChange, int> PendingLocationTypeChange;
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
public int LocationTypeChangeCooldown;
public string BaseName { get => baseName; }
@@ -76,12 +80,16 @@ namespace Barotrauma
public LocationType Type { get; private set; }
public LocationType OriginalType { get; private set; }
public LevelData LevelData { get; set; }
public int PortraitId { get; private set; }
public Reputation Reputation { get; set; }
public int TurnsInRadiation { get; set; }
#region Store
private const float StoreMaxReputationModifier = 0.1f;
@@ -168,12 +176,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;
@@ -216,6 +223,8 @@ namespace Barotrauma
public int TimeSinceLastTypeChange;
public bool IsGateBetweenBiomes;
private struct LoadedMission
{
public MissionPrefab MissionPrefab { get; }
@@ -239,38 +248,64 @@ 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 = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
Name = RandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(Name);
Connections = new List<LocationConnection>();
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)
{
//turn lairs into abandoned outposts
if (locationType.Equals("lair", StringComparison.OrdinalIgnoreCase))
{
Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("Abandoned", StringComparison.OrdinalIgnoreCase));
addInitialMissionsForType = Type;
}
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();
}
if (Type != null)
{
element.SetAttributeValue("type", Type.Identifier);
}
typeNotFound = true;
}
string originalLocationType = element.GetAttributeString("originaltype", locationType);
OriginalType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase));
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
Discovered = element.GetAttributeBool("discovered", false);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
TimeSinceLastTypeChange = element.GetAttributeInt("timesincelasttypechange", 0);
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
for (int i = 0; i < Type.CanChangeTo.Count; i++)
if (!typeNotFound)
{
ProximityTimer.Add(Type.CanChangeTo[i], element.GetAttributeInt("proximitytimer" + i, 0));
}
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));
}
}
int locationTypeChangeIndex = element.GetAttributeInt("pendinglocationtypechange", -1);
if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < Type.CanChangeTo.Count - 1)
{
PendingLocationTypeChange = new Pair<LocationTypeChange, int>(
Type.CanChangeTo[locationTypeChangeIndex],
element.GetAttributeInt("pendinglocationtypechangetimer", 0));
LoadLocationTypeChange(element);
}
string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]);
@@ -316,6 +351,42 @@ namespace Barotrauma
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);
if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
{
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex}).");
continue;
}
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)
@@ -335,9 +406,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)
@@ -348,6 +419,16 @@ namespace Barotrauma
Type = newType;
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);
}
@@ -361,6 +442,16 @@ namespace Barotrauma
#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();
#endif
}
public MissionPrefab UnlockMissionByIdentifier(string identifier)
{
if (AvailableMissions.Any(m => m.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))) { return null; }
@@ -399,12 +490,12 @@ 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;
}
MissionPrefab missionPrefab = suitableMissions.GetRandom();
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])))
@@ -428,6 +519,12 @@ namespace Barotrauma
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())
{
@@ -439,10 +536,7 @@ namespace Barotrauma
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);
Location destination = connection.OtherLocation(this);
var mission = prefab.Instantiate(new Location[] { this, destination });
mission.AdjustLevelData(connection.LevelData);
return mission;
return InstantiateMission(prefab, connection);
}
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
@@ -453,26 +547,47 @@ namespace Barotrauma
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;
if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
if (loadedMissions != null && loadedMissions.Any())
{
foreach (LoadedMission loadedMission in loadedMissions)
{
destination = map.Locations[loadedMission.DestinationIndex];
Location destination;
if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
{
destination = map.Locations[loadedMission.DestinationIndex];
}
else
{
destination = Connections.First().OtherLocation(this);
}
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination });
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { SelectedMission = mission; }
}
else
{
destination = Connections.First().OtherLocation(this);
}
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination });
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { SelectedMission = mission; }
loadedMissions = null;
}
if (addInitialMissionsForType != null)
{
if (addInitialMissionsForType.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandom());
}
if (addInitialMissionsForType.MissionTags.Any())
{
UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandom());
}
addInitialMissionsForType = null;
}
loadedMissions = null;
}
/// <summary>
@@ -484,6 +599,33 @@ namespace Barotrauma
SelectedMissionIndex = -1;
}
public bool HasOutpost()
{
if (!Type.HasOutpost) { return false; }
return !IsCriticallyRadiated();
}
public bool IsCriticallyRadiated()
{
if (GameMain.GameSession?.Map?.Radiation != null)
{
return TurnsInRadiation > GameMain.GameSession.Map.Radiation.Params.CriticalRadiationThreshold;
}
return false;
}
public LocationType GetLocationType()
{
if (IsCriticallyRadiated() && LocationType.List.FirstOrDefault(lt => lt.Identifier.Equals(Type.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newLocationType)
{
return newLocationType;
}
return Type;
}
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
{
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
@@ -583,6 +725,8 @@ namespace Barotrauma
}
}
public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this);
private List<PurchasedItem> CreateStoreStock()
{
var stock = new List<PurchasedItem>();
@@ -908,27 +1052,55 @@ namespace Barotrauma
{
var locationElement = new XElement("location",
new XAttribute("type", Type.Identifier),
new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
new XAttribute("basename", BaseName),
new XAttribute("name", Name),
new XAttribute("discovered", Discovered),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange));
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation));
LevelData.Save(locationElement);
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
if (ProximityTimer.ContainsKey(Type.CanChangeTo[i]))
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
{
locationElement.Add(new XAttribute("proximitytimer" + i, ProximityTimer[Type.CanChangeTo[i]]));
if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j]))
{
locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]]));
}
}
}
if (PendingLocationTypeChange != null)
if (PendingLocationTypeChange.HasValue)
{
locationElement.Add(new XAttribute("pendinglocationtypechange", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.First)));
locationElement.Add(new XAttribute("pendinglocationtypechangetimer", PendingLocationTypeChange.Second));
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));
locationElement.Add(changeElement);
}
else
{
int index = Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange);
changeElement.Add(new XAttribute("index", index));
if (index == -1)
{
DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
}
else
{
locationElement.Add(changeElement);
}
}
}
if (LocationTypeChangeCooldown > 0)
{
locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown));
}
if (takenItems.Any())
@@ -987,7 +1159,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),
@@ -14,6 +14,8 @@ namespace Barotrauma
public bool Passed;
public bool Locked;
public LevelData LevelData { get; set; }
public Vector2 CenterPos
@@ -32,6 +34,16 @@ 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)
@@ -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>();
@@ -32,18 +27,24 @@ namespace Barotrauma
public readonly float BeaconStationChance;
public readonly CharacterTeamType OutpostTeam;
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
{
@@ -55,11 +56,11 @@ namespace Barotrauma
get;
private set;
}
public string ReplaceInRadiation { get; }
public Sprite Sprite
{
get { return symbolSprite; }
}
public Sprite Sprite { get; private set; }
public Sprite RadiationSprite { get; }
public Color SpriteColor
{
@@ -79,9 +80,20 @@ 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();
ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), "");
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
try
@@ -135,11 +147,14 @@ 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 "radiationsymbol":
RadiationSprite = new Sprite(subElement, lazyLoad: true);
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);
@@ -8,36 +8,136 @@ 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;
public readonly bool RequireDiscovered;
public List<string> Messages = new List<string>();
/// <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;
public readonly bool RequireDiscovered;
/// <summary>
/// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations
/// </summary>
public readonly float ProximityProbabilityIncrease;
public List<Requirement> Requirements = new List<Requirement>();
public List<string> Messages = new List<string>();
/// <summary>
/// The change can't happen if there's one or more of the given types of locations near this one
@@ -49,41 +149,35 @@ namespace Barotrauma
/// </summary>
public readonly int DisallowedProximity;
/// <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)
public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f)
{
ChangeToType = element.GetAttributeString("type", "");
Probability = element.GetAttributeFloat("probability", 1.0f);
CurrentType = currentType;
ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", ""));
RequireDiscovered = element.GetAttributeBool("requirediscovered", false);
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);
if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f)
{
if (!RequiredLocations.Any())
{
DebugConsole.AddWarning(
$"Invalid location type change in location type \"{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 \"{currentType}\". " +
"Probability is configured to increase when near some other type of location, but the base probability is already 100%");
}
}
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", 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)
{
@@ -95,34 +189,70 @@ namespace Barotrauma
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 (RequiredLocations.Any() && !AnyWithinDistance(location, RequiredProximity, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); }))
{
return 0.0f;
}
if (DisallowedAdjacentLocations.Any() && AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
if (DisallowedAdjacentLocations.Any() &&
AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
{
return 0.0f;
}
float probability = Probability;
if (location.ProximityTimer.ContainsKey(this))
foreach (Requirement requirement in Requirements)
{
if (AnyWithinDistance(location, RequiredProximityForProbabilityIncrease, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); }))
if (requirement.AnyWithinDistance(location, requirement.RequiredProximity))
{
return probability += ProximityProbabilityIncrease * location.ProximityTimer[this];
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;
}
public bool AnyWithinDistance(Location location, int maxDistance, Func<Location, bool> predicate, int currentDistance = 0, HashSet<Location> checkedLocations = null)
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; }
@@ -141,22 +271,5 @@ namespace Barotrauma
return false;
}
private int CountWithinRequiredProximity(Location location, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > RequiredProximityForProbabilityIncrease) { return 0; }
int count = currentDistance > 0 && RequiredLocations.Contains(location.Type.Identifier) ? 1 : 0;
checkedLocations ??= new HashSet<Location>();
checkedLocations.Add(location);
foreach (var connection in location.Connections)
{
var otherLocation = connection.OtherLocation(location);
if (!checkedLocations.Contains(otherLocation)) { count += CountWithinRequiredProximity(otherLocation, currentDistance+1, checkedLocations); }
}
return count;
}
}
}
@@ -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 { get; private set; }
public int Height { get; private set; }
public Action<Location, LocationConnection> OnLocationSelected;
/// <summary>
@@ -56,20 +57,37 @@ namespace Barotrauma
public List<LocationConnection> Connections { get; private set; }
public Map()
public Radiation Radiation;
public Map(CampaignSettings settings)
{
generationParams = MapGenerationParams.Instance;
Width = generationParams.Width;
Height = generationParams.Height;
Locations = new List<Location>();
Connections = new List<LocationConnection>();
if (generationParams.RadiationParams != null)
{
Radiation = new Radiation(this, generationParams.RadiationParams)
{
Enabled = settings.RadiationEnabled
};
}
}
/// <summary>
/// Load a previously saved campaign map from XML
/// </summary>
private Map(CampaignMode campaign, XElement element) : this()
private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings)
{
Seed = element.GetAttributeString("seed", "a");
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
Width = element.GetAttributeInt("width", Width);
Height = element.GetAttributeInt("height", Height);
bool lairsFound = false;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -80,8 +98,15 @@ namespace Barotrauma
{
Locations.Add(null);
}
lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase);
Locations[i] = new Location(subElement);
break;
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement)
{
Enabled = settings.RadiationEnabled
};
break;
}
}
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
@@ -90,6 +115,7 @@ namespace Barotrauma
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
}
List<XElement> connectionElements = new List<XElement>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -100,6 +126,7 @@ namespace Barotrauma
var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
{
Passed = subElement.GetAttributeBool("passed", false),
Locked = subElement.GetAttributeBool("locked", false),
Difficulty = subElement.GetAttributeFloat("difficulty", 0.0f)
};
Locations[locationIndices.X].Connections.Add(connection);
@@ -111,6 +138,7 @@ namespace Barotrauma
LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ??
LevelGenerationParams.GetBiomes().First();
Connections.Add(connection);
connectionElements.Add(subElement);
break;
}
}
@@ -149,13 +177,30 @@ namespace Barotrauma
}
}
//backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds
if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds))
{
for (int i = 0; i < Connections.Count; i++)
{
float maxHuntingGroundsProbability = 0.3f;
Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability;
connectionElements[i].SetAttributeValue("hashuntinggrounds", true);
}
}
//backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml)
float maxX = Locations.Select(l => l.MapPosition.X).Max();
if (maxX > Width) { Width = (int)(maxX + 10); }
float maxY = Locations.Select(l => l.MapPosition.Y).Max();
if (maxY > Height) { Height = (int)(maxY + 10); }
InitProjectSpecific();
}
/// <summary>
/// Generate a new campaign map from the seed
/// </summary>
public Map(CampaignMode campaign, string seed) : this()
public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings)
{
Seed = seed;
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
@@ -255,14 +300,14 @@ namespace Barotrauma
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
@@ -285,20 +330,13 @@ namespace Barotrauma
if (connection2.Locations[1] == connection.Locations[0]) { connection2.Locations[1] = connection.Locations[1]; }
}
}
HashSet<Location> connectedLocations = new HashSet<Location>();
foreach (LocationConnection connection in Connections)
{
connection.Locations[0].Connections.Add(connection);
connection.Locations[1].Connections.Add(connection);
connectedLocations.Add(connection.Locations[0]);
connectedLocations.Add(connection.Locations[1]);
}
//remove orphans
Locations.RemoveAll(c => !connectedLocations.Contains(c));
//remove locations that are too close to each other
float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
for (int i = Locations.Count - 1; i >= 0; i--)
@@ -350,6 +388,58 @@ 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 (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { 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.Type.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase))
{
leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost && !lt.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase)));
}
leftMostLocation.IsGateBetweenBiomes = true;
Connections[i].Locked = true;
}
}
foreach (Location location in Locations)
{
for (int i = location.Connections.Count - 1; i >= 0; i--)
@@ -361,6 +451,9 @@ namespace Barotrauma
}
}
//remove orphans
Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
foreach (LocationConnection connection in Connections)
{
connection.Difficulty = MathHelper.Clamp((connection.CenterPos.X / Width * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f);
@@ -371,7 +464,18 @@ namespace Barotrauma
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location);
location.LevelData = new LevelData(location)
{
Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)
};
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)
{
@@ -381,6 +485,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);
@@ -412,7 +522,7 @@ namespace Barotrauma
{
allowedBiomes.Clear();
allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
float zoneX = Width - zoneWidth * i;
float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
foreach (Location location in Locations)
{
@@ -425,7 +535,7 @@ namespace Barotrauma
foreach (LocationConnection connection in Connections)
{
if (connection.Biome != null) { continue; }
connection.Biome = connection.Locations[0].Biome;
connection.Biome = connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? connection.Locations[0].Biome : connection.Locations[1].Biome;
}
System.Diagnostics.Debug.Assert(Locations.All(l => l.Biome != null));
@@ -558,10 +668,12 @@ namespace Barotrauma
CurrentLocation.CreateStore();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.CampaignMetadata is { } metadata)
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
{
metadata.SetValue("campaign.location.id", CurrentLocationIndex);
metadata.SetValue("campaign.location.name", CurrentLocation.Name);
metadata.SetValue("campaign.location.biome", CurrentLocation.Biome?.Identifier ?? "null");
metadata.SetValue("campaign.location.type", CurrentLocation.Type?.Identifier ?? "null");
}
}
@@ -614,9 +726,14 @@ namespace Barotrauma
}
SelectedLocation = Locations[index];
var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
SelectedConnection =
Connections.Find(c => c.Locations.Contains(GameMain.GameSession?.Campaign?.CurrentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
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());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
@@ -632,12 +749,15 @@ namespace Barotrauma
SelectedLocation = location;
SelectedConnection = 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());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
public void SelectMission(int missionIndex)
{
if (SelectedConnection == null) { return; }
if (CurrentLocation == null)
{
string errorMsg = "Failed to select a mission (current location not set).";
@@ -647,11 +767,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);
@@ -659,7 +786,7 @@ namespace Barotrauma
public void SelectRandomLocation(bool preferUndiscovered)
{
List<Location> nextLocations = CurrentLocation.Connections.Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
@@ -687,6 +814,8 @@ namespace Barotrauma
{
ProgressWorld();
}
Radiation?.OnStep(steps);
}
private void ProgressWorld()
@@ -710,7 +839,7 @@ namespace Barotrauma
continue;
}
if (location == CurrentLocation || location == SelectedLocation) { continue; }
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
ProgressLocationTypeChanges(location);
@@ -724,20 +853,24 @@ namespace Barotrauma
private void ProgressLocationTypeChanges(Location location)
{
location.TimeSinceLastTypeChange++;
location.LocationTypeChangeCooldown--;
if (location.PendingLocationTypeChange != null)
{
if (location.PendingLocationTypeChange.First.DetermineProbability(location) <= 0.0f)
if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
{
//remove pending type change if it's no longer allowed
location.PendingLocationTypeChange = null;
}
else
{
location.PendingLocationTypeChange.Second--;
if (location.PendingLocationTypeChange.Second <= 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.First);
ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
}
return;
}
@@ -764,9 +897,10 @@ namespace Barotrauma
{
if (selectedTypeChange.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange = new Pair<LocationTypeChange, int>(
selectedTypeChange,
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y));
location.PendingLocationTypeChange =
(selectedTypeChange,
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
null);
}
else
{
@@ -778,20 +912,19 @@ namespace Barotrauma
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
{
if (typeChange.AnyWithinDistance(
location,
typeChange.RequiredProximityForProbabilityIncrease,
(otherLocation) => { return typeChange.RequiredLocations.Contains(otherLocation.Type.Identifier); }))
foreach (var requirement in typeChange.Requirements)
{
if (!location.ProximityTimer.ContainsKey(typeChange)) { location.ProximityTimer[typeChange] = 0; }
location.ProximityTimer[typeChange] += 1;
}
else
{
location.ProximityTimer.Remove(typeChange);
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
{
if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
location.ProximityTimer[requirement] += 1;
}
else
{
location.ProximityTimer.Remove(requirement);
}
}
}
}
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
@@ -844,8 +977,12 @@ namespace Barotrauma
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationTypeProjSpecific(location, prevName, change);
location.ProximityTimer.Remove(change);
foreach (var requirement in change.Requirements)
{
location.ProximityTimer.Remove(requirement);
}
location.TimeSinceLastTypeChange = 0;
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
location.PendingLocationTypeChange = null;
}
@@ -856,9 +993,9 @@ namespace Barotrauma
/// <summary>
/// Load a previously saved map from an xml element
/// </summary>
public static Map Load(CampaignMode campaign, XElement element)
public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings)
{
Map map = new Map(campaign, element);
Map map = new Map(campaign, element, settings);
map.LoadState(element, false);
#if CLIENT
map.DrawOffset = -map.CurrentLocation.MapPosition;
@@ -889,16 +1026,12 @@ namespace Barotrauma
location.ProximityTimer.Clear();
for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
{
location.ProximityTimer.Add(location.Type.CanChangeTo[i], subElement.GetAttributeInt("changetimer" + i, 0));
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));
}
}
int locationTypeChangeIndex = subElement.GetAttributeInt("pendinglocationtypechange", -1);
if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < location.Type.CanChangeTo.Count - 1)
{
location.PendingLocationTypeChange = new Pair<LocationTypeChange, int>(
location.Type.CanChangeTo[locationTypeChangeIndex],
subElement.GetAttributeInt("pendinglocationtypechangetimer", 0));
}
location.TimeSinceLastTypeChange = subElement.GetAttributeInt("timesincelasttypechange", 0);
location.LoadLocationTypeChange(subElement);
location.Discovered = subElement.GetAttributeBool("discovered", false);
if (location.Discovered)
{
@@ -933,6 +1066,10 @@ namespace Barotrauma
case "connection":
int connectionIndex = subElement.GetAttributeInt("i", 0);
Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false);
break;
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
break;
}
}
@@ -945,6 +1082,7 @@ namespace Barotrauma
int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1);
if (currentLocationConnection >= 0)
{
Connections[currentLocationConnection].Locked = false;
SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation));
}
else
@@ -975,6 +1113,8 @@ namespace Barotrauma
mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData == Level.Loaded.LevelData))));
}
}
mapElement.Add(new XAttribute("width", Width));
mapElement.Add(new XAttribute("height", Height));
mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex));
mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation)));
mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation)));
@@ -993,6 +1133,7 @@ namespace Barotrauma
var connectionElement = new XElement("connection",
new XAttribute("passed", connection.Passed),
new XAttribute("locked", connection.Locked),
new XAttribute("difficulty", connection.Difficulty),
new XAttribute("biome", connection.Biome.Identifier),
new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1])));
@@ -1000,6 +1141,11 @@ namespace Barotrauma
mapElement.Add(connectionElement);
}
if (Radiation != null)
{
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,155 @@
#nullable enable
using System;
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; }
[Serialize(defaultValue: true, isSaveable: true)]
public bool Enabled { 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 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; }
float increaseAmount = Params.RadiationStep * steps;
if (Params.MaxRadiation > 0 && Params.MaxRadiation < Amount + increaseAmount)
{
increaseAmount = Params.MaxRadiation - Amount;
}
IncreaseRadiation(increaseAmount);
int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated());
foreach (Location location in Map.Locations.Where(Contains))
{
if (location.IsGateBetweenBiomes)
{
location.Connections.ForEach(c => c.Locked = false);
continue;
}
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,50 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
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; }
[Serialize(defaultValue: -1.0f, isSaveable: false, "Maximum amount of radiation.")]
public float MaxRadiation { get; set; }
[Serialize(defaultValue: "139,0,0,85", isSaveable: false, "The color of the radiated area.")]
public Color RadiationAreaColor { get; set; }
[Serialize(defaultValue: "255,0,0,255", isSaveable: false, "The tint of the radiation border sprites.")]
public Color RadiationBorderTint { get; set; }
[Serialize(defaultValue: 16.66f, isSaveable: false, "Speed of the border spritesheet animation.")]
public float BorderAnimationSpeed { get; set; }
public RadiationParams(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
}
@@ -3,14 +3,23 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
[Flags]
enum MapEntityCategory
{
Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, Wrecked = 256, Thalamus = 512, ItemAssembly = 1024, Legacy = 2048
Structure = 1,
Decorative = 2,
Machine = 4,
Equipment = 8,
Electrical = 16,
Material = 32,
Misc = 64,
Alien = 128,
Wrecked = 256,
ItemAssembly = 512,
Legacy = 1024
}
abstract partial class MapEntityPrefab : IPrefab, IDisposable
@@ -54,6 +63,7 @@ namespace Barotrauma
//is it possible to stretch the entity horizontally/vertically
[Serialize(false, false)]
public bool ResizeHorizontal { get; protected set; }
[Serialize(false, false)]
public bool ResizeVertical { get; protected set; }
@@ -118,6 +128,9 @@ namespace Barotrauma
[Serialize(false, false)]
public bool HideInMenus { get; set; }
[Serialize("", false)]
public string Subcategory { get; set; }
[Serialize(false, false)]
public bool Linkable
{
@@ -215,6 +228,11 @@ namespace Barotrauma
return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(",");
}
public bool HasSubCategory(string subcategory)
{
return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false;
}
protected virtual void CreateInstance(Rectangle rect)
{
if (constructor == null) return;
@@ -39,6 +39,37 @@ namespace Barotrauma
set;
}
[Serialize(false, isSaveable: true), Editable]
public bool AlwaysDestructible
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
public bool AlwaysRewireable
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
public bool AllowStealing
{
get;
set;
}
[Serialize(true, isSaveable: true), Editable]
public bool SpawnCrewInsideOutpost
{
get;
set;
}
[Serialize("", isSaveable: true), Editable]
public string ReplaceInRadiation { get; set; }
private readonly Dictionary<string, int> moduleCounts = new Dictionary<string, int>();
public IEnumerable<KeyValuePair<string, int>> ModuleCounts
@@ -68,6 +68,15 @@ namespace Barotrauma
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false)
{
var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule);
if (location != null)
{
if (location.IsCriticallyRadiated() && OutpostGenerationParams.Params.FirstOrDefault(p => p.Identifier.Equals(generationParams.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newParams)
{
generationParams = newParams;
}
locationType = location.GetLocationType();
}
//load the infos of the outpost module files
List<SubmarineInfo> outpostModules = new List<SubmarineInfo>();
@@ -169,6 +178,7 @@ namespace Barotrauma
Type = SubmarineType.Outpost
};
generationFailed = false;
outpostInfo.OutpostGenerationParams = generationParams;
sub = new Submarine(outpostInfo, loadEntities: loadEntities);
sub.Info.OutpostGenerationParams = generationParams;
if (!generationFailed)
@@ -669,10 +679,15 @@ namespace Barotrauma
if (availableModules.Count() == 0) { return null; }
var modulesSuitableForLocationType =
availableModules.Where(m =>
!m.OutpostModuleInfo.AllowedLocationTypes.Any() ||
m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
{
@@ -705,10 +720,15 @@ namespace Barotrauma
if (availableModules.Count() == 0) { return null; }
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m =>
!m.OutpostModuleInfo.AllowedLocationTypes.Any() ||
m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
{
@@ -963,7 +983,7 @@ namespace Barotrauma
var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath, -1);
//remove items that don't fit in the hallway
moduleEntities.Where(e => e is Item item && item.GetComponent<Door>() == null && e.Rect.Width > hallwayLength).ForEach(e => e.Remove());
moduleEntities.Where(e => e is Item item && item.GetComponent<Door>() == null && (isHorizontal ? e.Rect.Width : e.Rect.Height) > hallwayLength).ForEach(e => e.Remove());
//find the largest hull to use it as the center point of the hallway
//and the bounds of all the hulls, used when resizing the hallway to fit between the modules
@@ -1036,11 +1056,11 @@ namespace Barotrauma
}
}
}
else if (me is Structure structure)
else if (me is Structure || (me is Item item && item.GetComponent<Door>() == null))
{
if (isHorizontal)
{
if (!structure.ResizeHorizontal)
if (!me.ResizeHorizontal)
{
int xPos = (int)(leftHull.WorldRect.Right + (me.WorldPosition.X - hullBounds.X) * scaleFactor);
me.Rect = new Rectangle(xPos - me.RectWidth / 2, me.Rect.Y, me.Rect.Width, me.Rect.Height);
@@ -1054,9 +1074,9 @@ namespace Barotrauma
}
else
{
if (!structure.ResizeVertical)
if (!me.ResizeVertical)
{
int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.X - hullBounds.Bottom) * scaleFactor);
int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.Y - hullBounds.Bottom) * scaleFactor);
me.Rect = new Rectangle(me.Rect.X, yPos + me.RectHeight / 2, me.Rect.Width, me.Rect.Height);
}
else
@@ -1084,8 +1104,35 @@ namespace Barotrauma
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
continue;
}
startWaypoint.linkedTo.Add(endWaypoint);
endWaypoint.linkedTo.Add(startWaypoint);
if (startWaypoint.WorldPosition.X > endWaypoint.WorldPosition.X)
{
var temp = startWaypoint;
startWaypoint = endWaypoint;
endWaypoint = temp;
}
if (hallwayLength > 100 && isHorizontal)
{
WayPoint prevWayPoint = startWaypoint;
for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f)
{
var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub);
prevWayPoint.linkedTo.Add(newWayPoint);
newWayPoint.linkedTo.Add(prevWayPoint);
prevWayPoint = newWayPoint;
}
if (prevWayPoint != null)
{
prevWayPoint.linkedTo.Add(endWaypoint);
endWaypoint.linkedTo.Add(prevWayPoint);
}
}
else
{
startWaypoint.linkedTo.Add(endWaypoint);
endWaypoint.linkedTo.Add(startWaypoint);
}
WayPoint closestWaypoint = null;
float closestDistSqr = 30.0f * 30.0f;
@@ -1381,10 +1428,9 @@ namespace Barotrauma
Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name));
ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags());
if (gotoTarget == null)
{
gotoTarget = outpost.GetHulls(true).GetRandom();
gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.Server);
}
characterInfo.TeamID = CharacterTeamType.FriendlyNPC;
var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
@@ -1406,27 +1452,11 @@ namespace Barotrauma
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server);
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
{
item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing;
item.SpawnedInOutpost = true;
}
npc.GiveIdCardTags(gotoTarget as WayPoint);
if (npc.AIController is HumanAIController humanAI)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None)
{
idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull;
idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(gotoTarget);
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, humanPrefab.CampaignInteractionType);
}
else
{
idleObjective.Behavior = humanPrefab.Behavior;
foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
}
humanPrefab.InitializeCharacter(npc, gotoTarget);
}
}
}
@@ -20,15 +20,20 @@ namespace Barotrauma
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public readonly bool CanBeSpecial;
/// <summary>
/// The item isn't available in stores unless the level's difficulty is above this value
/// </summary>
public readonly int MinLevelDifficulty;
/// <summary>
/// Support for the old style of determining item prices
/// when there were individual Price elements for each location type
/// where the item was for sale.
/// </summary>
public PriceInfo (XElement element)
public PriceInfo(XElement element)
{
Price = element.GetAttributeInt("buyprice", 0);
MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
CanBeBought = true;
var minAmount = GetMinAmount(element);
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
@@ -37,13 +42,14 @@ namespace Barotrauma
MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount);
}
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true)
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0)
{
Price = price;
CanBeBought = canBeBought;
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, minAmount);
MinLevelDifficulty = minLevelDifficulty;
CanBeSpecial = canBeSpecial;
}
@@ -54,6 +60,7 @@ namespace Barotrauma
var soldByDefault = element.GetAttributeBool("soldbydefault", true);
var minAmount = GetMinAmount(element);
var maxAmount = GetMaxAmount(element);
var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
var canBeSpecial = element.GetAttributeBool("canbespecial", true);
var priceInfos = new List<Tuple<string, PriceInfo>>();
@@ -65,14 +72,16 @@ namespace Barotrauma
new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold,
minAmount: sold ? GetMinAmount(childElement, minAmount) : 0,
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial: canBeSpecial)));
canBeSpecial,
childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty))));
}
var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
minAmount: canBeBoughtAtOtherLocations ? minAmount : 0,
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial: canBeSpecial);
canBeSpecial,
minLevelDifficulty);
return priceInfos;
}
@@ -637,7 +637,7 @@ namespace Barotrauma
Vector2 bodyPos = WorldPosition + BodyOffset;
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, MathHelper.ToDegrees(BodyRotation));
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation);
return
Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
@@ -819,16 +819,12 @@ namespace Barotrauma
}
for (int i = 1; i <= particleAmount; i++)
{
var worldRect = section.WorldRect;
Vector2 particlePos = new Vector2(
Rand.Range(section.rect.X, section.rect.Right),
Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y));
Rand.Range(worldRect.X, worldRect.Right),
Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y));
if (Submarine != null)
{
particlePos += Submarine.DrawPosition;
}
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)));
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f);
if (particle == null) break;
}
}
@@ -360,7 +360,8 @@ namespace Barotrauma
}
}
if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category))
string categoryStr = element.GetAttributeString("category", "Structure");
if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category))
{
category = MapEntityCategory.Structure;
}
@@ -419,6 +420,13 @@ namespace Barotrauma
}
}
//backwards compatibility
if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase))
{
sp.Category = MapEntityCategory.Wrecked;
sp.Subcategory = "Thalamus";
}
if (string.IsNullOrEmpty(sp.identifier))
{
DebugConsole.ThrowError(
@@ -133,7 +133,7 @@ namespace Barotrauma
public Rectangle Borders
{
get
get
{
return subBody == null ? Rectangle.Empty : subBody.Borders;
}
@@ -155,7 +155,7 @@ namespace Barotrauma
private float? realWorldCrushDepth;
public float RealWorldCrushDepth
{
get
get
{
if (!realWorldCrushDepth.HasValue)
{
@@ -165,13 +165,11 @@ namespace Barotrauma
if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; }
realWorldCrushDepth = Math.Min(structure.CrushDepth, realWorldCrushDepth.Value);
}
if (Info.SubmarineClass == SubmarineClass.DeepDiver)
{
realWorldCrushDepth *= 1.2f;
}
realWorldCrushDepth *= Info.GetRealWorldCrushDepthMultiplier();
}
return realWorldCrushDepth.Value;
}
set { realWorldCrushDepth = value; }
}
/// <summary>
@@ -179,34 +177,30 @@ namespace Barotrauma
/// </summary>
public float RealWorldDepth
{
get
get
{
if (Level.Loaded?.GenerationParams == null)
{
return -WorldPosition.Y * Physics.DisplayToRealWorldRatio;
}
else if (GameMain.GameSession?.Campaign == null)
{
return (-(WorldPosition.Y - Level.Loaded.GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio;
}
return Level.Loaded.GetRealWorldDepth(WorldPosition.Y);
}
}
public bool AtEndPosition
public bool AtEndExit
{
get
get
{
if (Level.Loaded == null) { return false; }
if (Level.Loaded.EndOutpost != null && DockedTo.Contains(Level.Loaded.EndOutpost))
{
return true;
}
return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndPosition) < Level.ExitDistance * Level.ExitDistance);
return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance);
}
}
public bool AtStartPosition
public bool AtStartExit
{
get
{
@@ -215,7 +209,7 @@ namespace Barotrauma
{
return true;
}
return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartPosition) < Level.ExitDistance * Level.ExitDistance);
return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartExitPosition) < Level.ExitDistance * Level.ExitDistance);
}
}
@@ -251,10 +245,10 @@ namespace Barotrauma
public bool AtDamageDepth
{
get
get
{
if (Level.Loaded == null || subBody == null) { return false; }
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth & RealWorldDepth > RealWorldCrushDepth;
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth && RealWorldDepth > RealWorldCrushDepth;
}
}
@@ -324,6 +318,7 @@ namespace Barotrauma
{
Info.Type = SubmarineType.Wreck;
ShowSonarMarker = false;
DockedTo.ForEach(s => s.ShowSonarMarker = false);
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = CharacterTeamType.None;
@@ -333,7 +328,7 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.Submarine != this) { continue; }
if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard")
if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard")
{
foreach (string tag in item.GetTags().ToList())
{
@@ -341,7 +336,7 @@ namespace Barotrauma
string newTag = Level.Loaded.GetWreckIDTag(tag, this);
item.ReplaceTag(tag, newTag);
ReplaceIDCardTagRequirements(tag, newTag);
}
}
}
}
@@ -455,7 +450,7 @@ namespace Barotrauma
public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f, int verticalMoveDir = 0)
{
Rectangle dockedBorders = GetDockedBorders();
Vector2 diffFromDockedBorders =
Vector2 diffFromDockedBorders =
new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)
- new Vector2(Borders.Center.X, Borders.Y - Borders.Height / 2);
@@ -506,7 +501,7 @@ namespace Barotrauma
(e.Point1.Y > refPos.Y + minHeight * 0.5f && e.Point2.Y > refPos.Y + minHeight * 0.5f))
{
continue;
}
}
if (cell.Site.Coord.X < refPos.X)
{
@@ -553,7 +548,7 @@ namespace Barotrauma
//walls found at both sides, use their midpoint
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
}
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
return spawnPos - diffFromDockedBorders;
}
@@ -587,18 +582,31 @@ namespace Barotrauma
{
if (e is Item item)
{
if (item.GetComponent<Turret>() != null) { return false; }
if (item.body != null && !item.body.Enabled) { return true; }
}
if (e.HiddenInGame) { return true; }
return false;
});
if (entities.Count == 0) return Rectangle.Empty;
if (entities.Count == 0) { return Rectangle.Empty; }
float minX = entities[0].Rect.X, minY = entities[0].Rect.Y - entities[0].Rect.Height;
float maxX = entities[0].Rect.Right, maxY = entities[0].Rect.Y;
for (int i = 1; i < entities.Count; i++)
{
if (entities[i] is Item item)
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
minX = Math.Min(minX, entities[i].Rect.X + turret.TransformedBarrelPos.X * 2f);
minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height - turret.TransformedBarrelPos.Y * 2f);
maxX = Math.Max(maxX, entities[i].Rect.Right + turret.TransformedBarrelPos.X * 2f);
maxY = Math.Max(maxY, entities[i].Rect.Y - turret.TransformedBarrelPos.Y * 2f);
}
}
minX = Math.Min(minX, entities[i].Rect.X);
minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height);
maxX = Math.Max(maxX, entities[i].Rect.Right);
@@ -607,7 +615,7 @@ namespace Barotrauma
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
}
public static Rectangle AbsRect(Vector2 pos, Vector2 size)
{
if (size.X < 0.0f)
@@ -620,7 +628,7 @@ namespace Barotrauma
pos.Y -= size.Y;
size.Y = -size.Y;
}
return new Rectangle((int)pos.X, (int)pos.Y, (int)size.X, (int)size.Y);
}
@@ -674,7 +682,7 @@ namespace Barotrauma
closestFraction = 0.0f;
closestNormal = Vector2.Normalize(rayEnd - rayStart);
if (fixture.Body != null) closestBody = fixture.Body;
if (fixture.Body != null) closestBody = fixture.Body;
return false;
}, ref aabb);
if (closestFraction <= 0.0f)
@@ -685,7 +693,7 @@ namespace Barotrauma
return closestBody;
}
}
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return -1; }
@@ -702,7 +710,7 @@ namespace Barotrauma
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFraction = closestFraction;
lastPickedNormal = closestNormal;
return closestBody;
}
@@ -827,13 +835,13 @@ namespace Barotrauma
lastPickedPosition = rayEnd;
return null;
}
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
if (fixture == null) { return -1; }
if (ignoreSensors && fixture.IsSensor) { return -1; }
if (ignoreLevel && fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionWall)
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; }
if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; }
@@ -880,7 +888,7 @@ namespace Barotrauma
parents.Add(this);
flippedX = !flippedX;
Item.UpdateHulls();
List<Item> bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null);
@@ -1049,6 +1057,7 @@ namespace Barotrauma
}
steering.MaintainPos = true;
steering.PosToMaintain = WorldPosition;
steering.AutoPilot = true;
#if SERVER
steering.UnsentChanges = true;
@@ -1137,7 +1146,7 @@ namespace Barotrauma
{
if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port))
{
port.Undock();
port.Undock(applyEffects: false);
continue;
}
}
@@ -1164,7 +1173,8 @@ namespace Barotrauma
subBody.SetPosition(subBody.Position + amount);
}
public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true)
/// <param name="teamType">If has value, the sub must match the team type.</param>
public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true, bool ignoreRespawnShuttle = false, CharacterTeamType? teamType = null)
{
Submarine closest = null;
float closestDist = 0.0f;
@@ -1172,6 +1182,11 @@ namespace Barotrauma
{
if (ignoreOutposts && sub.Info.IsOutpost) { continue; }
if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (ignoreRespawnShuttle)
{
if (sub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
}
if (teamType.HasValue && sub.TeamID != teamType) { continue; }
float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition);
if (closest == null || dist < closestDist)
{
@@ -1334,14 +1349,19 @@ namespace Barotrauma
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = CharacterTeamType.FriendlyNPC;
bool indestructible =
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
foreach (MapEntity me in MapEntity.mapEntityList)
{
if (me.Submarine != this) { continue; }
if (me is Item item)
{
item.SpawnedInOutpost = true;
if (item.GetComponent<Repairable>() != null &&
(GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts))
item.SpawnedInOutpost = info.OutpostGenerationParams != null;
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
if (item.GetComponent<Repairable>() != null && indestructible)
{
item.Indestructible = true;
}
@@ -1350,7 +1370,10 @@ namespace Barotrauma
if (ic is ConnectionPanel connectionPanel)
{
//prevent rewiring
connectionPanel.Locked = true;
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
{
connectionPanel.Locked = true;
}
}
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
{
@@ -1363,9 +1386,9 @@ namespace Barotrauma
}
}
}
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts)
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
{
structure.Indestructible = GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts;
structure.Indestructible = true;
}
}
}
@@ -1415,7 +1438,7 @@ namespace Barotrauma
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
if (showWarningMessages &&
!string.IsNullOrEmpty(Info.FilePath) &&
Screen.Selected != GameMain.SubEditorScreen &&
Screen.Selected != GameMain.SubEditorScreen &&
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
{
DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. "
@@ -1428,6 +1451,7 @@ namespace Barotrauma
if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f);
}
}
GenerateOutdoorNodes();
}
protected override ushort DetermineID(ushort id, Submarine submarine)
@@ -1483,7 +1507,7 @@ namespace Barotrauma
element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax));
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? ""));
element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages)));
if (Info.Type == SubmarineType.OutpostModule)
{
Info.OutpostModuleInfo?.Save(element);
@@ -1589,7 +1613,7 @@ namespace Barotrauma
PhysicsBody.RemoveAll();
GameMain.World.Clear();
GameMain.World.Clear();
Unloading = false;
}
@@ -1634,12 +1658,18 @@ namespace Barotrauma
{
if (outdoorNodes == null)
{
outdoorNodes = PathNode.GenerateNodes(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path && wp.Submarine == this && wp.CurrentHull == null));
GenerateOutdoorNodes();
}
return outdoorNodes;
}
}
private void GenerateOutdoorNodes()
{
var waypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path && wp.Submarine == this && wp.CurrentHull == null);
outdoorNodes = PathNode.GenerateNodes(waypoints, removeOrphans: false);
}
private readonly Dictionary<Submarine, HashSet<PathNode>> obstructedNodes = new Dictionary<Submarine, HashSet<PathNode>>();
/// <summary>
@@ -1707,7 +1737,6 @@ namespace Barotrauma
}
}
}
node.Waypoint.FindHull();
}
}
@@ -1722,7 +1751,8 @@ namespace Barotrauma
nodes.Clear();
obstructedNodes.Remove(otherSub);
}
OutdoorNodes.ForEach(n => n.Waypoint.FindHull());
}
public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull());
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
@@ -449,12 +450,21 @@ namespace Barotrauma
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
#endif
if (Level.Loaded == null) { return; }
float submarineDepth = submarine.RealWorldDepth;
if (!Submarine.AtDamageDepth) { 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)
{
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)
{
if (wall.Submarine != submarine) { continue; }
@@ -462,12 +472,14 @@ namespace Barotrauma
float wallCrushDepth = wall.CrushDepth;
if (submarine.Info.SubmarineClass == SubmarineClass.DeepDiver) { wallCrushDepth *= 1.2f; }
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
if (pastCrushDepth < 0) { return; }
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f);
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, Math.Min(pastCrushDepth * 0.001f, 50.0f));
}
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f));
}
}
depthDamageTimer = 10.0f;
@@ -558,19 +570,29 @@ namespace Barotrauma
{
if (limb?.body?.FarseerBody == null || limb.character == null) { return; }
if (limb.Mass > MinImpactLimbMass)
float impactMass = limb.Mass;
var enemyAI = limb.character.AIController as EnemyAIController;
float attackMultiplier = 1.0f;
if (enemyAI?.ActiveAttack != null)
{
impactMass = Math.Max(Math.Max(limb.Mass, limb.character.AnimController.MainLimb.Mass), limb.character.AnimController.Collider.Mass);
attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier;
}
if (impactMass * attackMultiplier > MinImpactLimbMass)
{
Vector2 normal =
Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
Vector2.UnitY :
Vector2.Normalize(Body.SimPosition - limb.SimPosition);
float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1);
float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(impactMass / 300.0f, 1);
impact *= attackMultiplier;
ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
foreach (Submarine dockedSub in submarine.DockedTo)
{
dockedSub.SubBody.ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
dockedSub.SubBody.ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
}
}
@@ -803,7 +825,7 @@ namespace Barotrauma
#if CLIENT
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = impact * 2.0f;
GameMain.GameScreen.Cam.Shake = impact * 10.0f;
if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player))
{
float angularVelocity =
@@ -817,34 +839,41 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != submarine) { continue; }
if (c.KnockbackCooldownTimer > 0.0f) { continue; }
c.KnockbackCooldownTimer = Character.KnockbackCooldown;
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f);
}
c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
bool holdingOntoSomething = false;
if (c.SelectedConstruction != null)
{
var controller = c.SelectedConstruction.GetComponent<Items.Components.Controller>();
holdingOntoSomething = controller != null && controller.LimbPositions.Any();
holdingOntoSomething =
c.SelectedConstruction.GetComponent<Ladder>() != null ||
(c.SelectedConstruction.GetComponent<Controller>()?.LimbPositions.Any() ?? false);
}
//stun for up to 1 second if the impact equal or higher to the maximum impact
if (impact >= MaxCollisionImpact && !holdingOntoSomething)
if (!holdingOntoSomething)
{
c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f));
c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
//stun for up to 2 second if the impact equal or higher to the maximum impact
if (impact >= MaxCollisionImpact)
{
c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(3.0f).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true);
}
}
}
foreach (Item item in Item.ItemList)
{
if (item.Submarine != submarine || item.CurrentHull == null ||
item.body == null || !item.body.Enabled) continue;
if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; }
item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f);
item.PositionUpdateInterval = 0.0f;
}
float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f;
@@ -95,8 +95,9 @@ namespace Barotrauma
public OutpostModuleInfo OutpostModuleInfo { get; set; }
public bool IsOutpost => Type == SubmarineType.Outpost;
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
public bool IsWreck => Type == SubmarineType.Wreck;
public bool IsBeacon => Type == SubmarineType.BeaconStation;
public bool IsPlayer => Type == SubmarineType.Player;
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
@@ -274,7 +275,7 @@ namespace Barotrauma
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
}
#if CLIENT
PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage.Texture, null, null) : null;
PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null;
#endif
}
@@ -463,6 +464,49 @@ 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()
{
if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; }
bool structureCrushDepthsDefined = false;
float realWorldCrushDepth = float.PositiveInfinity;
foreach (var structureElement in SubmarineElement.GetChildElements("structure"))
{
string name = structureElement.Attribute("name")?.Value ?? "";
string identifier = structureElement.GetAttributeString("identifier", "");
var structurePrefab = Structure.FindPrefab(name, identifier);
if (structurePrefab == null || !structurePrefab.Body) { continue; }
if (!structureCrushDepthsDefined && structureElement.Attribute("crushdepth") != null)
{
structureCrushDepthsDefined = true;
}
float structureCrushDepth = structureElement.GetAttributeFloat("crushdepth", float.PositiveInfinity);
realWorldCrushDepth = Math.Min(structureCrushDepth, realWorldCrushDepth);
}
if (!structureCrushDepthsDefined)
{
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
}
realWorldCrushDepth *= GetRealWorldCrushDepthMultiplier();
return realWorldCrushDepth;
}
/// <summary>
/// Based on <see cref="SubmarineClass"/>
/// </summary>
public float GetRealWorldCrushDepthMultiplier()
{
if (SubmarineClass == SubmarineClass.DeepDiver)
{
return 1.2f;
}
else
{
return 1.0f;
}
}
//saving/loading ----------------------------------------------------
public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null)
@@ -11,7 +11,9 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 3, Corpse = 4 };
[Flags]
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 };
partial class WayPoint : MapEntity
{
public static List<WayPoint> WayPointList = new List<WayPoint>();
@@ -142,7 +144,7 @@ namespace Barotrauma
DebugConsole.Log("Created waypoint (" + ID + ")");
CurrentHull = Hull.FindHull(WorldPosition);
FindHull();
}
public override MapEntity Clone()
@@ -784,12 +786,19 @@ namespace Barotrauma
public void FindHull()
{
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
#if CLIENT
//we may not be able to find the hull with the optimized method in the sub editor if new hulls have been added, use the unoptimized method
if (Screen.Selected == GameMain.SubEditorScreen)
{
CurrentHull ??= Hull.FindHullUnoptimized(WorldPosition);
}
#endif
}
public override void OnMapLoaded()
{
InitializeLinks();
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
FindHull();
FindStairs();
}