v1.4.4.1 (Blood in the Water Update)

This commit is contained in:
Regalis11
2024-04-24 18:09:05 +03:00
parent 89b91d1c3e
commit ff1b8951a7
397 changed files with 15250 additions and 6479 deletions
@@ -1009,8 +1009,8 @@ namespace Barotrauma.MapCreatures.Behavior
Body branchBody = GameMain.World.CreateRectangle(ConvertUnits.ToSimUnits(rect.Width * scale), ConvertUnits.ToSimUnits(rect.Height * scale), 1.5f);
branchBody.BodyType = BodyType.Static;
branchBody.UserData = branch;
branchBody.SetCollidesWith(Physics.CollisionRepair);
branchBody.SetCollisionCategories(Physics.CollisionRepair);
branchBody.SetCollidesWith(Physics.CollisionRepairableWall);
branchBody.SetCollisionCategories(Physics.CollisionRepairableWall);
branchBody.Position = ConvertUnits.ToSimUnits(pos);
branchBody.Enabled = HasBrokenThrough;
@@ -54,6 +54,9 @@ namespace Barotrauma
public AITarget AiTarget => aiTarget;
/// <summary>
/// Indetectable characters can't be spotted by AIs and aren't visible on the sonar or health scanner.
/// </summary>
public bool InDetectable
{
get
@@ -35,7 +35,7 @@ namespace Barotrauma
/// 10% of the range if showEffects is true, 0 otherwise.
/// </override>
/// </doc>
private readonly float cameraShake;
public float CameraShake { get; set; }
/// <summary>
/// How far away does the camera shake effect reach.
@@ -45,7 +45,7 @@ namespace Barotrauma
/// Same as attack range if showEffects is true, 0 otherwise.
/// </override>
/// </doc>
private readonly float cameraShakeRange;
public float CameraShakeRange { get; set; }
/// <summary>
/// Color tint to apply to the player's screen when in range of the explosion.
@@ -173,6 +173,11 @@ namespace Barotrauma
/// </summary>
public bool OnlyOutside;
/// <summary>
/// Should the normal damage sounds be played when the explosion damages something. Usually disabled.
/// </summary>
public bool PlayDamageSounds;
/// <summary>
/// How much the explosion repairs items.
/// </summary>
@@ -239,6 +244,8 @@ namespace Barotrauma
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow);
PlayDamageSounds = element.GetAttributeBool(nameof(PlayDamageSounds), false);
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f);
@@ -247,8 +254,8 @@ namespace Barotrauma
decal = element.GetAttributeString("decal", "");
decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize");
cameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f);
cameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f);
CameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f);
CameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f);
screenColorRange = element.GetAttributeFloat("screencolorrange", showEffects ? Attack.Range * 0.1f : 0f);
screenColor = element.GetAttributeColor("screencolor", Color.Transparent);
@@ -301,7 +308,7 @@ namespace Barotrauma
Vector2 cameraPos = GameMain.GameScreen.Cam.Position;
float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f;
GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f);
GameMain.GameScreen.Cam.Shake = CameraShake * Math.Max((CameraShakeRange - cameraDist) / CameraShakeRange, 0.0f);
#if CLIENT
if (screenColor != Color.Transparent)
{
@@ -318,9 +325,12 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f))
{
RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker,
IgnoredSubmarines,
RangedStructureDamage(worldPosition, displayRange,
Attack.GetStructureDamage(1.0f),
Attack.GetLevelWallDamage(1.0f),
attacker, IgnoredSubmarines,
Attack.EmitStructureDamageParticles,
Attack.CreateWallDamageProjectiles,
DistanceFalloff);
}
@@ -455,6 +465,7 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (attack.OnlyHumans && !c.IsHuman) { continue; }
if (IgnoredCharacters.Contains(c)) { continue; }
if (!c.Enabled ||
@@ -485,6 +496,8 @@ namespace Barotrauma
Dictionary<Limb, float> damages = new Dictionary<Limb, float>();
List<Affliction> modifiedAfflictions = new List<Affliction>();
Limb closestLimb = null;
float closestDistFactor = 0;
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; }
@@ -511,6 +524,11 @@ namespace Barotrauma
if (distFactor > 0)
{
distFactors.Add(limb, distFactor);
if (distFactor > closestDistFactor)
{
closestLimb = limb;
closestDistFactor = distFactor;
}
}
}
@@ -558,7 +576,11 @@ namespace Barotrauma
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
Vector2 dir = worldPosition - limb.WorldPosition;
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
//only play the damage sound on the closest limb (playing it on all just sounds like a mess)
bool playSound = PlayDamageSounds && limb == closestLimb;
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, playSound: playSound, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
damages.Add(limb, attackResult.Damage);
}
}
@@ -622,7 +644,9 @@ namespace Barotrauma
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
/// </summary>
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable<Submarine> ignoredSubmarines = null,
bool emitWallDamageParticles = true, bool distanceFalloff = true)
bool emitWallDamageParticles = true,
bool createWallDamageProjectiles = false,
bool distanceFalloff = true)
{
float dist = 600.0f;
damagedStructures.Clear();
@@ -642,7 +666,7 @@ namespace Barotrauma
1.0f;
if (distFactor <= 0.0f) { continue; }
structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles);
structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles, createWallDamageProjectiles);
if (damagedStructures.ContainsKey(structure))
{
@@ -850,9 +850,9 @@ namespace Barotrauma
Gap g = new Gap(rect, isHorizontal, submarine, id: idRemap.GetOffsetId(element))
{
linkedToID = new List<ushort>(),
Layer = element.GetAttributeString(nameof(Layer), null)
};
g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame).ToLower(), g.HiddenInGame);
g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame), g.HiddenInGame);
return g;
}
@@ -863,7 +863,8 @@ namespace Barotrauma
element.Add(
new XAttribute("ID", ID),
new XAttribute("horizontal", IsHorizontal ? "true" : "false"),
new XAttribute(nameof(HiddenInGame).ToLower(), HiddenInGame));
new XAttribute(nameof(HiddenInGame), HiddenInGame),
new XAttribute(nameof(Layer), Layer ?? string.Empty));
element.Add(new XAttribute("rect",
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
@@ -1141,8 +1141,10 @@ namespace Barotrauma
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.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
//gap blocked if the door is closed, and we haven't made any predictions of it opening client-side
if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) ||
//OR we've predicted that the door is closed client-side
(g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
{
if (g.ConnectedDoor.OpenState < 0.1f)
{
@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
using Barotrauma.MapCreatures.Behavior;
using Barotrauma.Networking;
using System;
namespace Barotrauma
{
@@ -17,7 +17,12 @@ namespace Barotrauma
private readonly XElement configElement;
public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities;
public readonly record struct DisplayEntity(
Identifier Identifier,
Rectangle Rect,
float RotationRad);
public readonly ImmutableArray<DisplayEntity> DisplayEntities;
public readonly Rectangle Bounds;
@@ -81,23 +86,25 @@ namespace Barotrauma
int minX = int.MaxValue, minY = int.MaxValue;
int maxX = int.MinValue, maxY = int.MinValue;
var displayEntities = new List<(Identifier, Rectangle)>();
var displayEntities = new List<DisplayEntity>();
foreach (XElement entityElement in element.Elements())
{
ushort id = (ushort)entityElement.GetAttributeInt("ID", 0);
if (id > 0 && containedItemIDs.Contains(id)) { continue; }
if (entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { continue; }
Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant());
Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty);
if (!entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase)))
{
if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { displayEntities.Add((identifier, rect)); }
minX = Math.Min(minX, rect.X);
minY = Math.Min(minY, rect.Y - rect.Height);
maxX = Math.Max(maxX, rect.Right);
maxY = Math.Max(maxY, rect.Y);
float rotation = MathHelper.ToRadians(entityElement.GetAttributeFloat("rotation", 0.0f));
if (!entityElement.GetAttributeBool("hideinassemblypreview", false))
{
displayEntities.Add(new DisplayEntity(identifier, rect, rotation));
}
minX = Math.Min(minX, rect.X);
minY = Math.Min(minY, rect.Y - rect.Height);
maxX = Math.Max(maxX, rect.Right);
maxY = Math.Max(maxY, rect.Y);
}
DisplayEntities = displayEntities.ToImmutableArray();
@@ -121,7 +128,11 @@ namespace Barotrauma
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false)
{
return PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance);
var retVal = PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance);
#if CLIENT
GameMain.SubEditorScreen?.ReconstructLayers();
#endif
return retVal;
}
public static List<MapEntity> PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false)
@@ -2,6 +2,7 @@
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Voronoi2;
namespace Barotrauma
@@ -75,6 +76,7 @@ namespace Barotrauma
public void AddDamage(float damage, Vector2 worldPosition)
{
AddDamageProjSpecific(damage, worldPosition);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Destroyed) { return; }
if (!MathUtils.NearlyEqual(damage, 0.0f)) { NetworkUpdatePending = true; }
@@ -100,6 +102,7 @@ namespace Barotrauma
#if CLIENT
SoundPlayer.PlaySound("icebreak", WorldPosition);
#endif
Vector2 center = Vector2.Zero;
//generate initial triangles (one triangle from each edge to the center of the cell)
List<List<Vector2>> triangles = new List<List<Vector2>>();
foreach (var cell in Cells)
@@ -114,6 +117,11 @@ namespace Barotrauma
};
triangles.Add(triangleVerts);
}
center += cell.Center;
}
if (Cells.Any())
{
center /= Cells.Count;
}
//split triangles that have edges more than 1000 units long
@@ -174,23 +182,24 @@ namespace Barotrauma
Vector2 bodyDiff = simTriangleCenter - Body.Position;
fragment.Body.LinearVelocity = (bodyDiff + Rand.Vector(0.5f)).ClampLength(15.0f);
fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f);// MathHelper.Clamp(-bodyDiff.X * 0.1f, -0.5f, 0.5f);
fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f);
Level.Loaded.UnsyncedExtraWalls.Add(fragment);
#if CLIENT
for (int i = 0; i < 20; i++)
for (int i = 0; i < 5; i++)
{
int startEdgeIndex = Rand.Int(3);
Vector2 pos1 = triangle[startEdgeIndex];
Vector2 pos2 = triangle[(startEdgeIndex + 1) % 3];
var particle = GameMain.ParticleManager.CreateParticle("iceshards",
var particle = GameMain.ParticleManager.CreateParticle("iceexplosion",
triangleCenter + Vector2.Lerp(pos1, pos2, Rand.Range(0.0f, 1.0f)),
Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f);
velocity: (Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f));
if (particle != null)
{
particle.Size *= Rand.Range(1.0f, 5.0f);
particle.ColorMultiplier *= Rand.Range(0.7f, 1.0f);
}
}
#endif
@@ -119,7 +119,6 @@ namespace Barotrauma
/// <summary>
/// Caves, ruins, outposts and similar enclosed areas
/// </summary>
/// <returns></returns>
public bool IsEnclosedArea()
{
return
@@ -127,6 +126,7 @@ namespace Barotrauma
PositionType == PositionType.Ruin ||
PositionType == PositionType.Outpost ||
PositionType == PositionType.BeaconStation ||
PositionType == PositionType.Wreck ||
PositionType == PositionType.AbyssCave;
}
}
@@ -698,106 +698,8 @@ namespace Barotrauma
//----------------------------------------------------------------------------------
//generate voronoi sites
//----------------------------------------------------------------------------------
Point siteInterval = GenerationParams.VoronoiSiteInterval;
int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
Point siteVariance = GenerationParams.VoronoiSiteVariance;
siteCoordsX = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
siteCoordsY = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
const int caveSiteInterval = 500;
for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X)
{
for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y)
{
int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient);
int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient);
bool closeToTunnel = false;
bool closeToCave = false;
foreach (Tunnel tunnel in Tunnels)
{
float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; }
if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; }
if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; }
if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; }
double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (Math.Sqrt(tunnelDistSqr) < minDist)
{
closeToTunnel = true;
//tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (tunnel.Type == TunnelType.Cave)
{
closeToCave = true;
}
break;
}
}
}
if (!closeToTunnel)
{
//make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry
if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; }
}
if (!TooCloseToOtherSites(siteX, siteY))
{
siteCoordsX.Add(siteX);
siteCoordsY.Add(siteY);
}
if (closeToCave)
{
for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval)
{
for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
{
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval))
{
siteCoordsX.Add(caveSiteX);
siteCoordsY.Add(caveSiteY);
}
}
}
}
}
}
bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f)
{
float minDistanceSqr = minDistance * minDistance;
for (int i = 0; i < siteCoordsX.Count; i++)
{
if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr)
{
return true;
}
}
return false;
}
for (int i = 0; i < siteCoordsX.Count; i++)
{
Debug.Assert(
siteCoordsX[i] > 0 || siteCoordsY[i] > 0,
$"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
Debug.Assert(
siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height,
$"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
for (int j = i + 1; j < siteCoordsX.Count; j++)
{
Debug.Assert(
MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 1.0f,
"Potential error in level generation: two voronoi sites are extremely close to each other.");
}
}
GenerateVoronoiSites();
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen);
@@ -809,13 +711,52 @@ namespace Barotrauma
sw2.Start();
Debug.Assert(siteCoordsX.Count == siteCoordsY.Count);
List<GraphEdge> graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height);
Debug.WriteLine("MakeVoronoiGraph: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
//construct voronoi cells based on the graph edges
cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid);
int remainingRetries = 5;
bool voronoiGraphInvalid = false;
do
{
remainingRetries--;
voronoiGraphInvalid = false;
//construct voronoi cells based on the graph edges
List<GraphEdge> graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height);
cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid);
for (int i = 0; i < cells.Count; i++)
{
for (int j = i + 1; j < cells.Count; j++)
{
//sites can never be inside multiple cells in a voronoi graph by definition
//if they are, that'll cause severe issues with the rest of the level generation.
//There seems to be a very rare issue that sometimes causes the graph to generate incorrectly (see #10944 and #12980),
//leading to a crash due. I haven't been able to figure out what's causing that - there don't seem to be any issues in the sites,
//so I'm getting the feeling it could be a bug with the voronoi graph generation.
//If that happens, let's just retry a couple of times (re-randomizing the sites and regenerating
//the map seems to fix the issue in all cases I've seen)
if (cells[j].IsPointInside(cells[i].Center))
{
voronoiGraphInvalid = true;
break;
}
if (voronoiGraphInvalid) { break; }
}
}
if (voronoiGraphInvalid)
{
string errorMsg = "Unknown error during level generation. Invalid voronoi graph: the same voronoi site was inside multiple cells.";
if (remainingRetries > 0)
{
DebugConsole.AddWarning(errorMsg + " Retrying...");
GenerateVoronoiSites();
}
else
{
//throw a console error and let the generation finish, hoping for the best
DebugConsole.ThrowError(errorMsg);
}
}
} while (remainingRetries > 0 && voronoiGraphInvalid);
GenerateAbyssGeometry();
GenerateAbyssPositions();
@@ -1449,6 +1390,116 @@ namespace Barotrauma
Generating = false;
}
private void GenerateVoronoiSites()
{
Point siteInterval = GenerationParams.VoronoiSiteInterval;
int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
Point siteVariance = GenerationParams.VoronoiSiteVariance;
siteCoordsX = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
siteCoordsY = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
const int caveSiteInterval = 500;
for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X)
{
for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y)
{
int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient);
int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient);
bool closeToTunnel = false;
bool closeToCave = false;
foreach (Tunnel tunnel in Tunnels)
{
float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; }
if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; }
if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; }
if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; }
double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (Math.Sqrt(tunnelDistSqr) < minDist)
{
closeToTunnel = true;
//tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (tunnel.Type == TunnelType.Cave)
{
closeToCave = true;
}
break;
}
}
}
if (!closeToTunnel)
{
//make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry
if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; }
}
if (!TooCloseToOtherSites(siteX, siteY))
{
siteCoordsX.Add(siteX);
siteCoordsY.Add(siteY);
}
if (closeToCave)
{
for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval)
{
for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
{
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval))
{
siteCoordsX.Add(caveSiteX);
siteCoordsY.Add(caveSiteY);
}
}
}
}
}
}
bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f)
{
float minDistanceSqr = minDistance * minDistance;
for (int i = 0; i < siteCoordsX.Count; i++)
{
if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr)
{
return true;
}
}
return false;
}
for (int i = 0; i < siteCoordsX.Count; i++)
{
Debug.Assert(
!double.IsNaN(siteCoordsX[i]) && !double.IsInfinity(siteCoordsX[i]),
$"Potential error in level generation: invalid voronoi site ({siteCoordsX[i]})");
Debug.Assert(
!double.IsNaN(siteCoordsY[i]) && !double.IsInfinity(siteCoordsY[i]),
$"Potential error in level generation: invalid voronoi site ({siteCoordsY[i]})");
Debug.Assert(
siteCoordsX[i] > 0 || siteCoordsY[i] > 0,
$"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
Debug.Assert(
siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height,
$"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
for (int j = i + 1; j < siteCoordsX.Count; j++)
{
Debug.Assert(
MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 10.0f,
"Potential error in level generation: two voronoi sites are extremely close to each other.");
}
}
}
private List<Point> GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance)
{
List<Point> pathNodes = new List<Point> { startPosition };
@@ -3165,7 +3216,7 @@ namespace Barotrauma
private List<ClusterLocation> GetAllValidClusterLocations()
{
var subBorders = new List<Rectangle>();
Wrecks.ForEach(w => AddBordersToList(w));
Wrecks.ForEach(AddBordersToList);
AddBordersToList(BeaconStation);
var locations = new List<ClusterLocation>();
@@ -3186,6 +3237,8 @@ namespace Barotrauma
{
if (s == null) { return; }
var rect = Submarine.AbsRect(s.WorldPosition, s.Borders.Size.ToVector2());
// range of piezo crystal discharge is 3500, pad the rect to ensure no such kind of hazards spawn near
rect.Inflate(4000, 4000);
subBorders.Add(rect);
}
@@ -3202,9 +3255,14 @@ namespace Barotrauma
{
foreach (var r in subBorders)
{
if (Submarine.RectContains(r, e.Point1)) { return true; }
if (Submarine.RectContains(r, e.Point2)) { return true; }
if (Submarine.RectContains(r, eCenter)) { return true; }
if (r.Contains(e.Point1)) { return true; }
if (r.Contains(e.Point2)) { return true; }
if (r.Contains(eCenter)) { return true; }
if (MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, r, out _))
{
return true;
}
}
return false;
}
@@ -3291,7 +3349,7 @@ namespace Barotrauma
Vector2 endPos = startPos - Vector2.UnitY * Size.Y;
//try to find a level wall below the position unless the position is indoors
if (!potentialPos.IsEnclosedArea())
if (!potentialPos.PositionType.IsEnclosedArea())
{
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(startPos),
@@ -3684,7 +3742,12 @@ namespace Barotrauma
var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom;
// Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000);
int padding = 1500;
Rectangle paddedBorders = new Rectangle(
subBorders.X - padding,
subBorders.Y + padding,
subBorders.Width + padding * 2,
subBorders.Height + padding * 2);
var positions = new List<Vector2>();
var rects = new List<Rectangle>();
@@ -3733,7 +3796,8 @@ namespace Barotrauma
{
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance)
{
hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient);
hull.WaterVolume =
Math.Max(hull.WaterVolume, hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient));
}
}
// Only spawn thalamus when the wreck has some thalamus items defined.
@@ -3806,10 +3870,18 @@ namespace Barotrauma
}
}
positions.Add(spawnPoint);
bool isBlocked = IsBlocked(spawnPoint, subBorders.Size - new Point(step + 50));
//shrink the bounds a bit to allow the sub to go slightly inside the wall
//(just enough that it doesn't look like it's floating)
int shrinkAmount = step + 50;
Rectangle shrunkenBorders = new Rectangle(
subBorders.X + shrinkAmount,
subBorders.Y - shrinkAmount,
subBorders.Width - shrinkAmount * 2,
subBorders.Height - shrinkAmount * 2);
bool isBlocked = IsBlocked(spawnPoint, shrunkenBorders);
if (isBlocked)
{
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size));
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint() + subBorders.Location, subBorders.Size));
Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls.");
}
else if (!bottomFound)
@@ -3832,7 +3904,8 @@ namespace Barotrauma
float maxMovement = 5000;
float totalAmount = 0;
bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
while (!IsSideBlocked(subBorders, amount > 0))
//move until the side is no longer blocked
while (IsSideBlocked(subBorders, front: amount < 0))
{
foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
totalAmount += amount;
@@ -3854,7 +3927,7 @@ namespace Barotrauma
{
var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
waypoints.Remove(wp);
if (!IsBlocked(wp.WorldPosition, paddedDimensions))
if (!IsBlocked(wp.WorldPosition, paddedBorders))
{
spawnPoint = wp.WorldPosition;
return true;
@@ -3867,76 +3940,73 @@ namespace Barotrauma
{
// Shoot five rays and pick the highest hit point.
int rayCount = 5;
var positions = new Vector2[rayCount];
var hitPositions = new Vector2[rayCount];
bool hit = false;
for (int i = 0; i < rayCount; i++)
{
float quarterWidth = subBorders.Width * 0.25f;
Vector2 rayStart = spawnPoint;
switch (i)
{
case 1:
rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y);
break;
case 2:
rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y);
break;
case 3:
rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y);
break;
case 4:
rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y);
break;
}
//cast rays starting from the left side of the sub, offset by 20% to 80% of the sub's width
//(ignoring the very back and front of the sub, it's fine if they overlap a bit)
float xOffset =
subBorders.Width * MathHelper.Lerp(0.2f, 0.8f, i / (float)(rayCount - 1));
Vector2 rayStart = new Vector2(
spawnPoint.X + subBorders.Location.X + xOffset,
spawnPoint.Y);
var simPos = ConvertUnits.ToSimUnits(rayStart);
var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1),
customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
if (body != null)
{
positions[i] =
ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) +
new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1));
hitPositions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition);
hit = true;
}
}
float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y);
spawnPoint = new Vector2(spawnPoint.X, highestPoint);
int dir = placement == PlacementType.Bottom ? -1 : 1;
float highestPoint = placement == PlacementType.Bottom ? hitPositions.Max(p => p.Y) : hitPositions.Min(p => p.Y);
float halfHeight = subBorders.Height / 2;
float centerOffset = subBorders.Y - halfHeight;
spawnPoint = new Vector2(spawnPoint.X, highestPoint + halfHeight * -dir - centerOffset);
return hit;
}
bool IsSideBlocked(Rectangle subBorders, bool front)
{
Point centerOffset = new Point(subBorders.Center.X, subBorders.Y - subBorders.Height / 2);
// Shoot three rays and check whether any of them hits.
int rayCount = 3;
int dir = front ? 1 : -1;
Vector2 halfSize = subBorders.Size.ToVector2() / 2;
Vector2 quarterSize = halfSize / 2;
var positions = new Vector2[rayCount];
for (int i = 0; i < rayCount; i++)
{
float dir = front ? 1 : -1;
Vector2 rayStart;
Vector2 to;
Vector2 rayStart, to;
switch (i)
{
case 1:
rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y);
to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y);
break;
case 2:
rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y);
to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y);
float yOffset = quarterSize.Y * (i == 1 ? 1 : -1);
//from a position half-way towards the edge, to the edge.
//we start half-way towards the edge instead of the center, because we want to allow things to poke partially inside the sub
rayStart = new Vector2(spawnPoint.X + quarterSize.X * dir, spawnPoint.Y + yOffset);
to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
break;
case 0:
default:
//center to center-right
rayStart = spawnPoint;
to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
break;
}
rayStart += centerOffset.ToVector2();
to += centerOffset.ToVector2();
Vector2 simPos = ConvertUnits.ToSimUnits(rayStart);
if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to),
customPredicate: f => f.Body?.UserData is VoronoiCell cell,
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
allowInsideFixture: true) != null)
{
return true;
}
@@ -3944,10 +4014,11 @@ namespace Barotrauma
return false;
}
bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1)
bool IsBlocked(Vector2 pos, Rectangle submarineBounds)
{
float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared();
Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size);
Rectangle bounds = new Rectangle(
(int)pos.X + submarineBounds.X, (int)pos.Y + submarineBounds.Y,
submarineBounds.Width, submarineBounds.Height);
if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds)))
{
return true;
@@ -3958,13 +4029,16 @@ namespace Barotrauma
{
return true;
}
return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
return cells.Any(c =>
c.Body != null &&
c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
}
}
// For debugging
private readonly Dictionary<Submarine, List<Vector2>> wreckPositions = new Dictionary<Submarine, List<Vector2>>();
private readonly Dictionary<Submarine, List<Rectangle>> blockedRects = new Dictionary<Submarine, List<Rectangle>>();
private void CreateWrecks()
{
var totalSW = new Stopwatch();
@@ -4005,6 +4079,18 @@ namespace Barotrauma
wreckCount = Math.Max(wreckCount, 1);
}
if (LevelData.ForceWreck != null)
{
//force the desired wreck to be chosen first
var matchingFile = wreckFiles.FirstOrDefault(w => w.Path == LevelData.ForceWreck.FilePath);
if (matchingFile != null)
{
wreckFiles.Remove(matchingFile);
wreckFiles.Insert(0, matchingFile);
}
wreckCount = Math.Max(wreckCount, 1);
}
Wrecks = new List<Submarine>(wreckCount);
for (int i = 0; i < wreckCount; i++)
{
@@ -4286,7 +4372,7 @@ namespace Barotrauma
private void CreateBeaconStation()
{
if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; }
if (!LevelData.HasBeaconStation && LevelData.ForceBeaconStation == null && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; }
var beaconStationFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<BeaconStationFile>())
.OrderBy(f => f.UintIdentifier).ToList();
@@ -4307,6 +4393,10 @@ namespace Barotrauma
DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead...");
}
}
else if (LevelData.ForceBeaconStation != null)
{
contentFile = beaconStationFiles.FirstOrDefault(b => b.Path == LevelData.ForceBeaconStation.FilePath);
}
if (contentFile == null)
{
@@ -4384,10 +4474,17 @@ namespace Barotrauma
fuelPrefab, reactorContainer.Inventory,
onSpawned: (it) => reactorComponent.PowerUpImmediately());
}
beaconSonar.CurrentMode = Sonar.Mode.Active;
if (beaconSonar == null)
{
DebugConsole.AddWarning($"Beacon station \"{BeaconStation.Info.Name}\" has no sonar. Beacon missions might not work correctly with this beacon station.");
}
else
{
beaconSonar.CurrentMode = Sonar.Mode.Active;
#if SERVER
beaconSonar.Item.CreateServerEvent(beaconSonar);
beaconSonar.Item.CreateServerEvent(beaconSonar);
#endif
}
}
else if (GameMain.NetworkMember is not { IsClient: true })
{
@@ -4470,7 +4567,7 @@ namespace Barotrauma
{
foreach (var connectedSub in parentSub.GetConnectedSubs())
{
connectedSub.RealWorldCrushDepth = Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000);
connectedSub.SetCrushDepth(Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000));
}
}
@@ -4631,7 +4728,7 @@ namespace Barotrauma
}
/// <summary>
/// Calculate the "real" depth in meters from the surface of Europa
/// Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav terminal)
/// </summary>
public float GetRealWorldDepth(float worldPositionY)
{
@@ -4733,4 +4830,30 @@ namespace Barotrauma
Loaded = null;
}
}
static class PositionTypeExtensions
{
/// <summary>
/// Caves, ruins, outposts and similar enclosed areas
/// </summary>
public static bool IsEnclosedArea(this Level.PositionType positionType)
{
return
positionType == Level.PositionType.Cave ||
positionType == Level.PositionType.AbyssCave ||
positionType.IsIndoorsArea();
}
/// <summary>
/// Area inside a submarine (outpost, wreck, beacon station)
/// </summary>
public static bool IsIndoorsArea(this Level.PositionType positionType)
{
return
positionType == Level.PositionType.Outpost ||
positionType == Level.PositionType.BeaconStation ||
positionType == Level.PositionType.Ruin ||
positionType == Level.PositionType.Wreck;
}
}
}
@@ -43,6 +43,10 @@ namespace Barotrauma
public OutpostGenerationParams ForceOutpostGenerationParams;
public SubmarineInfo ForceBeaconStation;
public SubmarineInfo ForceWreck;
public bool AllowInvalidOutpost;
public readonly Point Size;
@@ -405,25 +405,35 @@ namespace Barotrauma
}
}
public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture otherFixture, Entity separatingEntity)
/// <summary>
/// Checks whether any fixture of the trigger body is in contact with any fixture belonging to the physics bodies of separatingEntity
/// </summary>
/// <param name="triggerBody">Physics body of the trigger</param>
/// <param name="separatingFixture">Fixture that got separated from the trigger</param>
/// <param name="separatingEntity">Entity that got separated from the trigger</param>
/// <returns></returns>
public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture separatingFixture, Entity separatingEntity)
{
//check if there are contacts with any other fixture of the trigger
//(the OnSeparation callback happens when two fixtures separate,
//e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body)
foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList)
foreach (Fixture triggerFixture in triggerBody.FarseerBody.FixtureList)
{
ContactEdge contactEdge = fixture.Body.ContactList;
ContactEdge contactEdge = triggerFixture.Body.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.Enabled &&
contactEdge.Contact.IsTouching)
{
if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == otherFixture ?
//which fixture of this contact belongs to the "other" body (not the trigger itself)
Fixture otherFixture =
contactEdge.Contact.FixtureA == triggerFixture ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
contactEdge.Contact.FixtureA;
if (otherFixture != separatingFixture)
{
var otherEntity = GetEntity(otherFixture);
if (otherEntity == separatingEntity) { return true; }
}
}
@@ -307,6 +307,12 @@ namespace Barotrauma
// Adjust by current reputation
price *= GetReputationModifier(true);
// Adjust by campaign difficulty settings
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
price *= campaign.Settings.ShopPriceMultiplier;
}
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
{
@@ -768,27 +774,22 @@ namespace Barotrauma
else
{
DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
TextManager.Get(nameIdentifier) :
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
TextManager.Get(nameIdentifier) :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
}
TryAssignFactionBasedOnLocationType(campaign);
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
if (Faction == null)
{
Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced);
}
if (SecondaryFaction == null)
{
SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced);
}
if (Type.Faction == Identifier.Empty) { Faction ??= campaign.GetRandomFaction(Rand.RandSync.Unsynced); }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); }
}
else
{
Faction = null;
SecondaryFaction = null;
if (Type.Faction == Identifier.Empty) { Faction = null; }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
}
UnlockInitialMissions(Rand.RandSync.Unsynced);
@@ -799,6 +800,30 @@ namespace Barotrauma
}
}
public void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
{
if (campaign == null) { return; }
if (Type.Faction != Identifier.Empty)
{
Faction = Type.Faction == "None" ? null : TryFindFaction(Type.Faction);
}
if (Type.SecondaryFaction != Identifier.Empty)
{
SecondaryFaction = Type.SecondaryFaction == "None" ? null : TryFindFaction(Type.SecondaryFaction);
}
Faction TryFindFaction(Identifier identifier)
{
var faction = campaign.GetFaction(identifier);
if (faction == null)
{
DebugConsole.ThrowError($"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".",
contentPackage: Type.ContentPackage);
}
return faction;
}
}
public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
if (Type.MissionIdentifiers.Any())
@@ -1127,6 +1152,7 @@ namespace Barotrauma
nameIdentifier = type.GetRandomNameId(rand, existingLocations);
if (nameIdentifier.IsEmpty)
{
//backwards compatibility
rawName = type.GetRandomRawName(rand, existingLocations);
if (rawName.IsNullOrEmpty())
{
@@ -1134,7 +1160,15 @@ namespace Barotrauma
rawName = "none";
}
nameIdentifier = rawName.ToIdentifier();
DisplayName = rawName;
if (type.NameFormats == null || !type.NameFormats.Any())
{
DisplayName = rawName;
}
else
{
nameFormatIndex = rand.Next() % type.NameFormats.Count;
DisplayName = type.NameFormats[nameFormatIndex].Replace("[name]", rawName);
}
}
else
{
@@ -86,6 +86,16 @@ namespace Barotrauma
public Identifier ReplaceInRadiation { get; }
/// <summary>
/// If set, forces the location to be assigned to this faction. Set to "None" if you don't want the location to be assigned to any faction.
/// </summary>
public Identifier Faction { get; }
/// <summary>
/// If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want the location to be assigned to any secondary faction.
/// </summary>
public Identifier SecondaryFaction { get; }
public Sprite Sprite { get; private set; }
public Sprite RadiationSprite { get; }
@@ -134,6 +144,9 @@ namespace Barotrauma
AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true);
AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true);
Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
SecondaryFaction = element.GetAttributeIdentifier(nameof(SecondaryFaction), Identifier.Empty);
ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true);
MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty<Identifier>()).ToImmutableArray();
@@ -298,7 +298,7 @@ namespace Barotrauma
//if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
if (CurrentLocation == null)
{
FindStartLocation(l => l.Type.HasOutpost);
FindStartLocation(l => l.Type.HasOutpost && l.Type.OutpostTeam == CharacterTeamType.FriendlyNPC);
}
void FindStartLocation(Func<Location, bool> predicate)
@@ -313,25 +313,38 @@ namespace Barotrauma
}
}
StartLocation.SecondaryFaction = null;
StartLocation.SecondaryFaction = null;
var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost);
if (startOutpostFaction != null)
{
StartLocation.Faction = startOutpostFaction;
foreach (var connection in StartLocation.Connections)
}
foreach (var connection in StartLocation.Connections)
{
//force locations adjacent to the start location to have an outpost
//non-inhabited locations seem to be confusing to new players, particularly
//on the first round/mission when they still don't know how transitions between levels work
var otherLocation = connection.OtherLocation(StartLocation);
if (!otherLocation.HasOutpost())
{
var otherLocation = connection.OtherLocation(StartLocation);
if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
if (LocationType.Prefabs.TryGet("outpost".ToIdentifier(), out LocationType outpostLocationType))
{
otherLocation.Faction = startOutpostFaction;
otherLocation.ChangeType(campaign, outpostLocationType);
}
}
if (otherLocation.HasOutpost() &&
otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC &&
otherLocation.Type.Faction.IsEmpty)
{
otherLocation.Faction = startOutpostFaction;
}
}
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0);
if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium))
if (loops == 0 && (campaign.Settings.WorldHostility == WorldHostilityOption.Low || campaign.Settings.WorldHostility == WorldHostilityOption.Medium))
{
if (StartLocation != null)
{
@@ -705,10 +718,19 @@ namespace Barotrauma
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
location.TryAssignFactionBasedOnLocationType(campaign);
if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
if (location.Type.Faction.IsEmpty)
{
//no faction defined in the location type, assign a random one
location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
}
if (location.Type.SecondaryFaction.IsEmpty)
{
//no secondary faction defined in the location type, assign a random one
location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
}
}
location.CreateStores(force: true);
}
@@ -1502,6 +1524,10 @@ namespace Barotrauma
LocationType prevLocationType = location.Type;
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
location.ChangeType(campaign, newLocationType);
var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
if (showNotifications && prevLocationType != location.Type)
{
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
@@ -1512,9 +1538,6 @@ namespace Barotrauma
}
}
var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
@@ -634,9 +634,22 @@ namespace Barotrauma
#endif
Powered.UpdatePower(deltaTime);
Item.UpdatePendingConditionUpdates(deltaTime);
foreach (Item item in Item.ItemList)
Item lastUpdatedItem = null;
try
{
item.Update(deltaTime, cam);
foreach (Item item in Item.ItemList)
{
lastUpdatedItem = item;
item.Update(deltaTime, cam);
}
}
catch (InvalidOperationException e)
{
GameAnalyticsManager.AddErrorEventOnce(
"MapEntity.UpdateAll:ItemUpdateInvalidOperation",
GameAnalyticsManager.ErrorSeverity.Critical,
$"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}");
throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e);
}
UpdateAllProjSpecific(deltaTime);
@@ -131,6 +131,13 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable]
public string ReplaceInRadiation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "By default, sonar only shows the outline of the sub/outpost from the outside. Enable this if you want to see each structure individually."), Editable]
public bool AlwaysShowStructuresOnSonar
{
get;
set;
}
public ContentPath OutpostFilePath { get; set; }
public class ModuleCount
@@ -238,8 +238,7 @@ namespace Barotrauma
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != sub) { continue; }
if (string.IsNullOrEmpty(hull.RoomName) ||
hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrEmpty(hull.RoomName))
{
hull.RoomName = hull.CreateRoomName();
}
@@ -877,16 +876,16 @@ namespace Barotrauma
}
}
if (availableModules.Count() == 0) { return null; }
if (!availableModules.Any()) { return null; }
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
modulesSuitableForLocationType = availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInAnyLocationType());
}
if (!modulesSuitableForLocationType.Any())
@@ -956,11 +955,12 @@ namespace Barotrauma
{
if (disallowNonLocationTypeSpecific)
{
//don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
}
else
{
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any());
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
}
}
if (requireAllowAttachToPrevious && prevModule != null)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -132,6 +133,16 @@ namespace Barotrauma
this.allowedLocationTypes.Add(locationType);
}
}
public bool IsAllowedInAnyLocationType()
{
return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier());
}
public bool IsAllowedInLocationType(LocationType locationType)
{
if (locationType == null || IsAllowedInAnyLocationType()) { return true; }
return allowedLocationTypes.Contains(locationType.Identifier);
}
public void DetermineGapPositions(Submarine sub)
{
@@ -24,6 +24,8 @@ namespace Barotrauma
public float damage;
public Gap gap;
public bool NoPhysicsBody;
public Structure Wall { get; }
public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this));
public Vector2 WorldPosition => Wall.SectionPosition(Wall.Sections.IndexOf(this), world: true);
@@ -49,21 +51,18 @@ namespace Barotrauma
public static List<Structure> WallList = new List<Structure>();
const float LeakThreshold = 0.1f;
const float BigGapThreshold = 0.7f;
#if CLIENT
public SpriteEffects SpriteEffects = SpriteEffects.None;
#endif
//dimensions of the wall sections' physics bodies (only used for debug rendering)
private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>();
private readonly Dictionary<Body, Vector2> bodyDimensions = new Dictionary<Body, Vector2>();
private static Explosion explosionOnBroken;
#if DEBUG
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)]
#else
[Serialize(false, IsPropertySaveable.Yes)]
#endif
public bool Indestructible
{
get;
@@ -596,7 +595,7 @@ namespace Barotrauma
private void CreateStairBodies()
{
Bodies = new List<Body>();
bodyDebugDimensions.Clear();
bodyDimensions.Clear();
float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f));
@@ -621,7 +620,7 @@ namespace Barotrauma
newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale;
bodyDebugDimensions.Add(new Vector2(bodyWidth, bodyHeight));
bodyDimensions.Add(newBody, new Vector2(bodyWidth, bodyHeight));
Bodies.Add(newBody);
}
@@ -962,18 +961,22 @@ namespace Barotrauma
return true;
}
public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true)
public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true, bool createWallDamageProjectiles = false)
{
if (!Prefab.Body || Prefab.Platform || Indestructible) { return; }
if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; }
var section = Sections[sectionIndex];
float prevDamage = section.damage;
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
SetDamage(sectionIndex, section.damage + damage, attacker);
}
#if CLIENT
if (damage > 0 && emitParticles)
{
float dmg = Math.Min(MaxHealth - section.damage, damage);
float dmg = Math.Min(section.damage - prevDamage, damage);
float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f)));
// Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle
if (particleAmount < 1 && Rand.Value() < 0.10f)
@@ -996,13 +999,13 @@ namespace Barotrauma
var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle,
position: particlePosFinal,
velocity: Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f);
if (particle == null) break;
if (particle == null) { break; }
}
}
#endif
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
SetDamage(sectionIndex, section.damage + damage, attacker);
SetDamage(sectionIndex, section.damage + damage, attacker, createWallDamageProjectiles: createWallDamageProjectiles);
}
}
@@ -1132,7 +1135,7 @@ namespace Barotrauma
if (MathUtils.CircleIntersectsRectangle(transformedPos, attack.DamageRange, sectionRect))
{
damageAmount = attack.GetStructureDamage(deltaTime);
AddDamage(i, damageAmount, attacker);
AddDamage(i, damageAmount, attacker, createWallDamageProjectiles: attack.CreateWallDamageProjectiles);
#if CLIENT
if (attack.EmitStructureDamageParticles)
{
@@ -1165,7 +1168,11 @@ namespace Barotrauma
return new AttackResult(damageAmount, null);
}
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true)
public void SetDamage(int sectionIndex, float damage, Character attacker = null,
bool createNetworkEvent = true,
bool isNetworkEvent = true,
bool createExplosionEffect = true,
bool createWallDamageProjectiles = false)
{
if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; }
if (!Prefab.Body) { return; }
@@ -1173,6 +1180,8 @@ namespace Barotrauma
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
if (Sections[sectionIndex].NoPhysicsBody) { return; }
#if SERVER
if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
{
@@ -1301,13 +1310,22 @@ namespace Barotrauma
}
var gap = Sections[sectionIndex].gap;
float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
float damageRatio = MaxHealth <= 0.0f ? 0 : damage / MaxHealth;
float gapOpen = 0;
if (damageRatio > BigGapThreshold)
{
gapOpen = MathHelper.Lerp(0.35f, 0.75f, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio));
}
else if (damageRatio > LeakThreshold)
{
gapOpen = MathHelper.Lerp(0f, 0.35f, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio));
}
gap.Open = gapOpen;
//gap appeared or became much larger -> explosion effect
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
{
CreateWallDamageExplosion(gap, attacker);
CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles);
}
}
@@ -1337,7 +1355,7 @@ namespace Barotrauma
UpdateSections();
}
private static void CreateWallDamageExplosion(Gap gap, Character attacker)
private static void CreateWallDamageExplosion(Gap gap, Character attacker, bool createProjectiles)
{
const float explosionRange = 500.0f;
float explosionStrength = gap.Open;
@@ -1367,31 +1385,52 @@ namespace Barotrauma
{
explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null);
}
explosionOnBroken.CameraShake = 5.0f;
explosionOnBroken.IgnoreCover = false;
explosionOnBroken.OnlyInside = true;
explosionOnBroken.DistanceFalloff = false;
explosionOnBroken.PlayDamageSounds = true;
explosionOnBroken.DisableParticles();
}
explosionOnBroken.CameraShake = 25.0f;
explosionOnBroken.IgnoredCover = gap.ConnectedWall?.ToEnumerable();
explosionOnBroken.Attack.Range = explosionRange * gap.Open;
explosionOnBroken.Attack.Range = explosionOnBroken.CameraShakeRange = explosionRange * gap.Open;
explosionOnBroken.Attack.DamageMultiplier = explosionStrength;
explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f);
explosionOnBroken.IgnoredCharacters.Clear();
if (attacker?.AIController is EnemyAIController) { explosionOnBroken.IgnoredCharacters.Add(attacker); }
explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker);
if (createProjectiles)
{
if (ItemPrefab.Prefabs.TryGet("walldamageprojectile", out var projectilePrefab) && linkedHull != null)
{
float angle = gap.IsHorizontal ?
(linkedHull.WorldPosition.X < gap.WorldPosition.X ? MathHelper.Pi : 0) :
(linkedHull.WorldPosition.Y < gap.WorldPosition.Y ? -MathHelper.PiOver2 : MathHelper.PiOver2);
Spawner.AddItemToSpawnQueue(projectilePrefab, gap.WorldPosition, onSpawned: (item) =>
{
item.body.SetTransformIgnoreContacts(item.body.SimPosition, angle);
var projectile = item.GetComponent<Items.Components.Projectile>();
projectile?.Use();
});
}
}
#if CLIENT
SoundPlayer.PlaySound("Ricochet", gap.WorldPosition);
if (linkedHull != null)
{
for (int i = 0; i <= 50; i++)
{
Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y));
var velocity = gap.IsHorizontal ?
var emitDirection = gap.IsHorizontal ?
gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX :
gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY;
velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f));
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f);
if (particle == null) { break; }
Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y));
emitDirection = new Vector2(emitDirection.X + Rand.Range(-0.2f, 0.2f), emitDirection.Y + Rand.Range(-0.2f, 0.2f));
var shrapnelParticle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, emitDirection * Rand.Range(100.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.1f);
var sparkParticle = GameMain.ParticleManager.CreateParticle("whitespark", particlePos, emitDirection * Rand.Range(1000.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.05f);
if (shrapnelParticle == null || sparkParticle == null) { break; }
}
}
#endif
@@ -1416,7 +1455,7 @@ namespace Barotrauma
GameMain.World.Remove(b);
}
Bodies.Clear();
bodyDebugDimensions.Clear();
bodyDimensions.Clear();
#if CLIENT
convexHulls?.ForEach(ch => ch.Remove());
convexHulls?.Clear();
@@ -1454,8 +1493,27 @@ namespace Barotrauma
if (hasHoles || !Bodies.Any())
{
Body sensorBody = CreateRectBody(rect, createConvexHull: false);
sensorBody.CollisionCategories = Physics.CollisionRepair;
sensorBody.SetIsSensor(true);
sensorBody.CollisionCategories = Physics.CollisionRepairableWall;
}
foreach (var section in Sections)
{
bool intersectsWithBody = false;
foreach (var body in Bodies)
{
var bodyRect = new Rectangle(
ConvertUnits.ToDisplayUnits(body.Position - bodyDimensions[body] / 2).ToPoint(),
ConvertUnits.ToDisplayUnits(bodyDimensions[body]).ToPoint());
Rectangle sectionRect = section.rect;
sectionRect.Y -= section.rect.Height;
if (bodyRect.Intersects(sectionRect))
{
intersectsWithBody = true;
break;
}
}
section.NoPhysicsBody = !intersectsWithBody;
}
}
@@ -1511,7 +1569,7 @@ namespace Barotrauma
}
Bodies.Add(newBody);
bodyDebugDimensions.Add(new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height)));
bodyDimensions.Add(newBody, new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height)));
return newBody;
}
@@ -1534,7 +1592,7 @@ namespace Barotrauma
StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left;
Bodies.ForEach(b => GameMain.World.Remove(b));
Bodies.Clear();
bodyDebugDimensions.Clear();
bodyDimensions.Clear();
CreateStairBodies();
}
@@ -1562,7 +1620,7 @@ namespace Barotrauma
StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left;
Bodies.ForEach(b => GameMain.World.Remove(b));
Bodies.Clear();
bodyDebugDimensions.Clear();
bodyDimensions.Clear();
CreateStairBodies();
}
@@ -188,7 +188,6 @@ namespace Barotrauma
}
return realWorldCrushDepth.Value;
}
set { realWorldCrushDepth = value; }
}
/// <summary>
@@ -976,7 +975,7 @@ namespace Barotrauma
if (ignoreLevel && fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionWall)
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; }
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionRepairableWall)) { return -1; }
if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; }
if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; }
if (fixture.Body.UserData as string == "ruinroom") { return -1; }
@@ -1113,27 +1112,45 @@ namespace Barotrauma
}
public void EnableFactionSpecificEntities(Identifier factionIdentifier)
{
foreach (var faction in FactionPrefab.Prefabs)
{
SetLayerEnabled(faction.Identifier, faction.Identifier == factionIdentifier);
}
}
public bool LayerExists(Identifier layer)
{
foreach (MapEntity me in MapEntity.MapEntityList)
{
if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; }
var layerAsIdentifier = me.Layer.ToIdentifier();
if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier))
{
me.HiddenInGame = factionIdentifier != layerAsIdentifier;
#if CLIENT
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
if (me.HiddenInGame && me is Item item)
{
foreach (var lightComponent in item.GetComponents<LightComponent>())
{
lightComponent.Light.Enabled = false;
}
}
#endif
}
if (me.Submarine == this || me.Layer == layer) { return true; }
}
return false;
}
public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false)
{
foreach (MapEntity me in MapEntity.MapEntityList)
{
if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this || me.Layer != layer) { continue; }
me.HiddenInGame = !enabled;
#if CLIENT
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
if (me.HiddenInGame && me is Item item)
{
foreach (var lightComponent in item.GetComponents<LightComponent>())
{
lightComponent.Light.Enabled = false;
}
}
#endif
}
#if SERVER
if (sendNetworkEvent)
{
GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled));
}
#endif
}
public void Update(float deltaTime)
@@ -1230,6 +1247,8 @@ namespace Barotrauma
public void NeutralizeBallast()
{
if (PhysicsBody.BodyType != BodyType.Dynamic) { return; }
float neutralBallastLevel = 0.5f;
int selectedSteeringValue = 0;
foreach (Item item in Item.ItemList)
@@ -1240,8 +1259,8 @@ namespace Barotrauma
//find how many pumps/engines in this sub the steering item is connected to
int steeringValue = 1;
Connection connectionX = item.GetComponent<ConnectionPanel>()?.Connections.Find(c => c.Name == "velocity_x_out");
Connection connectionY = item.GetComponent<ConnectionPanel>()?.Connections.Find(c => c.Name == "velocity_y_out");
Connection connectionX = item.GetComponent<ConnectionPanel>()?.Connections.Find(static c => c.Name == "velocity_x_out");
Connection connectionY = item.GetComponent<ConnectionPanel>()?.Connections.Find(static c => c.Name == "velocity_y_out");
if (connectionX != null)
{
foreach (Engine engine in steering.Item.GetConnectedComponentsRecursive<Engine>(connectionX))
@@ -1371,7 +1390,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns true if the sub is same as the other.
/// Returns true if the sub is same as the other, or connected to it via docking ports.
/// </summary>
public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub);
@@ -1459,22 +1478,39 @@ namespace Barotrauma
public static Rectangle GetBorders(XElement submarineElement)
{
Vector4 bounds = Vector4.Zero;
Vector4 bounds = new Vector4(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue);
foreach (XElement element in submarineElement.Elements())
{
if (element.Name != "Structure") { continue; }
if (element.Name == "Structure")
{
string name = element.GetAttributeString("name", "");
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = Structure.FindPrefab(name, identifier);
if (prefab == null || !prefab.Body) { continue; }
string name = element.GetAttributeString("name", "");
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
StructurePrefab prefab = Structure.FindPrefab(name, identifier);
if (prefab == null || !prefab.Body) { continue; }
var rect = element.GetAttributeRect("rect", Rectangle.Empty);
bounds = new Vector4(
Math.Min(rect.X, bounds.X),
Math.Max(rect.Y, bounds.Y),
Math.Max(rect.Right, bounds.Z),
Math.Min(rect.Y - rect.Height, bounds.W));
}
else if (element.Name == "LinkedSubmarine")
{
Point dimensions = element.GetAttributePoint("dimensions", Point.Zero);
Point pos = element.GetAttributeVector2("pos", Vector2.Zero).ToPoint();
bounds = new Vector4(
Math.Min(pos.X - dimensions.X / 2, bounds.X),
Math.Max(pos.Y + dimensions.Y / 2, bounds.Y),
Math.Max(pos.X + dimensions.X / 2, bounds.Z),
Math.Min(pos.Y - dimensions.Y / 2, bounds.W));
}
}
var rect = element.GetAttributeRect("rect", Rectangle.Empty);
bounds = new Vector4(
Math.Min(rect.X, bounds.X),
Math.Max(rect.Y, bounds.Y),
Math.Max(rect.Right, bounds.Z),
Math.Min(rect.Y - rect.Height, bounds.W));
if (bounds.X == float.MaxValue || bounds.Y == float.MinValue || bounds.Z == float.MinValue || bounds.W == float.MaxValue)
{
//no bounds found
return Rectangle.Empty;
}
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
@@ -1667,6 +1703,11 @@ namespace Barotrauma
}
}
foreach (Identifier layer in Info.LayersHiddenByDefault)
{
SetLayerEnabled(layer, enabled: false);
}
GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged.Register(upgradeEventIdentifier, _ => ResetCrushDepth());
#if CLIENT
@@ -1721,6 +1762,22 @@ namespace Barotrauma
realWorldCrushDepth = null;
}
/// <summary>
/// Normally crush depth is determined by the crush depths of the walls and upgrades applied on them.
/// This method forces the crush depths of all the walls to the specified value.
/// </summary>
/// </summary>
/// <param name="realWorldCrushDepth">Depth in "real world" units (meters from the surface of Europa, the value you see on the nav terminal).</param>
public void SetCrushDepth(float realWorldCrushDepth)
{
foreach (Structure structure in Structure.WallList)
{
if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; }
structure.CrushDepth = realWorldCrushDepth;
}
this.realWorldCrushDepth = realWorldCrushDepth;
}
public static void RepositionEntities(Vector2 moveAmount, IEnumerable<MapEntity> entities)
{
if (moveAmount.LengthSquared() < 0.00001f) { return; }
@@ -1777,6 +1834,10 @@ namespace Barotrauma
element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax));
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString()));
element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages)));
if (Info.LayersHiddenByDefault.Any())
{
element.Add(new XAttribute("layerhiddenbydefault", string.Join(", ", Info.LayersHiddenByDefault)));
}
if (Info.Type == SubmarineType.OutpostModule)
{
@@ -504,7 +504,10 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) { continue; }
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True)
{
continue;
}
foreach (Limb limb in c.AnimController.Limbs)
{
@@ -567,7 +570,7 @@ namespace Barotrauma
{
buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay);
}
return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f) * massRatio;
return new Vector2(0.0f, buoyancy * totalMass * 10.0f) * massRatio;
}
public void ApplyForce(Vector2 force)
@@ -684,9 +687,26 @@ namespace Barotrauma
private bool CheckCharacterCollision(Contact contact, Character character)
{
//characters that can't enter the sub always collide regardless of gaps
if (!character.AnimController.CanEnterSubmarine) { return true; }
if (character.Submarine != null) { return false; }
switch (character.AnimController.CanEnterSubmarine)
{
case CanEnterSubmarine.False:
//characters that can't enter the sub always collide regardless of gaps
return true;
case CanEnterSubmarine.Partial:
//characters that can partially enter the sub can poke their limbs inside, but not the collider
if (contact.FixtureB.Body ==
character.AnimController.Collider.FarseerBody)
{
return true;
}
if (contact.FixtureB.Body.UserData is Limb limb &&
!limb.Params.CanEnterSubmarine)
{
return true;
}
break;
}
contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
@@ -717,17 +737,23 @@ namespace Barotrauma
if (adjacentGap == null) { return true; }
}
if (newHull != null)
{
CoroutineManager.Invoke(() =>
{
if (character != null && !character.Removed)
{
character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true);
}
});
if (character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial)
{
return contact.FixtureB.Body == character.AnimController.Collider.FarseerBody;
}
else
{
if (newHull != null)
{
CoroutineManager.Invoke(() =>
{
if (character != null && !character.Removed)
{
character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true);
}
});
}
}
return false;
}
@@ -880,17 +906,12 @@ namespace Barotrauma
}
}
#if CLIENT
int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50);
for (int i = 0; i < particleAmount; i++)
{
GameMain.ParticleManager.CreateParticle("iceshards",
ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)),
Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity);
}
#endif
HandleLevelCollisionProjSpecific(impact);
}
partial void HandleLevelCollisionProjSpecific(Impact impact);
private void HandleSubCollision(Impact impact, Submarine otherSub)
{
Debug.Assert(otherSub != submarine);
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -234,6 +234,11 @@ namespace Barotrauma
public readonly Dictionary<Identifier, List<Character>> OutpostNPCs = new Dictionary<Identifier, List<Character>>();
/// <summary>
/// Names of layers that get automatically hidden when loading the sub
/// </summary>
public HashSet<Identifier> LayersHiddenByDefault { get; private set; } = new HashSet<Identifier>();
//constructors & generation ----------------------------------------------------
public SubmarineInfo()
{
@@ -319,6 +324,7 @@ namespace Barotrauma
IsManuallyOutfitted = original.IsManuallyOutfitted;
Tags = original.Tags;
OutpostGenerationParams = original.OutpostGenerationParams;
LayersHiddenByDefault = original.LayersHiddenByDefault;
if (original.OutpostModuleInfo != null)
{
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
@@ -385,6 +391,12 @@ namespace Barotrauma
RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0);
RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0);
var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier());
foreach (Identifier hiddenLayer in SubmarineElement.GetAttributeIdentifierArray("layerhiddenbydefault", Array.Empty<Identifier>()))
{
LayersHiddenByDefault.Add(hiddenLayer);
}
// Backwards compatibility
if (recommendedCrewExperience == "Beginner")
{
@@ -793,6 +805,13 @@ namespace Barotrauma
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
float price = Price;
// Adjust by campaign difficulty settings
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
price *= campaign.Settings.ShipyardPriceMultiplier;
}
if (characterList.Any())
{
if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
@@ -1060,7 +1060,8 @@ namespace Barotrauma
Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element))
{
spawnType = spawnType
spawnType = spawnType,
Layer = element.GetAttributeString(nameof(Layer), null)
};
string idCardDescString = element.GetAttributeString("idcarddesc", "");
@@ -1115,7 +1116,8 @@ namespace Barotrauma
element.Add(new XAttribute("ID", ID),
new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)),
new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)),
new XAttribute("spawn", spawnType));
new XAttribute("spawn", spawnType),
new XAttribute(nameof(Layer), Layer ?? string.Empty));
if (SpawnType == SpawnType.ExitPoint)
{
element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize)));
@@ -0,0 +1,193 @@
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
static class WreckConverter
{
private static readonly string[] itemsToRemove =
{
"circuitboxcomponent",
"wire",
};
public static XElement ConvertToWreck(XElement submarineElement)
{
ImmutableHashSet<Identifier> availableWreckContainerTags = ItemPrefab.Prefabs
.SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary)))
.Where(t => !ItemPrefab.Prefabs.ContainsKey(t) && t.StartsWith("wreck"))
.ToImmutableHashSet();
bool monsterSpawnPointCreated = false;
List<string> warnings = new List<string>();
var wreckElement = new XElement(submarineElement);
foreach (var element in wreckElement.Elements().ToList())
{
var identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
if (element.NameAsIdentifier() == "waypoint")
{
if (element.GetAttributeEnum("spawn", SpawnType.Path) != SpawnType.Human) { continue; }
if (element.GetAttributeIdentifier("job", Identifier.Empty) == Identifier.Empty)
{
element.SetAttributeValue("spawn", SpawnType.Enemy);
DebugConsole.NewMessage("Converted a non-job-specific spawnpoint to an enemy spawnpoint.");
monsterSpawnPointCreated = true;
}
else
{
element.SetAttributeValue("spawn", SpawnType.Corpse);
}
}
continue;
}
//remove if set to be removed
var tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>());
if (itemsToRemove.Any(it => tags.Contains(it.ToIdentifier())))
{
element.Remove();
continue;
}
bool tagsModified = false;
for (int i = 0; i < tags.Length; i++)
{
Identifier wreckTag = ("wreck" + tags[i]).ToIdentifier();
if (availableWreckContainerTags.Contains(wreckTag))
{
DebugConsole.NewMessage($"Replaced tag {tags[i]} with {wreckTag} in item \"{identifier}\".");
tags[i] = wreckTag;
tagsModified = true;
}
}
if (tagsModified)
{
element.SetAttributeValue("tags", string.Join(",", tags.Select(t => t.ToString())));
}
Identifier[] wreckedIdentifiers =
{
(identifier + "wrecked").ToIdentifier(),
(identifier + "_wrecked").ToIdentifier(),
};
//turn to wrecked version if one is available
foreach (var wreckedIdentifier in wreckedIdentifiers)
{
var wreckedPrefab = MapEntityPrefab.FindByIdentifier(wreckedIdentifier);
if (wreckedPrefab == null) { continue; }
var oldPrefab = MapEntityPrefab.FindByIdentifier(identifier);
element.SetAttributeValue("identifier", wreckedIdentifier);
float currentScale = element.GetAttributeFloat("scale", oldPrefab.Scale);
element.SetAttributeValue("scale", currentScale * (wreckedPrefab.Scale / oldPrefab.Scale));
if (wreckedPrefab is ItemPrefab wreckedItemPrefab)
{
//remove connections that don't exist in the wreck version
var originalConnectionPanelElement = element.GetChildElement(nameof(ConnectionPanel));
var wreckedConnectionPanelElement = wreckedItemPrefab.ConfigElement.GetChildElement(nameof(ConnectionPanel));
if (originalConnectionPanelElement != null && wreckedConnectionPanelElement != null)
{
foreach (var connectionElement in originalConnectionPanelElement.Elements().ToList())
{
var elementName = connectionElement.NameAsIdentifier();
if (elementName != "input" && elementName != "output") { continue; }
string connectionName = connectionElement.GetAttributeString("name", string.Empty);
if (wreckedConnectionPanelElement
.GetChildElements(connectionElement.Name.LocalName)
.None(c => c.GetAttributeString("name", string.Empty) == connectionName))
{
connectionElement.Remove();
}
}
}
}
else if (wreckedPrefab is StructurePrefab wreckedStructurePrefab)
{
//if the dimensions of the structures are different, rescale
//ignore small differences, they tend to be just irrelevant differences in how the sourcerect is scaled
const int MaximumSizeDifference = 5;
Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
if (!wreckedStructurePrefab.ResizeHorizontal)
{
if (Math.Abs(wreckedStructurePrefab.ScaledSize.X - rect.Width) > MaximumSizeDifference)
{
DebugConsole.NewMessage($"The prefab {wreckedStructurePrefab.Name} has different dimensions than the original one. Changing the width from {rect.Width} to {(int)wreckedStructurePrefab.ScaledSize.X}.", Color.Yellow);
}
rect.Width = (int)wreckedStructurePrefab.ScaledSize.X;
}
if (!wreckedStructurePrefab.ResizeVertical)
{
if (Math.Abs(wreckedStructurePrefab.ScaledSize.Y - rect.Height) > MaximumSizeDifference)
{
DebugConsole.NewMessage($"The prefab {wreckedStructurePrefab.Name} has different dimensions than the original one. Changing the height from {rect.Height} to {(int)wreckedStructurePrefab.ScaledSize.Y}.", Color.Yellow);
}
rect.Height = (int)wreckedStructurePrefab.ScaledSize.Y;
}
element.SetAttributeValue("rect", XMLExtensions.RectToString(rect));
}
break;
}
var itemContainerElement = element.GetChildElement(nameof(ItemContainer));
if (itemContainerElement != null)
{
string containedString = itemContainerElement.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(',');
var itemIds = new HashSet<ushort>();
foreach (string idListStr in itemIdStrings)
{
foreach (string idStr in idListStr.Split(';'))
{
if (int.TryParse(idStr, out int id)) { itemIds.Add((UInt16)id); }
}
}
if (itemIds.Any())
{
List<string> containedItemNames = new List<string>();
foreach (var itemElement in wreckElement.Elements())
{
var id = itemElement.GetAttributeUInt16("id", Entity.NullEntityID);
if (itemIds.Contains(id))
{
containedItemNames.Add(itemElement.GetAttributeString("identifier", string.Empty));
}
}
warnings.Add($"Potential issue in container \"{identifier}\". The following items are pre-placed, and may interfere with the loot generated in the wreck: " + string.Join(", ", containedItemNames));
}
}
//set to 0 condition if repairable, exclude doors and hatches
if (element.GetChildElement(nameof(Repairable)) != null && element.GetChildElement(nameof(Door)) == null)
{
element.SetAttributeValue("conditionpercentage", 0.0f);
}
}
foreach (var warning in warnings)
{
DebugConsole.AddWarning(warning);
}
if (!monsterSpawnPointCreated)
{
DebugConsole.ThrowError("There are no monster spawnpoints in the wreck. Remember to add some for monsters to spawn properly!");
}
return wreckElement;
}
}
}