Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-12-11 10:44:53 -03:00
257 changed files with 4793 additions and 1653 deletions
@@ -86,7 +86,12 @@ namespace Barotrauma
public readonly UInt64 CreationIndex;
public string ErrorLine
=> $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}";
/// <summary>
/// Which content package is this entity from (if it's something like an item or a character that's loaded from a package, otherwise we assume it's the vanilla package).
/// </summary>
public virtual ContentPackage ContentPackage => GameMain.VanillaContent;
public Entity(Submarine submarine, ushort id)
{
this.Submarine = submarine;
@@ -317,9 +317,9 @@ namespace Barotrauma
Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f));
Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration);
}
foreach (Item item in Item.ItemList)
foreach (Sonar sonar in Sonar.SonarList)
{
item.GetComponent<Sonar>()?.RegisterExplosion(this, worldPosition);
sonar.RegisterExplosion(this, worldPosition);
}
#endif
@@ -35,6 +35,18 @@ namespace Barotrauma
public readonly float GlowEffectT;
private readonly List<Gap> overlappingGaps = new List<Gap>();
/// <summary>
/// Do we need to recheck which gaps are overlapping with this one, and how much they should reduce this gap's flow?
/// </summary>
private bool overlappingGapsDirty;
/// <summary>
/// How much overlapping gaps reduce the flow rate of this one?
/// </summary>
private float overlappingGapFlowRateReduction;
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
private float open;
@@ -68,22 +80,29 @@ namespace Barotrauma
set
{
if (float.IsNaN(value)) { return; }
float prevValue = open;
if (value > open)
{
openedTimer = 1.0f;
}
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
open = MathHelper.Clamp(value, 0.0f, 1.0f);
if (!MathUtils.NearlyEqual(open, prevValue))
{
if (value > open && value >= 1.0f)
overlappingGapsDirty = true;
FlagOverlappingGapsDirty();
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
{
InformWaypointsAboutGapState(this, open: true);
}
else if (value < open && open >= 1.0f)
{
InformWaypointsAboutGapState(this, open: false);
if (open > prevValue && open >= 1.0f)
{
InformWaypointsAboutGapState(this, open: true);
}
else if (open < prevValue && prevValue >= 1.0f)
{
InformWaypointsAboutGapState(this, open: false);
}
}
}
open = MathHelper.Clamp(value, 0.0f, 1.0f);
static void InformWaypointsAboutGapState(Gap gap, bool open)
{
@@ -206,7 +225,7 @@ namespace Barotrauma
Physics.CollisionWall,
Physics.CollisionCharacter,
findNewContacts: false);
outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})";
outsideCollisionBlocker.UserData = this;
outsideCollisionBlocker.Enabled = false;
#if CLIENT
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
@@ -339,7 +358,11 @@ namespace Barotrauma
{
if (hulls[i] == null) { continue; }
linkedTo.Add(hulls[i]);
if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this);
if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); }
foreach (var gap in hulls[i].ConnectedGaps)
{
gap.overlappingGapsDirty = true;
}
}
}
@@ -365,6 +388,12 @@ namespace Barotrauma
deltaTime *= updateCount;
updateCount = 0;
if (overlappingGapsDirty)
{
RefreshOverlappingGaps();
overlappingGapsDirty = false;
}
flowForce = Vector2.Zero;
outsideColliderRaycastTimer -= deltaTime;
@@ -432,7 +461,7 @@ namespace Barotrauma
//a variable affecting the water flow through the gap
//the larger the gap is, the faster the water flows
float sizeModifier = Size / 100.0f * open;
float sizeModifier = Size / 100.0f * open * (1.0f - overlappingGapFlowRateReduction);
//horizontal gap (such as a regular door)
if (IsHorizontal)
@@ -598,7 +627,7 @@ namespace Barotrauma
{
//a variable affecting the water flow through the gap
//the larger the gap is, the faster the water flows
float sizeModifier = Size * open * open;
float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction);
float delta = 500.0f * sizeModifier * deltaTime;
@@ -795,6 +824,52 @@ namespace Barotrauma
return null;
}
private void RefreshOverlappingGaps()
{
overlappingGapFlowRateReduction = 0.0f;
overlappingGaps.Clear();
foreach (var linked in linkedTo)
{
if (linked is not Hull hull) { continue; }
foreach (var connectedGap in hull.ConnectedGaps)
{
if (connectedGap == this) { continue; }
//let the "more open" gap reduce this gap's flow rate
//or if they're both equally open, let the one that was created first handle it
//(note that we can't use Entity.ID here because gaps on walls don't have IDs)
if (connectedGap.open > open ||
(connectedGap.open == open && connectedGap.CreationIndex < CreationIndex))
{
Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect);
if (intersection.Width > 0 && intersection.Height > 0)
{
//reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is
float relativeOverlap = IsHorizontal ?
intersection.Height / (float)rect.Height :
intersection.Width / (float)rect.Width;
overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open;
}
}
if (overlappingGapFlowRateReduction >= 1.0f)
{
overlappingGapFlowRateReduction = 1.0f;
break;
}
}
}
}
/// <summary>
/// Mark all gaps that are currently known to overlap with this one as needing a refresh of overlapping gaps
/// </summary>
private void FlagOverlappingGapsDirty()
{
foreach (var overlappingGap in overlappingGaps)
{
overlappingGap.overlappingGapsDirty = true;
}
}
public override void ShallowRemove()
{
base.ShallowRemove();
@@ -303,7 +303,11 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(value)) { return; }
waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; }
if (waterVolume <= Volume)
{
//recalculate pressure, but only if there's less water than the volume, above that point the "overpressure" logic kicks in
Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
}
if (waterVolume > 0.0f)
{
update = true;
@@ -1,4 +1,8 @@
using Microsoft.Xna.Framework;
using System;
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -8,6 +12,112 @@ namespace Barotrauma
Vector2 WorldPosition { get; }
Vector2 SimPosition { get; }
Submarine Submarine { get; }
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
if (seeingEntity is Character seeingCharacter)
{
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
}
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
public static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null || target.Removed) { return false; }
if (seeingEntity == null) { return false; }
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
Limb leftExtremity = null, rightExtremity = null;
float leftMostDot = 0.0f, rightMostDot = 0.0f;
Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
foreach (Limb limb in target.AnimController.Limbs)
{
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
if (limb.Hidden) { continue; }
Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
float leftDot = Vector2.Dot(limbDir, leftDir);
if (leftDot > leftMostDot)
{
leftMostDot = leftDot;
leftExtremity = limb;
continue;
}
float rightDot = Vector2.Dot(limbDir, rightDir);
if (rightDot > rightMostDot)
{
rightMostDot = rightDot;
rightExtremity = limb;
}
}
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
}
return false;
}
public static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
if (seeingEntity == null) { return false; }
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
if (checkFacing && seeingEntity is Character seeingCharacter)
{
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
}
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
{
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//we're outside, the other character inside
else if (seeingEntity.Submarine == null)
{
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//both inside different subs
else
{
return
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
bool IsBlocking(Fixture f)
{
var body = f.Body;
if (body == null) { return false; }
if (body.UserData is Structure wall)
{
if (!wall.CastShadow && seeThroughWindows) { return false; }
return wall != target;
}
else if (body.UserData is Item item)
{
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
{
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
}
return item != target;
}
return true;
}
}
}
interface IIgnorable : ISpatialEntity
@@ -1,4 +1,4 @@
using System;
using System;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -21,6 +21,8 @@ namespace Barotrauma
public float ActualMaxDifficulty => maxDifficulty;
public float AdjustedMaxDifficulty => maxDifficulty - 0.1f;
public readonly float ExperienceFromMissionRewards;
public readonly ImmutableHashSet<int> AllowedZones;
@@ -50,6 +52,10 @@ namespace Barotrauma
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0);
maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
float baseExperience = 0.09f;
float difficultyRewardMultiplier = 0.25f;
float calculateDefaultExperience = baseExperience + MinDifficulty * difficultyRewardMultiplier / 100;
ExperienceFromMissionRewards = element.GetAttributeFloat("ExperienceFromMissionRewards", calculateDefaultExperience);
var submarineAvailabilityOverrides = new HashSet<SubmarineAvailability>();
if (element.GetChildElement("submarines") is ContentXElement availabilityElement)
@@ -217,8 +217,13 @@ namespace Barotrauma
/// Makes the cell rounder by subdividing the edges and offsetting them at the middle
/// </summary>
/// <param name="minEdgeLength">How small the individual subdivided edges can be (smaller values produce rounder shapes, but require more geometry)</param>
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f)
/// <param name="minThickness">How "thin" irregularity is allowed to make parts of the cell. Very high irregularity values can lead to thin "spikes" or even parts where the "spike's" thickness becomes negative and wall segments intersect each other.</param>
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f, float minThickness = 0.0f)
{
//we need to make sure the vertices of the wall are still ordered counter-clockwise -
//if we deform some vertices so much the cell becomes concave, rendering the triangles will break (parts of the inside of the wall will render outside the edges)
var compareCCW = new CompareCCW(cell.Center);
List<GraphEdge> tempEdges = new List<GraphEdge>();
foreach (GraphEdge edge in cell.Edges)
{
@@ -231,8 +236,10 @@ namespace Barotrauma
Vector2 edgeDiff = edge.Point2 - edge.Point1;
Vector2 edgeDir = Vector2.Normalize(edgeDiff);
float maxExtrusion = float.PositiveInfinity;
const float minPassageWidth = 200.0f;
//If the edge is next to an empty cell and there's another solid cell at the other side of the empty one,
//don't touch this edge. Otherwise we may end up closing off small passages between cells.
//we need to calculate how far we can extrude the edge so it doesn't end up closing off small passages between cells.
var adjacentEmptyCell = edge.AdjacentCell(cell);
if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; }
if (adjacentEmptyCell != null)
@@ -252,8 +259,15 @@ namespace Barotrauma
}
if (adjacentEdge != null)
{
tempEdges.Add(edge);
continue;
maxExtrusion =
new[]
{
Vector2.Distance(edge.Point1, adjacentEdge.Point1),
Vector2.Distance(edge.Point1, adjacentEdge.Point2),
Vector2.Distance(edge.Point1, adjacentEdge.Point2),
Vector2.Distance(edge.Point2, adjacentEdge.Point1),
}.Min();
maxExtrusion = Math.Max(0, maxExtrusion - minPassageWidth);
}
}
List<Vector2> edgePoints = new List<Vector2>();
@@ -274,12 +288,53 @@ namespace Barotrauma
else
{
//value that's 0 at edges, 0.5 at center
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient);
Vector2 extrudedPoint =
//make the value "curve" from 0 to 1 at the center, instead of going linearly from 0 to 1
centerF = MathF.Sin(centerF * MathHelper.Pi);
//magic number intended to make old rounding values behave roughly the same with the new formula
//previously the extrusion increased linearly towards the center, forming a "spike" like "/\"
//now it follows a sine curve, which makes lower values produce rounder results
const float RoundingScale = 0.25f;
//magic number intended to make old variance values behave roughly the same with the new formula
//previously a value of 1 would allow a maximum extrusion of 50% of the edge's length at the center,
//now we extrude by the variance at any point on the edge (not more in the center)
const float RandomVarianceScale = 0.25f;
float randomVariance = irregularity * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
float extrusionAmount = edgeLength * ((roundingAmount * RoundingScale * centerF) + randomVariance * RandomVarianceScale);
extrusionAmount = Math.Min(extrusionAmount, maxExtrusion);
Vector2 nonExtrudedPoint =
edge.Point1 +
edgeDiff * (i / (float)pointCount) +
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
edgeDiff * (i / (float)pointCount);
Vector2 nextPoint =
edge.Point1 +
edgeDiff * ((i + 1) / (float)pointCount);
//"extruding" inwards, need to make sure we don't make the edge poke through the cell from the other side
if (extrusionAmount < 0 && minThickness > 0.0f)
{
foreach (GraphEdge otherEdge in cell.Edges)
{
if (otherEdge == edge) { continue; }
float margin = minThickness * Math.Sign(extrusionAmount);
if (MathUtils.GetLineIntersection(
nonExtrudedPoint, nonExtrudedPoint + edgeNormal * (extrusionAmount + margin),
otherEdge.Point1, otherEdge.Point2, areLinesInfinite: false, out Vector2 intersection))
{
extrusionAmount = Math.Min(extrusionAmount, Vector2.Distance(edge.Point1, intersection)) - margin;
//make sure we don't "overshoot", fix the inwards extrusion by instead extruding too much outwards
//(can happen on small cells in caves for example)
extrusionAmount = Math.Min(extrusionAmount, edge.Length / 2);
}
}
}
Vector2 extrudedPoint = nonExtrudedPoint + edgeNormal * extrusionAmount;
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
bool isInside = false;
@@ -306,14 +361,29 @@ namespace Barotrauma
}
if (isInside) { break; }
}
if (isInside) { continue; }
if (!isInside)
{
edgePoints.Add(extrudedPoint);
}
//if adding the point would deform the edge so much that the normal of the new edge would point
//in the opposite direction from the undeformed edge's normal, don't allow adding the point
//(that would lead to the edge being "inside out", the wall texture and objects on the wall pointing inwards)
bool isNormalInverted =
Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, edgePoints.Last(), extrudedPoint)) < 0 ||
//check that the edge at the other side of the new point doesn't get inverted either
Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, extrudedPoint, edge.Point2)) < 0;
if (isNormalInverted) { continue; }
//make sure extruding the point doesn't change the vertex order
//(they're assumed to be sorted counter-clockwise, and if they're not, the triangles will generate incorrectly)
bool vertexOrderChanged =
compareCCW.Compare(edgePoints.Last(), nonExtrudedPoint) != compareCCW.Compare(edgePoints.Last(), extrudedPoint) ||
compareCCW.Compare(nonExtrudedPoint, nextPoint) != compareCCW.Compare(extrudedPoint, nextPoint);
if (vertexOrderChanged) { continue; }
edgePoints.Add(extrudedPoint);
}
}
for (int i = 0; i < edgePoints.Count - 1; i++)
{
tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
@@ -376,19 +446,7 @@ namespace Barotrauma
continue;
}
Vector2 minVert = tempVertices[0];
Vector2 maxVert = tempVertices[0];
foreach (var vert in tempVertices)
{
minVert = new Vector2(
Math.Min(minVert.X, vert.X),
Math.Min(minVert.Y, vert.Y));
maxVert = new Vector2(
Math.Max(maxVert.X, vert.X),
Math.Max(maxVert.Y, vert.Y));
}
Vector2 center = (minVert + maxVert) / 2;
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center));
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
if (bodyPoints.Count < 2) { continue; }
@@ -411,7 +469,7 @@ namespace Barotrauma
if (cell.CellType == CellType.Empty) { continue; }
cellBody.UserData = cell;
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center));
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
for (int i = 0; i < triangles.Count; i++)
{
@@ -1205,7 +1205,8 @@ namespace Barotrauma
CaveGenerator.RoundCell(cell,
minEdgeLength: GenerationParams.CellSubdivisionLength,
roundingAmount: GenerationParams.CellRoundingAmount,
irregularity: GenerationParams.CellIrregularity);
irregularity: GenerationParams.CellIrregularity,
minThickness: GenerationParams.WallTextureExpandInwardsAmount);
}
}
@@ -1278,12 +1279,22 @@ namespace Barotrauma
Debug.Assert(triangleLists.Count == cellBatches.Count);
for (int i = 0; i < triangleLists.Count; i++)
{
//the solid black inner part of the wall
var wallVerts = CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells,
expandOutwards: 0.0f, expandInwards: GenerationParams.WallTextureExpandInwardsAmount,
outerColor: GenerationParams.WallColor, innerColor: Color.Black,
this, zCoord: 0.9f, preventExpandThroughCell: true).ToArray();
CaveGenerator.GenerateTextureCoordinates(wallVerts, GenerationParams.WallTextureSize);
renderer.SetVertices(
CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(),
CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(),
wallVerts,
CaveGenerator.GenerateWallEdgeVertices(
cellBatches[i].cells,
GenerationParams.WallEdgeExpandOutwardsAmount, GenerationParams.WallEdgeExpandInwardsAmount,
outerColor: GenerationParams.WallColor, innerColor: GenerationParams.WallColor,
this, zCoord: 0.9f).ToArray(),
CaveGenerator.GenerateWallVertices(triangleLists[i], Color.Black, zCoord: 0.9f).ToArray(),
cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture,
cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture,
GenerationParams.WallColor);
cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture);
}
#endif
@@ -2808,6 +2819,7 @@ namespace Barotrauma
if (l.Cell == null || l.Edge == null) { return false; }
if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; }
if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; }
if (l.Edge.Length < itemPrefab.Size.X) { return false; }
if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; }
return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
@@ -2839,6 +2851,7 @@ namespace Barotrauma
{
if (l.Cell == null || l.Edge == null) { return false; }
if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; }
if (l.Edge.Length < selectedPrefab.Size.X) { return false; }
l.InitializeResources();
return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _);
}, randSync: Rand.RandSync.ServerAndClient);
@@ -3375,37 +3388,41 @@ namespace Barotrauma
private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List<Item> placedResources,
float? edgeLength = null, float maxResourceOverlap = 0.4f)
{
edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
edgeLength ??= location.Edge.Length;
Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value;
if (!MathUtils.IsValid(edgeDir))
{
edgeDir = Vector2.Zero;
}
var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
float minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap);
var lerpAmounts = new float[resourceCount];
float[] lerpAmounts = new float[resourceCount];
lerpAmounts[0] = 0.0f;
var lerpAmount = 0.0f;
float lerpAmount = 0.0f;
for (int i = 1; i < resourceCount; i++)
{
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value;
lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
float overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
lerpAmount = Math.Clamp(lerpAmount + (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value, 0.0f, 1.0f);
lerpAmounts[i] = lerpAmount;
}
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
placedResources = new List<Item>();
for (int i = 0; i < resourceCount; i++)
{
Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
Vector2 selectedPos =
location.Edge.Length < resourcePrefab.Size.X ?
location.Edge.Center :
Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
var item = new Item(resourcePrefab, selectedPos, submarine: null);
Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
item.Move(edgeNormal * moveAmount);
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
if (item.GetComponent<Holdable>() is Holdable h)
{
h.AttachToWall();
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
}
else if (item.body != null)
{
@@ -3914,7 +3931,7 @@ namespace Barotrauma
attempt++;
spawnPoint = wayPoint.WorldPosition;
success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
positionHistory.Add($"{info.Name}: {attempt}", positions.ToList());
positionHistory.TryAdd($"{info.Name}: {attempt}", positions.ToList());
positions.Clear();
if (success)
{
@@ -3940,6 +3957,11 @@ namespace Barotrauma
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub));
foreach (Hull hull in sub.GetHulls(false))
{
if (hull.WaterPercentage > 0)
{
// Don't override the water level set by the sub designer
continue;
}
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance)
{
hull.WaterVolume =
@@ -4296,7 +4318,7 @@ namespace Barotrauma
if (LevelData.ForceWreck != null)
{
//force the desired wreck to be chosen first
var matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath);
PlaceableWreck matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath);
if (matchingFile.WreckFile != null)
{
placeableWrecks.Remove(matchingFile);
@@ -4337,7 +4359,12 @@ namespace Barotrauma
{
var placeableWreck = placeableWrecks.First();
var wreckFile = placeableWreck.WreckFile;
placeableWrecks.RemoveAt(0);
if (LevelData.ForceWreck == null)
{
// If a wreck is forced, don't remove it -> only spawns those wrecks (makes testing them in the editor easier).
// Normally we don't want two instances of the same wreck to spawn in the same level, but when we test or debug certain wrecks, we want only them.
placeableWrecks.RemoveAt(0);
}
LevelData.ThalamusSpawn thalamusSpawn = requireThalamus ? LevelData.ThalamusSpawn.Forced : LevelData.ThalamusSpawn.Random;
if (LevelData.ForceWreck != null) { thalamusSpawn = LevelData.ForceThalamus; }
@@ -4783,40 +4810,13 @@ namespace Barotrauma
#endif
}
}
else if (GameMain.NetworkMember is not { IsClient: true })
{
bool allowDisconnectedWires = true;
bool allowDamagedDevices = true;
bool allowDamagedWalls = true;
if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info)
{
allowDisconnectedWires = info.AllowDisconnectedWires;
allowDamagedWalls = info.AllowDamagedWalls;
allowDamagedDevices = info.AllowDamagedDevices;
}
//remove wires
float disconnectWireMinDifficulty = 20.0f;
float disconnectWireProbability = MathUtils.InverseLerp(disconnectWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f;
if (disconnectWireProbability > 0.0f && allowDisconnectedWires)
{
DisconnectBeaconStationWires(disconnectWireProbability);
}
if (allowDamagedDevices)
{
DamageBeaconStationDevices(breakDeviceProbability: 0.5f);
}
if (allowDamagedWalls)
{
DamageBeaconStationWalls(damageWallProbability: 0.25f);
}
}
SetLinkedSubCrushDepth(BeaconStation);
}
public void DisconnectBeaconStationWires(float disconnectWireProbability)
{
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDisconnectedWires: false }) { return; }
if (disconnectWireProbability <= 0.0f) { return; }
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
@@ -4852,6 +4852,8 @@ namespace Barotrauma
public void DamageBeaconStationDevices(float breakDeviceProbability)
{
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDamagedDevices: false }) { return; }
if (breakDeviceProbability <= 0.0f) { return; }
//break powered items
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
@@ -4867,6 +4869,8 @@ namespace Barotrauma
public void DamageBeaconStationWalls(float damageWallProbability)
{
if (BeaconStation?.Info?.BeaconStationInfo is { AllowDamagedWalls: false }) { return; }
if (damageWallProbability <= 0.0f) { return; }
//poke holes in the walls
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
@@ -236,7 +236,7 @@ namespace Barotrauma
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
public float CellRoundingAmount
{
@@ -247,7 +247,7 @@ namespace Barotrauma
}
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
public float CellIrregularity
{
@@ -525,19 +525,19 @@ namespace Barotrauma
[Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int MaxCorpseCount { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float HuskProbability { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float ThalamusProbability { get; set; }
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float WreckHullFloodingChance { get; set; }
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float WreckFloodingHullMinWaterPercentage { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float WreckFloodingHullMaxWaterPercentage { get; set; }
#endregion
@@ -602,6 +602,13 @@ namespace Barotrauma
private set;
}
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "How deep inside the walls the wall texture extends to before fading to black."), Editable(minValue: 0.0f, maxValue: 10000.0f)]
public float WallTextureExpandInwardsAmount
{
get;
private set;
}
[Header("Colors")]
[Serialize("27,30,36", IsPropertySaveable.Yes), Editable]
public Color AmbientLightColor
@@ -339,11 +339,11 @@ namespace Barotrauma
InitProjSpecific(element);
}
//use the maximum width of the sprite as the minimum surface width if no value is given
if (element != null && !element.Attributes("minsurfacewidth").Any())
//use (a bit less than) the maximum width of the sprite as the minimum surface width if no value is given
if (element != null && element.GetAttribute("minsurfacewidth") == null)
{
if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize;
if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize);
if (Sprites.Any()) { MinSurfaceWidth = Sprites[0].size.X * MaxSize * 0.8f; }
if (DeformableSprite != null) { MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize * 0.8f); }
}
}
@@ -609,6 +609,21 @@ namespace Barotrauma
return;
}
if (PhysicsBody != null)
{
if (currentForceFluctuation <= 0.0f && statusEffects.None() && attacks.None())
{
//no force atm, and no status effects or attacks the trigger could apply
// -> we can disable the collider and get a minor physics performance improvement
PhysicsBody.Enabled = false;
return;
}
else
{
PhysicsBody.Enabled = true;
}
}
foreach (Entity triggerer in triggerers)
{
if (triggerer.Removed) { continue; }
@@ -124,8 +124,17 @@ namespace Barotrauma
/// </summary>
public int PriceModifier { get; set; }
public Location Location { get; }
/// <summary>
/// The maximum effect positive reputation can have on store prices (e.g. 0.5 = 50% discount with max reputation).
/// </summary>
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
/// <summary>
/// The maximum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation).
/// </summary>
private float MinReputationModifier => Location.StoreMinReputationModifier;
private StoreInfo(Location location)
{
Location = location;
@@ -343,7 +352,7 @@ namespace Barotrauma
if (characters.Any())
{
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag)));
}
// Price should never go below 1 mk
@@ -373,7 +382,7 @@ namespace Barotrauma
}
else
{
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
return MathHelper.Lerp(1.0f, 1.0f + MinReputationModifier, reputation.Value / reputation.MinReputation);
}
}
else
@@ -384,7 +393,7 @@ namespace Barotrauma
}
else
{
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
return MathHelper.Lerp(1.0f, 1.0f - MinReputationModifier, reputation.Value / reputation.MinReputation);
}
}
}
@@ -398,6 +407,7 @@ namespace Barotrauma
public Dictionary<Identifier, StoreInfo> Stores { get; set; }
private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
private float StoreMinReputationModifier => Type.StoreMinReputationModifier;
private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
@@ -118,6 +118,7 @@ namespace Barotrauma
}
public float StoreMaxReputationModifier { get; } = 0.1f;
public float StoreMinReputationModifier { get; } = 1.0f;
public float StoreSellPriceModifier { get; } = 0.3f;
public float DailySpecialPriceModifier { get; } = 0.5f;
public float RequestGoodPriceModifier { get; } = 2f;
@@ -264,6 +265,7 @@ namespace Barotrauma
break;
case "store":
StoreMaxReputationModifier = subElement.GetAttributeFloat("maxreputationmodifier", StoreMaxReputationModifier);
StoreMinReputationModifier = subElement.GetAttributeFloat("minreputationmodifier", StoreMaxReputationModifier);
StoreSellPriceModifier = subElement.GetAttributeFloat("sellpricemodifier", StoreSellPriceModifier);
DailySpecialPriceModifier = subElement.GetAttributeFloat("dailyspecialpricemodifier", DailySpecialPriceModifier);
RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier);
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -301,12 +302,13 @@ namespace Barotrauma
protected void LoadDescription(ContentXElement element)
{
Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
string originalDescription = Description.Value;
if (descriptionIdentifier != Identifier.Empty)
const string descriptionIdentifierAttributeName = "descriptionidentifier";
XAttribute descriptionIdenfifierAttribute = element.GetAttribute(descriptionIdentifierAttributeName);
if (descriptionIdenfifierAttribute != null)
{
Identifier descriptionIdentifier = element.GetAttributeIdentifier(descriptionIdentifierAttributeName, Identifier.Empty);
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}");
}
else if (nameIdentifier == Identifier.Empty)
@@ -115,7 +115,11 @@ namespace Barotrauma
{
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
}
if (!generationParams.OutpostTag.IsEmpty)
if (generationParams.OutpostTag.IsEmpty)
{
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None());
}
else
{
if (outpostInfos.Any(o => o.OutpostTags.Contains(generationParams.OutpostTag)))
{
@@ -448,6 +452,8 @@ namespace Barotrauma
entities[selectedModule] = moduleEntities;
}
int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height)));
bool overlapsFound = true;
int iteration = 0;
while (overlapsFound)
@@ -465,7 +471,7 @@ namespace Barotrauma
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0)
{
overlapsFound = true;
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary<PlacedModule, Vector2> solution))
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
{
foreach (KeyValuePair<PlacedModule, Vector2> kvp in solution)
{
@@ -909,7 +915,12 @@ namespace Barotrauma
/// <param name="allmodules">All generated modules</param>
/// <param name="solution">The solution to the overlap (if any). Key = placed module, value = distance to move the module</param>
/// <returns>Was a solution found for resolving the overlap.</returns>
private static bool FindOverlapSolution(IEnumerable<PlacedModule> movableModules, PlacedModule module1, PlacedModule module2, IEnumerable<PlacedModule> allmodules, out Dictionary<PlacedModule, Vector2> solution)
private static bool FindOverlapSolution(
IEnumerable<PlacedModule> movableModules,
PlacedModule module1, PlacedModule module2,
IEnumerable<PlacedModule> allmodules,
int maxMoveAmount,
out Dictionary<PlacedModule, Vector2> solution)
{
solution = new Dictionary<PlacedModule, Vector2>();
foreach (PlacedModule module in movableModules)
@@ -925,7 +936,6 @@ namespace Barotrauma
Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
Vector2 moveStep = moveDir * 50.0f;
Vector2 currentMove = Vector2.Zero;
float maxMoveAmount = 2000.0f;
List<PlacedModule> subsequentModules2 = new List<PlacedModule>();
GetSubsequentModules(module, movableModules, ref subsequentModules2);
@@ -53,6 +53,8 @@ namespace Barotrauma
const float LeakThreshold = 0.1f;
const float BigGapThreshold = 0.7f;
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
#if CLIENT
public SpriteEffects SpriteEffects = SpriteEffects.None;
#endif
@@ -1522,7 +1522,14 @@ namespace Barotrauma
if (entity.Submarine == null) { return false; }
if (includingConnectedSubs)
{
return GetConnectedSubs().Any(s => s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type));
// Performance-sensitive code -> implemented without Linq.
foreach (Submarine s in connectedSubs)
{
if (s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type))
{
return true;
}
}
}
return false;
}
@@ -1938,8 +1945,8 @@ namespace Barotrauma
{
bool hasThalamus = false;
var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity).ToImmutableHashSet();
var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct().ToImmutableHashSet();
var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity);
var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct();
foreach (ItemPrefab prefab in prefabsOnSub)
{
@@ -2077,7 +2084,6 @@ namespace Barotrauma
#if CLIENT
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
depthSortedDamageable.Clear();
#endif
var _loaded = new List<Submarine>(loaded);
foreach (Submarine sub in _loaded)
@@ -504,10 +504,8 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True)
{
continue;
}
//character inside some sub, no need to displace
if (c.Submarine != null) { continue; }
foreach (Limb limb in c.AnimController.Limbs)
{
@@ -525,13 +523,11 @@ namespace Barotrauma
continue;
}
//"+ translatedir" in order to move the character slightly away from the wall
c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir);
return;
break;
}
}
}