v1.4.4.1 (Blood in the Water Update)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user