v0.12.0.2

This commit is contained in:
Joonas Rikkonen
2021-02-10 17:08:21 +02:00
parent 5c80a59bdd
commit 694cdfee7b
353 changed files with 12897 additions and 5028 deletions
@@ -91,6 +91,9 @@ namespace Barotrauma.MapCreatures.Behavior
public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
#endif
private static List<BallastFloraBehavior> _entityList = new List<BallastFloraBehavior>();
public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
public enum NetworkHeader
{
Spawn,
@@ -199,6 +202,9 @@ namespace Barotrauma.MapCreatures.Behavior
[Serialize(5f, true, "How much damage is taken from open fires")]
public float FireVulnerability { get; set; }
[Serialize(0.5f, true, "How much resistance against fire is gained while submerged.")]
public float SubmergedWaterResistance { get; set; }
[Serialize(0.8f, true, "What depth the branches will be drawn on")]
public float BranchDepth { get; set; }
@@ -295,6 +301,7 @@ namespace Barotrauma.MapCreatures.Behavior
LoadPrefab(prefab.Element);
StateMachine = new BallastFloraStateMachine(this);
if (firstGrowth) { GenerateStem(); }
_entityList.Add(this);
}
partial void LoadPrefab(XElement element);
@@ -427,7 +434,8 @@ namespace Barotrauma.MapCreatures.Behavior
if (GameMain.DebugDraw)
{
GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, GetWorldPosition() + branch.Position, Vector2.UnitY * 10.0f, 3f, playSound: false);
var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position;
GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1);
}
#elif SERVER
SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage);
@@ -469,7 +477,7 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
}
UpdateSelfDamage(deltaTime);
if (Anger > 1f)
@@ -862,6 +870,7 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
// damage is handled server side currently
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -875,7 +884,7 @@ namespace Barotrauma.MapCreatures.Behavior
{
if (IsInWater(branch))
{
return;
damage *= 1f - SubmergedWaterResistance;
}
if (defenseCooldown <= 0)
@@ -883,24 +892,24 @@ namespace Barotrauma.MapCreatures.Behavior
if (!(StateMachine.State is DefendWithPumpState))
{
StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker));
defenseCooldown = 60f;
defenseCooldown = 180f;
}
defenseCooldown = 10f;
}
}
branch.AccumulatedDamage += amount;
branch.AccumulatedDamage += damage;
branch.Health -= amount;
branch.Health -= damage;
if (type != AttackType.Other)
{
Anger += amount * 0.001f;
Anger += damage * 0.001f;
}
#if SERVER
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, amount);
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
#endif
if (branch.Health < 0)
@@ -923,6 +932,8 @@ namespace Barotrauma.MapCreatures.Behavior
{
target.Infector = null;
}
_entityList.Remove(this);
}
public void RemoveBranch(BallastFloraBranch branch)
@@ -1028,6 +1039,8 @@ namespace Barotrauma.MapCreatures.Behavior
target.Infector = null;
}
StateMachine?.State?.Exit();
// clean up leftover (can probably be removed)
foreach (Body body in bodies)
{
@@ -12,8 +12,9 @@ namespace Barotrauma
public const ushort NullEntityID = 0;
public const ushort EntitySpawnerID = ushort.MaxValue;
public const ushort RespawnManagerID = ushort.MaxValue - 1;
public const ushort DummyID = ushort.MaxValue - 2;
public const ushort ReservedIDStart = ushort.MaxValue - 2;
public const ushort ReservedIDStart = ushort.MaxValue - 3;
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static IEnumerable<Entity> GetEntities()
@@ -25,6 +25,7 @@ namespace Barotrauma
private readonly float screenColorRange, screenColorDuration;
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
private bool playTinnitus;
private bool applyFireEffects;
private readonly float flashDuration;
private readonly float? flashRange;
@@ -63,6 +64,8 @@ namespace Barotrauma
underwaterBubble = element.GetAttributeBool("underwaterbubble", true);
smoke = element.GetAttributeBool("smoke", true);
playTinnitus = element.GetAttributeBool("playtinnitus", true);
applyFireEffects = element.GetAttributeBool("applyfireeffects", flames);
flash = element.GetAttributeBool("flash", true);
@@ -225,7 +228,7 @@ namespace Barotrauma
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
{
if (attack.Range <= 0.0f) { return; }
@@ -323,10 +326,10 @@ namespace Barotrauma
}
}
if (c == Character.Controlled && !c.IsDead)
if (c == Character.Controlled && !c.IsDead && playTinnitus)
{
Limb head = c.AnimController.GetLimb(LimbType.Head);
if (damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor))
if (head != null && damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor))
{
PlayTinnitusProjSpecific(headFactor);
}
@@ -299,7 +299,7 @@ namespace Barotrauma
//GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between)
if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange)
{
return;
continue;
}
float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered && !l.Hidden);
@@ -346,11 +346,17 @@ namespace Barotrauma
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
Item container = item.Container;
bool fireProof = false;
while (container != null)
{
if (container.FireProof) return;
if (container.FireProof)
{
fireProof = true;
break;
}
container = container.Container;
}
if (fireProof) { continue; }
float range = (float)Math.Sqrt(size.X) * 10.0f;
if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) { continue; }
@@ -1,11 +1,9 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Xml.Linq;
namespace Barotrauma
@@ -120,10 +118,17 @@ namespace Barotrauma
return "Gap";
}
}
public Gap(MapEntityPrefab prefab, Rectangle rectangle)
: this (rectangle, Submarine.MainSub)
{ }
: this(rectangle, Submarine.MainSub)
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
}
#endif
}
public Gap(Rectangle rect, Submarine submarine)
: this(rect, rect.Width < rect.Height, submarine)
@@ -233,6 +238,13 @@ namespace Barotrauma
{
Hull[] hulls = new Hull[2];
foreach (var linked in linkedTo)
{
if (linked is Hull hull)
{
hull.ConnectedGaps.Remove(this);
}
}
linkedTo.Clear();
Vector2[] searchPos = new Vector2[2];
@@ -595,7 +607,7 @@ namespace Barotrauma
}
Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition);
Vector2 rayEnd = rayStart + rayDir * 500.0f;
Vector2 rayEnd = rayStart + rayDir * 5.0f;
var levelCells = Level.Loaded.GetCells(WorldPosition, searchDepth: 1);
foreach (var cell in levelCells)
@@ -397,7 +397,12 @@ namespace Barotrauma
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
: this (prefab, rectangle, Submarine.MainSub)
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
}
#endif
}
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID)
@@ -791,7 +796,7 @@ namespace Barotrauma
//make waves propagate through horizontal gaps
foreach (Gap gap in ConnectedGaps)
{
if (this != gap.linkedTo[0] as Hull)
if (this != gap.linkedTo.FirstOrDefault() as Hull)
{
//let the first linked hull handle the water propagation
continue;
@@ -8,6 +8,11 @@ namespace Barotrauma
Vector2 WorldPosition { get; }
Vector2 SimPosition { get; }
Submarine Submarine { get; }
bool IgnoreByAI => false;
}
interface IIgnorable : ISpatialEntity
{
bool IgnoreByAI { get; }
bool OrderedToBeIgnored { get; set; }
}
}
@@ -65,8 +65,21 @@ namespace Barotrauma
var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase));
if (containerElement == null) { continue; }
var itemIds = containerElement.GetAttributeIntArray("contained", new int[0]);
containedItemIDs.AddRange(itemIds.Select(id => (ushort)id));
string containedString = containerElement.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(',');
var itemIds = new List<ushort>[itemIdStrings.Length];
for (int i = 0; i < itemIdStrings.Length; i++)
{
itemIds[i] ??= new List<ushort>();
foreach (string idStr in itemIdStrings[i].Split(';'))
{
if (int.TryParse(idStr, out int id))
{
itemIds[i].Add((ushort)id);
containedItemIDs.Add((ushort)id);
}
}
}
}
int minX = int.MaxValue, minY = int.MaxValue;
@@ -110,16 +123,18 @@ namespace Barotrauma
protected override void CreateInstance(Rectangle rect)
{
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
#if CLIENT
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub, selectInstance: Screen.Selected == GameMain.SubEditorScreen);
if (Screen.Selected is SubEditorScreen)
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false));
}
#else
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
#endif
}
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false)
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false)
{
int idOffset = Entity.FindFreeID(1);
if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); }
@@ -148,7 +163,7 @@ namespace Barotrauma
MapEntity.MapLoaded(entities, true);
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen && selectPrefabs)
if (Screen.Selected == GameMain.SubEditorScreen && selectInstance)
{
MapEntity.SelectedList.Clear();
entities.ForEach(MapEntity.AddSelection);
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -135,6 +136,13 @@ namespace Barotrauma
case "walledge":
WallEdgeSprite = new Sprite(subElement);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant();
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
}
break;
}
}
}
@@ -193,5 +201,30 @@ namespace Barotrauma
}
}
}
public void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element, true);
foreach (KeyValuePair<string, float> overrideCommonness in OverrideCommonness)
{
bool elementFound = false;
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("overridecommonness", StringComparison.OrdinalIgnoreCase)
&& subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, StringComparison.OrdinalIgnoreCase))
{
subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture);
elementFound = true;
break;
}
}
if (!elementFound)
{
element.Add(new XElement("overridecommonness",
new XAttribute("leveltype", overrideCommonness.Key),
new XAttribute("commonness", overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture))));
}
}
}
}
}
@@ -169,14 +169,16 @@ namespace Barotrauma
sw2.Start();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
if (targetCells.Count == 0) { return pathCells; }
VoronoiCell currentCell = targetCells[0];
currentCell.CellType = CellType.Path;
pathCells.Add(currentCell);
int currentTargetIndex = 0;
int iterationsLeft = cells.Count;
int iterationsLeft = cells.Count / 2;
do
{
@@ -189,10 +191,14 @@ namespace Barotrauma
if (adjacentCell == null) { continue; }
double dist = MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y);
dist += MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, currentCell.Site.Coord.X, currentCell.Site.Coord.Y) * 0.5f;
//disfavor small edges to prevent generating a very small passage
if (Vector2.Distance(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 200.0f)
//disfavor short edges to prevent generating a very small passage
if (Vector2.DistanceSquared(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 150.0f * 150.0f)
{
dist += 1000000;
//divide by the number of times the current cell has been used
// prevents the path from getting "stuck" (jumping back and forth between adjacent cells)
// if there's no other way to the destination than going through a short edge
dist *= 10.0f / Math.Max(pathCells.Count(c => c == currentCell), 1.0f);
}
if (dist < smallestDist)
{
@@ -58,7 +58,7 @@ namespace Barotrauma
public DestructibleLevelWall(List<Vector2> vertices, Color color, Level level, float? health = null, bool giftWrap = false)
: base (vertices, color, level, giftWrap)
{
MaxHealth = health ?? MathHelper.Clamp(Body.Mass, 100.0f, 1000.0f);
MaxHealth = health ?? MathHelper.Clamp(Body.Mass * 0.5f, 50.0f, 1000.0f);
Cells.ForEach(c => c.IsDestructible = true);
}
@@ -45,14 +45,35 @@ namespace Barotrauma
public bool IsValid;
public Submarine Submarine;
public Ruin Ruin;
public Cave Cave;
public InterestingPosition(Point position, PositionType positionType, bool isValid = true, Submarine submarine = null, Ruin ruin = null)
public InterestingPosition(Point position, PositionType positionType, Submarine submarine = null, bool isValid = true)
{
Position = position;
PositionType = positionType;
IsValid = isValid;
Submarine = submarine;
Ruin = null;
Cave = null;
}
public InterestingPosition(Point position, PositionType positionType, Ruin ruin, bool isValid = true)
{
Position = position;
PositionType = positionType;
IsValid = isValid;
Submarine = null;
Ruin = ruin;
Cave = null;
}
public InterestingPosition(Point position, PositionType positionType, Cave cave, bool isValid = true)
{
Position = position;
PositionType = positionType;
IsValid = isValid;
Submarine = null;
Ruin = null;
Cave = cave;
}
}
@@ -106,6 +127,8 @@ namespace Barotrauma
public Point StartPos, EndPos;
public bool DisplayOnSonar;
public readonly CaveGenerationParams CaveGenerationParams;
public Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos)
@@ -375,6 +398,7 @@ namespace Barotrauma
minWidth = Math.Min(minWidth, MaxSubmarineWidth);
}
minWidth = Math.Min(minWidth, borders.Width / 5);
LevelData.MinMainPathWidth = minWidth;
Rectangle pathBorders = borders;
pathBorders.Inflate(
@@ -435,6 +459,7 @@ namespace Barotrauma
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));
int caveSiteInterval = 500;
for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X)
{
for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y)
@@ -448,7 +473,7 @@ namespace Barotrauma
{
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
float minDist = Math.Max(tunnel.MinWidth, Math.Max(siteInterval.X, siteInterval.Y)) * 2.0f;
float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
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; }
@@ -459,7 +484,7 @@ namespace Barotrauma
{
closeToTunnel = true;
tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (tunnel.Type == TunnelType.Cave )
if (tunnel.Type == TunnelType.Cave)
{
closeToCave = true;
}
@@ -474,31 +499,64 @@ namespace Barotrauma
if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; }
}
if (closeToCave)
{
//add some more sites around caves to generate more small voronoi cells
if (x < borders.Width - siteInterval.X)
{
siteCoordsX.Add(x + Rand.Range(siteInterval.X / 4, siteInterval.X / 2, Rand.RandSync.Server));
siteCoordsY.Add(y);
}
if (y < borders.Height - siteInterval.Y)
{
siteCoordsX.Add(x);
siteCoordsY.Add(y + Rand.Range(siteInterval.Y / 4, siteInterval.Y / 2, Rand.RandSync.Server));
}
if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y)
{
siteCoordsX.Add(x + Rand.Range(siteInterval.X / 4, siteInterval.X / 2, Rand.RandSync.Server));
siteCoordsY.Add(y + Rand.Range(siteInterval.Y / 4, siteInterval.Y / 2, Rand.RandSync.Server));
}
}
siteCoordsX.Add(siteX);
siteCoordsY.Add(siteY);
if (closeToCave)
{
for (int x2 = x; x2 < x + siteInterval.X; x2 += caveSiteInterval)
{
for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
{
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server);
bool tooClose = false;
for (int i = 0; i < siteCoordsX.Count; i++)
{
if (MathUtils.DistanceSquared(caveSiteX, caveSiteY, siteCoordsX[i], siteCoordsY[i]) < 10.0f * 10.0f)
{
tooClose = true;
break;
}
}
if (tooClose) { continue; }
siteCoordsX.Add(caveSiteX);
siteCoordsY.Add(caveSiteY);
}
}
}
}
}
/*int caveSiteInterval = 500;
foreach (Cave cave in Caves)
{
for (int x = cave.Area.X; x < cave.Area.Right; x += caveSiteInterval)
{
for (int y = cave.Area.Y; y < cave.Area.Bottom; y += caveSiteInterval)
{
int siteX = x + Rand.Int(caveSiteInterval / 2);
int siteY = y + Rand.Int(caveSiteInterval / 2);
bool tooClose = false;
for (int i = 0; i<siteCoordsX.Count; i++)
{
if (MathUtils.DistanceSquared(siteX, siteY, siteCoordsX[i],siteCoordsY[i]) < 10.0f * 10.0f)
{
tooClose = true;
break;
}
}
if (tooClose) { continue; }
siteCoordsX.Add(siteX);
siteCoordsY.Add(siteY);
}
}
}*/
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
//----------------------------------------------------------------------------------
@@ -530,11 +588,13 @@ namespace Barotrauma
CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders);
if (tunnel.Type == TunnelType.MainPath || tunnel.Type == TunnelType.SidePath)
{
for (int i = 2; i < tunnel.Cells.Count; i += 3)
var distinctCells = tunnel.Cells.Distinct().ToList();
for (int i = 2; i < distinctCells.Count; i += 3)
{
PositionsOfInterest.Add(new InterestingPosition(
new Point((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Site.Coord.Y),
tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath));
new Point((int)distinctCells[i].Site.Coord.X, (int)distinctCells[i].Site.Coord.Y),
tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath,
Caves.Find(cave => cave.Tunnels.Contains(tunnel))));
}
}
GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel);
@@ -703,7 +763,12 @@ namespace Barotrauma
{
PositionsOfInterest[i] = new InterestingPosition(
new Point(borders.Width - PositionsOfInterest[i].Position.X, PositionsOfInterest[i].Position.Y),
PositionsOfInterest[i].PositionType);
PositionsOfInterest[i].PositionType)
{
Submarine = PositionsOfInterest[i].Submarine,
Cave = PositionsOfInterest[i].Cave,
Ruin = PositionsOfInterest[i].Ruin,
};
}
foreach (WayPoint waypoint in WayPoint.WayPointList)
@@ -726,6 +791,7 @@ namespace Barotrauma
cellGrid[x, y].Add(cell);
}
float destructibleWallRatio = MathHelper.Lerp(0.2f, 1.0f, LevelData.Difficulty / 100.0f);
foreach (Cave cave in Caves)
{
CreatePathToClosestTunnel(cave.StartPos);
@@ -734,7 +800,7 @@ namespace Barotrauma
caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
foreach (var caveCell in caveCells)
{
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < cave.CaveGenerationParams.DestructibleWallRatio)
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
{
var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
if (chunk != null)
@@ -755,7 +821,7 @@ namespace Barotrauma
Ruins = new List<Ruin>();
for (int i = 0; i < GenerationParams.RuinCount; i++)
{
GenerateRuin(mainPath.Cells, mirror);
GenerateRuin(mainPath, mirror);
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
@@ -825,25 +891,30 @@ namespace Barotrauma
};
foreach (Cave cave in Caves)
{
cellBatches.Add(new Pair<List<VoronoiCell>, Cave>(new List<VoronoiCell>(), cave));
var newCellBatch = new Pair<List<VoronoiCell>, Cave>(new List<VoronoiCell>(), cave);
foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells))
{
foreach (var edge in caveCell.Edges)
{
if (!edge.NextToCave) { continue; }
if (edge.Cell1?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell1))
if (edge.Cell1?.CellType == CellType.Solid && !newCellBatch.First.Contains(edge.Cell1))
{
cellBatches.First().First.Remove(edge.Cell1);
cellBatches.Last().First.Add(edge.Cell1);
cellBatches.ForEach(cb => cb.First.Remove(edge.Cell1));
newCellBatch.First.Add(edge.Cell1);
}
if (edge.Cell2?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell2))
if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.First.Contains(edge.Cell2))
{
cellBatches.First().First.Remove(edge.Cell2);
cellBatches.Last().First.Add(edge.Cell2);
cellBatches.ForEach(cb => cb.First.Remove(edge.Cell2));
newCellBatch.First.Add(edge.Cell2);
}
}
}
if (newCellBatch.First.Any())
{
cellBatches.Add(newCellBatch);
}
}
cellBatches.RemoveAll(cb => !cb.First.Any());
Debug.Assert(cellsWithBody.Count == cellBatches.Sum(cb => cb.First.Count));
@@ -1140,8 +1211,9 @@ namespace Barotrauma
private void GenerateWaypoints(Tunnel tunnel, Tunnel parentTunnel)
{
List<WayPoint> wayPoints = new List<WayPoint>();
if (tunnel.Cells.Count == 0) { return; }
List<WayPoint> wayPoints = new List<WayPoint>();
for (int i = 0; i < tunnel.Cells.Count; i++)
{
tunnel.Cells[i].CellType = CellType.Path;
@@ -1228,10 +1300,8 @@ namespace Barotrauma
private List<VoronoiCell> GetTooCloseCells(List<VoronoiCell> emptyCells, float minDistance)
{
List<VoronoiCell> tooCloseCells = new List<VoronoiCell>();
if (minDistance <= 0.0f) { return tooCloseCells; }
foreach (var cell in emptyCells)
foreach (var cell in emptyCells.Distinct())
{
foreach (var tooCloseCell in GetTooCloseCells(cell.Center, minDistance))
{
@@ -1241,25 +1311,13 @@ namespace Barotrauma
}
}
}
/*minDistance *= 0.5f;
do
{
tooCloseCells.AddRange(GetTooCloseCells(position, minDistance));
position += Vector2.Normalize(emptyCells[targetCellIndex].Center - position) * step;
if (Vector2.Distance(emptyCells[targetCellIndex].Center, position) < step * 2.0f) targetCellIndex++;
} while (Vector2.Distance(position, emptyCells[emptyCells.Count - 1].Center) > step * 2.0f);*/
return tooCloseCells;
}
public List<VoronoiCell> GetTooCloseCells(Vector2 position, float minDistance)
{
HashSet<VoronoiCell> tooCloseCells = new HashSet<VoronoiCell>();
var closeCells = GetCells(position, 3);
var closeCells = GetCells(position, searchDepth: Math.Max((int)Math.Ceiling(minDistance / GridCellSize), 3));
float minDistSqr = minDistance * minDistance;
foreach (VoronoiCell cell in closeCells)
{
@@ -1353,7 +1411,7 @@ namespace Barotrauma
int padding = (int)(caveSize.X * 1.2f);
Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2);
var cavePos = FindPosAwayFromMainPath(radius, asFarAwayAsPossible: true, allowedArea);
var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.2f, asCloseAsPossible: true, allowedArea);
Point closestParentNode = parentTunnel.Nodes.First();
double closestDist = double.PositiveInfinity;
@@ -1407,7 +1465,7 @@ namespace Barotrauma
foreach (Tunnel branch in caveBranches)
{
PositionsOfInterest.Add(new InterestingPosition(branch.Nodes.Last(), PositionType.Cave));
PositionsOfInterest.Add(new InterestingPosition(branch.Nodes.Last(), PositionType.Cave, cave));
cave.Tunnels.Add(branch);
}
@@ -1426,7 +1484,7 @@ namespace Barotrauma
}
}
private void GenerateRuin(List<VoronoiCell> mainPath, bool mirror)
private void GenerateRuin(Tunnel mainPath, bool mirror)
{
var ruinGenerationParams = RuinGenerationParams.GetRandom();
@@ -1435,12 +1493,12 @@ namespace Barotrauma
Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server));
int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2;
Point ruinPos = FindPosAwayFromMainPath(ruinRadius + Tunnels.First().MinWidth, asFarAwayAsPossible: false,
Point ruinPos = FindPosAwayFromMainPath((ruinRadius + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true,
limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize));
VoronoiCell closestPathCell = null;
double closestDist = 0.0f;
foreach (VoronoiCell pathCell in mainPath)
foreach (VoronoiCell pathCell in mainPath.Cells)
{
double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y);
if (closestPathCell == null || dist < closestDist)
@@ -1506,22 +1564,22 @@ namespace Barotrauma
CreatePathToClosestTunnel(ruinPos);
}
private Point FindPosAwayFromMainPath(double minDistance, bool asFarAwayAsPossible, Rectangle? limits = null)
private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null)
{
var validPoints = distanceField.FindAll(d => d.Second >= minDistance && (limits == null || limits.Value.Contains(d.First)));
validPoints.RemoveAll(d => d.First.Y < GetBottomPosition(d.First.X).Y + minDistance);
if (asFarAwayAsPossible || !validPoints.Any())
if (asCloseAsPossible || !validPoints.Any())
{
if (!validPoints.Any()) { validPoints = distanceField; }
Pair<Point, double> furthestPoint = null;
Pair<Point, double> closestPoint = null;
foreach (var point in validPoints)
{
if (furthestPoint == null || point.Second > furthestPoint.Second)
if (closestPoint == null || point.Second < closestPoint.Second)
{
furthestPoint = point;
closestPoint = point;
}
}
return furthestPoint.First;
return closestPoint.First;
}
else
{
@@ -1581,6 +1639,7 @@ namespace Barotrauma
vertices.Add(edge.Point2);
}
}
if (vertices.Count < 3) { return null; }
return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health);
}
@@ -1635,6 +1694,8 @@ namespace Barotrauma
Vector2 edgeNormal = closestEdge.GetNormal(closestCell);
float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength);
spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f);
Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server);
Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server);
List<Vector2> vertices = new List<Vector2>()
@@ -2101,7 +2162,7 @@ namespace Barotrauma
if (resourcesInCluster < 1) { return false; }
PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLenght: edgeLength);
PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLength: edgeLength);
itemCount += resourcesInCluster;
location.InitializeResources();
location.Resources.AddRange(placedResources);
@@ -2136,7 +2197,7 @@ namespace Barotrauma
c.Equals(location) &&
c.Resources.Any(r => r != null && !r.Removed &&
(!(r.GetComponent<Holdable>() is Holdable h) || (h.Attachable && h.Attached)))));
if(locationHasResources)
if (locationHasResources)
{
allValidLocations.RemoveAt(i);
}
@@ -2231,10 +2292,10 @@ namespace Barotrauma
}
private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List<Item> placedResources,
float? edgeLenght = null, float maxResourceOverlap = 0.4f)
float? edgeLength = null, float maxResourceOverlap = 0.4f)
{
edgeLenght ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
var minResourceOverlap = -((edgeLenght.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
minResourceOverlap = Math.Max(minResourceOverlap, 0.0f);
var lerpAmounts = new float[resourceCount];
lerpAmounts[0] = 0.0f;
@@ -2242,7 +2303,7 @@ namespace Barotrauma
for (int i = 1; i < resourceCount; i++)
{
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.Server);
lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLenght.Value;
lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value;
lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
}
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.Server);
@@ -2252,7 +2313,9 @@ namespace Barotrauma
Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1, location.Edge.Point2, startOffset + lerpAmounts[i]);
var item = new Item(resourcePrefab, selectedPos, submarine: null);
Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
item.Move(edgeNormal * (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)), ignoreContacts: true);
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.Server);
item.Move(edgeNormal * moveAmount, ignoreContacts: true);
if (item.GetComponent<Holdable>() is Holdable h)
{
h.AttachToWall();
@@ -2268,7 +2331,7 @@ namespace Barotrauma
}
}
public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f)
public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f, Func<InterestingPosition, bool> filter = null)
{
if (!PositionsOfInterest.Any())
{
@@ -2280,7 +2343,7 @@ namespace Barotrauma
int tries = 0;
do
{
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos);
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter);
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server);
if (!cells.Any(c => c.IsPointInside(startPos + offset)))
@@ -2312,14 +2375,14 @@ namespace Barotrauma
return position;
}
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position)
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func<InterestingPosition, bool> filter = null)
{
bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos);
bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, filter);
position = pos.ToVector2();
return success;
}
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position)
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Func<InterestingPosition, bool> filter = null)
{
if (!PositionsOfInterest.Any())
{
@@ -2328,8 +2391,12 @@ namespace Barotrauma
}
List<InterestingPosition> suitablePositions = PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
if (filter != null)
{
suitablePositions.RemoveAll(p => !filter(p));
}
//avoid floating ice chunks on the main path
if (positionType == PositionType.MainPath || positionType == PositionType.SidePath)
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
{
suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2()))));
}
@@ -2513,26 +2580,38 @@ namespace Barotrauma
}
cells.Remove(cell);
//if the edge is very short, remove an adjacent cell to prevent making the passage too narrow
if (Vector2.DistanceSquared(e.Point1, e.Point2) < 200.0f * 200.0f)
//go through the edges of this cell and find the ones that are next to a removed cell
foreach (var otherEdge in cell.Edges)
{
foreach (GraphEdge e2 in cell.Edges)
var otherAdjacent = otherEdge.AdjacentCell(cell);
if (otherAdjacent == null || otherAdjacent.CellType == CellType.Solid) { continue; }
//if the edge is very short, remove adjacent cells to prevent making the passage too narrow
if (Vector2.DistanceSquared(otherEdge.Point1, otherEdge.Point2) < 500.0f * 500.0f)
{
if (e2 == e) { continue; }
var adjacentCell = e2.AdjacentCell(cell);
if (adjacentCell == null || adjacentCell.CellType == CellType.Removed) { continue; }
adjacentCell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
foreach (GraphEdge e2 in cell.Edges)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
if (e2 == otherEdge || e2 == otherEdge) { continue; }
if (!MathUtils.NearlyEqual(otherEdge.Point1, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point2))
{
cellGrid[x, y].Remove(adjacentCell);
continue;
}
var adjacentCell = e2.AdjacentCell(cell);
if (adjacentCell == null || adjacentCell.CellType == CellType.Removed) { continue; }
adjacentCell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cellGrid[x, y].Remove(adjacentCell);
}
}
cells.Remove(adjacentCell);
}
cells.Remove(adjacentCell);
break;
}
}
break;
}
@@ -2639,7 +2718,7 @@ namespace Barotrauma
{
sub.ShowSonarMarker = false;
sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static;
sub.TeamID = Character.TeamType.None;
sub.TeamID = CharacterTeamType.None;
}
tempSW.Stop();
Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
@@ -2833,6 +2912,12 @@ namespace Barotrauma
{
return true;
}
if (Caves.Any(c =>
ToolBox.GetWorldBounds(c.Area.Center, c.Area.Size).IntersectsWorld(bounds) ||
ToolBox.GetWorldBounds(c.StartPos, new Point(1500)).IntersectsWorld(bounds)))
{
return true;
}
return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
}
}
@@ -3113,15 +3198,26 @@ namespace Barotrauma
if (!(GameMain.NetworkMember?.IsClient ?? false))
{
//empty the reactor
foreach (Item item in reactorContainer.Inventory.Items)
foreach (Item item in reactorContainer.Inventory.AllItems)
{
if (item == null) { continue; }
if (item.NonInteractable) { continue; }
Entity.Spawner.AddToRemoveQueue(item);
}
//remove wires
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
{
if (item.NonInteractable) { continue; }
Wire wire = item.GetComponent<Wire>();
if (wire.Locked) { continue; }
if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent<ConnectionPanel>().Locked))
{
continue;
}
if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent<ConnectionPanel>().Locked))
{
continue;
}
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f)
{
Entity.Spawner.AddToRemoveQueue(item);
@@ -3131,6 +3227,7 @@ namespace Barotrauma
//break powered items
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered)))
{
if (item.NonInteractable) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f)
{
item.Condition *= Rand.Range(0.2f, 0.6f, Rand.RandSync.Unsynced);
@@ -3221,7 +3318,7 @@ namespace Barotrauma
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server);
var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, true);
corpse.TeamID = Character.TeamType.None;
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
selectedPrefab.GiveItems(corpse, wreck);
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
@@ -35,6 +35,11 @@ namespace Barotrauma
public readonly int InitialDepth;
/// <summary>
/// Determined during level generation based on the size of the submarine. Null if the level hasn't been generated.
/// </summary>
public int? MinMainPathWidth;
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
@@ -52,7 +52,11 @@ namespace Barotrauma
public Sprite Sprite
{
get { return spriteIndex < 0 || Prefab.Sprites.Count == 0 ? null : Prefab.Sprites[spriteIndex % Prefab.Sprites.Count]; }
get
{
var prefab = ActivePrefab?.Sprites.Count > 0 ? ActivePrefab : Prefab;
return spriteIndex < 0 || prefab.Sprites.Count == 0 ? null : prefab.Sprites[spriteIndex % prefab.Sprites.Count];
}
}
Vector2 ISpatialEntity.Position => new Vector2(Position.X, Position.Y);
@@ -63,6 +67,8 @@ namespace Barotrauma
public Submarine Submarine => null;
public Level.Cave ParentCave;
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
{
ActivePrefab = Prefab = prefab;
@@ -110,6 +116,19 @@ namespace Barotrauma
Triggers.Add(newTrigger);
}
if (spriteIndex == -1)
{
foreach (var overrideProperties in prefab.OverrideProperties)
{
if (overrideProperties == null) { continue; }
if (overrideProperties.Sprites.Count > 0)
{
spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.Server);
break;
}
}
}
NeedsUpdate = NeedsNetworkSyncing || (Triggers != null && Triggers.Any()) || Prefab.PhysicsBodyTriggerIndex > -1;
InitProjSpecific();
@@ -171,7 +171,7 @@ namespace Barotrauma
for (int i = 0; i < cave.CaveGenerationParams.LevelObjectAmount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs);
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs, requireCaveSpecificOverride: true);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
@@ -184,10 +184,10 @@ namespace Barotrauma
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
PlaceObject(prefab, spawnPosition, level, cave);
if (prefab.MaxCount < amount)
{
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
if (objects.Count(o => o.Prefab == prefab && o.ParentCave == cave) >= prefab.MaxCount)
{
availablePrefabs.Remove(prefab);
}
@@ -196,7 +196,51 @@ namespace Barotrauma
}
}
private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level)
public void PlaceNestObjects(Level level, Level.Cave cave, Vector2 nestPosition, float nestRadius, int objectAmount)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(level.Seed));
var availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall)));
Dictionary<LevelObjectPrefab, List<SpawnPosition>> suitableSpawnPositions = new Dictionary<LevelObjectPrefab, List<SpawnPosition>>();
Dictionary<LevelObjectPrefab, List<float>> spawnPositionWeights = new Dictionary<LevelObjectPrefab, List<float>>();
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
var caveCells = cave.Tunnels.SelectMany(t => t.Cells);
List<VoronoiCell> caveWallCells = new List<VoronoiCell>();
foreach (var edge in caveCells.SelectMany(c => c.Edges))
{
if (!edge.NextToCave) { continue; }
if (MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), nestPosition.ToPoint()) > nestRadius * nestRadius) { continue; }
if (edge.Cell1?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell1); }
if (edge.Cell2?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell2); }
}
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(caveWallCells.Distinct(), LevelObjectPrefab.SpawnPosType.CaveWall));
for (int i = 0; i < objectAmount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs, requireCaveSpecificOverride: false);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
suitableSpawnPositions.Add(prefab,
availableSpawnPositions.Where(sp =>
sp.Length >= prefab.MinSurfaceWidth &&
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
{
availablePrefabs.Remove(prefab);
}
}
}
private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level, Level.Cave parentCave = null)
{
float rotation = 0.0f;
if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null)
@@ -228,6 +272,7 @@ namespace Barotrauma
var newObject = new LevelObject(prefab,
new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation);
AddObject(newObject, level);
newObject.ParentCave = parentCave;
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
{
@@ -237,7 +282,7 @@ namespace Barotrauma
var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name));
int prefabCount = matchingPrefabs.Count();
var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server));
if (childPrefab == null) continue;
if (childPrefab == null) { continue; }
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth;
@@ -247,6 +292,7 @@ namespace Barotrauma
rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server));
AddObject(childObject, level);
childObject.ParentCave = parentCave;
}
}
}
@@ -402,7 +448,7 @@ namespace Barotrauma
return objectsInRange;
}
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType)
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true)
{
List<LevelObjectPrefab.SpawnPosType> spawnPosTypes = new List<LevelObjectPrefab.SpawnPosType>(4);
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
@@ -498,12 +544,12 @@ namespace Barotrauma
availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server);
}
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs)
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs, bool requireCaveSpecificOverride)
{
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams)) <= 0.0f) { return null; }
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(caveParams)).ToList(), Rand.RandSync.Server);
availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.Server);
}
public override void Remove()
@@ -37,11 +37,12 @@ namespace Barotrauma
MainPathWall = 1,
SidePathWall = 2,
CaveWall = 4,
RuinWall = 8,
SeaFloor = 16,
MainPath = 32,
LevelStart = 64,
LevelEnd = 128,
NestWall = 8,
RuinWall = 16,
SeaFloor = 32,
MainPath = 64,
LevelStart = 128,
LevelEnd = 256,
Wall = MainPathWall | SidePathWall | CaveWall,
}
@@ -442,14 +443,14 @@ namespace Barotrauma
partial void InitProjSpecific(XElement element);
public float GetCommonness(CaveGenerationParams generationParams)
public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true)
{
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
{
return commonness;
}
return 0.0f;
return requireCaveSpecificOverride ? 0.0f : Commonness;
}
public float GetCommonness(LevelGenerationParams generationParams)
@@ -34,44 +34,38 @@ namespace Barotrauma
public Action<LevelTrigger, Entity> OnTriggered;
private PhysicsBody physicsBody;
/// <summary>
/// Effects applied to entities that are inside the trigger
/// </summary>
private List<StatusEffect> statusEffects = new List<StatusEffect>();
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
/// <summary>
/// Attacks applied to entities that are inside the trigger
/// </summary>
private List<Attack> attacks = new List<Attack>();
private readonly List<Attack> attacks = new List<Attack>();
private float cameraShake;
private readonly float cameraShake;
private Vector2 unrotatedForce;
private float forceFluctuationTimer, currentForceFluctuation = 1.0f;
private HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private TriggererType triggeredBy;
private readonly TriggererType triggeredBy;
private float randomTriggerInterval;
private float randomTriggerProbability;
private readonly float randomTriggerInterval;
private readonly float randomTriggerProbability;
private float randomTriggerTimer;
private float triggeredTimer;
//how far away this trigger can activate other triggers from
private float triggerOthersDistance;
private HashSet<string> tags = new HashSet<string>();
private readonly HashSet<string> tags = new HashSet<string>();
//other triggers have to have at least one of these tags to trigger this one
private HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
private readonly HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
/// <summary>
/// How long the trigger stays in the triggered state after triggerers have left
/// </summary>
private float stayTriggeredDelay;
private readonly float stayTriggeredDelay;
public LevelTrigger ParentTrigger;
@@ -88,30 +82,24 @@ namespace Barotrauma
set
{
worldPosition = value;
physicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), physicsBody.Rotation);
PhysicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), PhysicsBody.Rotation);
}
}
public float Rotation
{
get { return physicsBody == null ? 0.0f : physicsBody.Rotation; }
get { return PhysicsBody == null ? 0.0f : PhysicsBody.Rotation; }
set
{
if (physicsBody == null) return;
physicsBody.SetTransform(physicsBody.Position, value);
if (PhysicsBody == null) return;
PhysicsBody.SetTransform(PhysicsBody.Position, value);
CalculateDirectionalForce();
}
}
public PhysicsBody PhysicsBody
{
get { return physicsBody; }
}
public PhysicsBody PhysicsBody { get; private set; }
public float TriggerOthersDistance
{
get { return triggerOthersDistance; }
}
public float TriggerOthersDistance { get; private set; }
public IEnumerable<Entity> Triggerers
{
@@ -153,7 +141,7 @@ namespace Barotrauma
private set;
}
private TriggerForceMode forceMode;
private readonly TriggerForceMode forceMode;
public TriggerForceMode ForceMode
{
get { return forceMode; }
@@ -198,6 +186,9 @@ namespace Barotrauma
get;
set;
}
private bool triggeredOnce;
private readonly bool triggerOnce;
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
{
@@ -206,20 +197,20 @@ namespace Barotrauma
worldPosition = position;
if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any())
{
physicsBody = new PhysicsBody(element, scale)
PhysicsBody = new PhysicsBody(element, scale)
{
CollisionCategories = Physics.CollisionLevel,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall
};
physicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
physicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
physicsBody.FarseerBody.SetIsSensor(true);
physicsBody.FarseerBody.BodyType = BodyType.Static;
physicsBody.FarseerBody.BodyType = BodyType.Kinematic;
PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
PhysicsBody.FarseerBody.BodyType = BodyType.Kinematic;
ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
physicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
}
cameraShake = element.GetAttributeFloat("camerashake", 0.0f);
@@ -227,6 +218,8 @@ namespace Barotrauma
InfectIdentifier = element.GetAttributeString("infectidentifier", null);
InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f);
triggerOnce = element.GetAttributeBool("triggeronce", false);
stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f);
randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f);
randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f);
@@ -256,7 +249,7 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
}
UpdateCollisionCategories();
triggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
var tagsArray = element.GetAttributeStringArray("tags", new string[0]);
foreach (string tag in tagsArray)
@@ -283,11 +276,14 @@ namespace Barotrauma
case "attack":
case "damage":
var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName);
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
if (!triggerOnce)
{
attack.Afflictions.Add(affliction, null);
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
{
attack.Afflictions.Add(affliction, null);
}
}
attacks.Add(attack);
break;
@@ -300,14 +296,14 @@ namespace Barotrauma
private void UpdateCollisionCategories()
{
if (physicsBody == null) return;
if (PhysicsBody == null) return;
var collidesWith = Physics.CollisionNone;
if (triggeredBy.HasFlag(TriggererType.Character) || triggeredBy.HasFlag(TriggererType.Creature)) collidesWith |= Physics.CollisionCharacter;
if (triggeredBy.HasFlag(TriggererType.Item)) collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile;
if (triggeredBy.HasFlag(TriggererType.Submarine)) collidesWith |= Physics.CollisionWall;
if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; }
if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; }
if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; }
physicsBody.CollidesWith = collidesWith;
PhysicsBody.CollidesWith = collidesWith;
}
private void CalculateDirectionalForce()
@@ -362,7 +358,7 @@ namespace Barotrauma
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return;
if (entity == null) { return; }
if (entity is Character character &&
(!character.Enabled || character.Removed) &&
@@ -376,22 +372,25 @@ namespace Barotrauma
//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)
ContactEdge contactEdge = fixtureA.Body.ContactList;
while (contactEdge != null)
foreach (Fixture fixture in PhysicsBody.FarseerBody.FixtureList)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.Enabled &&
contactEdge.Contact.IsTouching)
ContactEdge contactEdge = fixture.Body.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact.FixtureA != fixtureA && contactEdge.Contact.FixtureB != fixtureA)
if (contactEdge.Contact != null &&
contactEdge.Contact.Enabled &&
contactEdge.Contact.IsTouching)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) { return; }
if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) { return; }
}
}
contactEdge = contactEdge.Next;
}
contactEdge = contactEdge.Next;
}
if (triggerers.Contains(entity))
@@ -403,10 +402,10 @@ namespace Barotrauma
private Entity GetEntity(Fixture fixture)
{
if (fixture.Body == null || fixture.Body.UserData == null) return null;
if (fixture.Body.UserData is Entity entity) return entity;
if (fixture.Body.UserData is Limb limb) return limb.character;
if (fixture.Body.UserData is SubmarineBody subBody) return subBody.Submarine;
if (fixture.Body == null || fixture.Body.UserData == null) { return null; }
if (fixture.Body.UserData is Entity entity) { return entity; }
if (fixture.Body.UserData is Limb limb) { return limb.character; }
if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; }
return null;
}
@@ -416,15 +415,15 @@ namespace Barotrauma
/// </summary>
public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger)
{
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) return;
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; }
//check if the other trigger has appropriate tags
if (allowedOtherTriggerTags.Count > 0)
{
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) return;
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) { return; }
}
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.triggerOthersDistance * otherTrigger.triggerOthersDistance)
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.TriggerOthersDistance * otherTrigger.TriggerOthersDistance)
{
bool wasAlreadyTriggered = IsTriggered;
triggeredTimer = stayTriggeredDelay;
@@ -441,10 +440,10 @@ namespace Barotrauma
triggerers.RemoveWhere(t => t.Removed);
if (physicsBody != null)
if (PhysicsBody != null)
{
//failsafe to ensure triggerers get removed when they're far from the trigger
float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f);
float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(PhysicsBody.GetMaxExtent() * 5), 5000.0f);
triggerers.RemoveWhere(t =>
{
return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent;
@@ -500,17 +499,43 @@ namespace Barotrauma
}
}
if (triggerOnce)
{
if (triggeredOnce) { return; }
if (triggerers.Count > 0) { triggeredOnce = true; }
}
foreach (Entity triggerer in triggerers)
{
foreach (StatusEffect effect in statusEffects)
{
if (triggerer is Character)
Vector2? position = null;
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; }
if (triggerer is Character character)
{
effect.Apply(effect.type, deltaTime, triggerer, (Character)triggerer);
effect.Apply(effect.type, deltaTime, triggerer, character, position);
if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null)
{
foreach (Item item in character.Inventory.AllItemsMod)
{
if (item.ContainedItems == null) { continue; }
foreach (Item containedItem in item.ContainedItems)
{
effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position);
}
}
}
}
else if (triggerer is Item)
else if (triggerer is Item item)
{
effect.Apply(effect.type, deltaTime, triggerer, ((Item)triggerer).AllPropertyObjects);
effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(worldPosition, targets);
effect.Apply(effect.type, deltaTime, triggerer, targets);
}
}
@@ -1,9 +1,14 @@
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
#if DEBUG
using System.Xml;
#else
using Barotrauma.IO;
#endif
namespace Barotrauma.RuinGeneration
{
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
namespace Barotrauma
{
@@ -98,6 +99,8 @@ namespace Barotrauma
LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position);
sl.filePath = filePath;
sl.saveElement = doc.Root;
sl.saveElement.Name = "LinkedSubmarine";
return sl;
}
@@ -132,7 +135,11 @@ namespace Barotrauma
public override MapEntity Clone()
{
return CreateDummy(Submarine, filePath, Position);
XElement cloneElement = new XElement(saveElement);
LinkedSubmarine sl = CreateDummy(Submarine, cloneElement, Position);
sl.saveElement = cloneElement;
sl.filePath = filePath;
return sl;
}
private void GenerateWallVertices(XElement rootElement)
@@ -232,6 +239,7 @@ namespace Barotrauma
IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset);
sub = Submarine.Load(info, false, parentRemap);
sub.Info.SubmarineClass = Submarine.Info.SubmarineClass;
IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset);
@@ -290,6 +298,7 @@ namespace Barotrauma
originalMyPortID = myPort.Item.ID;
myPort.Undock();
myPort.DockingDir = 0;
//something else is already docked to the port this sub should be docked to
//may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be,
@@ -324,7 +333,7 @@ namespace Barotrauma
if (wall.Submarine != sub) { continue; }
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -wall.MaxHealth);
wall.SetDamage(i, 0, createNetworkEvent: false);
}
}
foreach (Hull hull in Hull.hullList)
@@ -349,15 +358,15 @@ namespace Barotrauma
{
var doc = SubmarineInfo.OpenFile(filePath);
saveElement = doc.Root;
saveElement.Name = "LinkedSubmarine";
saveElement.Add(new XAttribute("filepath", filePath));
}
else
{
saveElement = this.saveElement;
}
saveElement.Name = "LinkedSubmarine";
if (saveElement.Attribute("pos") != null) saveElement.Attribute("pos").Remove();
if (saveElement.Attribute("pos") != null) { saveElement.Attribute("pos").Remove(); }
saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)));
var linkedPort = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
@@ -62,7 +62,9 @@ namespace Barotrauma
public bool Discovered;
public int TypeChangeTimer;
public readonly Dictionary<LocationTypeChange, int> ProximityTimer = new Dictionary<LocationTypeChange, int>();
public Pair<LocationTypeChange, int> PendingLocationTypeChange;
public string BaseName { get => baseName; }
@@ -80,12 +82,74 @@ namespace Barotrauma
public Reputation Reputation { get; set; }
#region Store
private const float StoreMaxReputationModifier = 0.1f;
private const float StoreSellPriceModifier = 0.8f;
private const float MechanicalMaxDiscountPercentage = 50.0f;
private const float DailySpecialPriceModifier = 0.9f;
private const float RequestGoodPriceModifier = 1.5f;
public const int StoreInitialBalance = 5000;
public int StoreCurrentBalance { get; set; }
/// <summary>
/// In percentages
/// </summary>
private const int StorePriceModifierRange = 5;
/// <summary>
/// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa.
/// </summary>
public int StorePriceModifier { get; private set; }
public Color BalanceColor => ActiveStoreBalanceStatus.Color;
public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; }
private static StoreBalanceStatus DefaultBalanceStatus { get; } = new StoreBalanceStatus(1.0f, 1.0f, Color.White);
private static List<StoreBalanceStatus> StoreBalanceStatuses { get; } = new List<StoreBalanceStatus>
{
new StoreBalanceStatus(0.5f, 0.75f, Color.Orange),
new StoreBalanceStatus(0.25f, 0.2f, Color.Red),
};
public struct StoreBalanceStatus
{
public float PercentageOfInitialBalance { get; }
public float SellPriceModifier { get; }
public Color Color { get; }
public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color)
{
PercentageOfInitialBalance = percentage;
SellPriceModifier = sellPriceModifier;
Color = color;
}
}
private int storeCurrentBalance;
public int StoreCurrentBalance
{
get
{
return storeCurrentBalance;
}
set
{
storeCurrentBalance = value;
ActiveStoreBalanceStatus = GetStoreBalanceStatus(value);
}
}
public List<PurchasedItem> StoreStock { get; set; }
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
/// <summary>
/// How many map progress steps it takes before the discounts should be updated.
/// </summary>
private const int SpecialsUpdateInterval = 3;
private const int DailySpecialsCount = 3;
private const int RequestedGoodsCount = 3;
private int StepsSinceSpecialsUpdated { get; set; }
#endregion
private const float MechanicalMaxDiscountPercentage = 50.0f;
private readonly List<TakenItem> takenItems = new List<TakenItem>();
public IEnumerable<TakenItem> TakenItems
@@ -150,6 +214,8 @@ namespace Barotrauma
public string LastTypeChangeMessage;
public int TimeSinceLastTypeChange;
private struct LoadedMission
{
public MissionPrefab MissionPrefab { get; }
@@ -189,10 +255,23 @@ namespace Barotrauma
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
TypeChangeTimer = element.GetAttributeInt("changetimer", 0);
Discovered = element.GetAttributeBool("discovered", false);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
TimeSinceLastTypeChange = element.GetAttributeInt("timesincelasttypechange", 0);
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
ProximityTimer.Add(Type.CanChangeTo[i], element.GetAttributeInt("proximitytimer" + i, 0));
}
int locationTypeChangeIndex = element.GetAttributeInt("pendinglocationtypechange", -1);
if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < Type.CanChangeTo.Count - 1)
{
PendingLocationTypeChange = new Pair<LocationTypeChange, int>(
Type.CanChangeTo[locationTypeChangeIndex],
element.GetAttributeInt("pendinglocationtypechangetimer", 0));
}
string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]);
foreach (string takenItem in takenItemStr)
@@ -232,13 +311,8 @@ namespace Barotrauma
LevelData = new LevelData(element.Element("Level"));
PortraitId = ToolBox.StringToInt(Name);
if (element.GetChildElement("store") is XElement storeElement)
{
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
StoreStock = LoadStoreStock(storeElement);
}
LoadStore(element);
LoadMissions(element);
}
@@ -456,6 +530,59 @@ namespace Barotrauma
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
}
public void LoadStore(XElement locationElement)
{
StoreStock?.Clear();
DailySpecials.Clear();
RequestedGoods.Clear();
if (locationElement.GetChildElement("store") is XElement storeElement)
{
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
StorePriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
StoreStock ??= new List<PurchasedItem>();
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
{
var id = stockElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var qty = stockElement.GetAttributeInt("qty", 0);
if (qty < 1) { continue; }
StoreStock.Add(new PurchasedItem(prefab, qty));
}
StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0);
if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement)
{
var loadedDailySpecials = LoadStoreSpecials(specialsElement);
DailySpecials.AddRange(loadedDailySpecials);
}
if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement)
{
var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
RequestedGoods.AddRange(loadedRequestedGoods);
}
static List<ItemPrefab> LoadStoreSpecials(XElement element)
{
List<ItemPrefab> specials = new List<ItemPrefab>();
foreach (var childElement in element.GetChildElements("item"))
{
var id = childElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Find(null, id);
if (prefab == null) { continue; }
specials.Add(prefab);
}
return specials;
}
}
}
private List<PurchasedItem> CreateStoreStock()
{
var stock = new List<PurchasedItem>();
@@ -463,31 +590,28 @@ namespace Barotrauma
{
if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo))
{
var quantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount :
(priceInfo.MaxAvailableAmount > 0 ? Math.Min(priceInfo.MaxAvailableAmount, 5) : 5);
int quantity = PriceInfo.DefaultAmount;
if (priceInfo.MaxAvailableAmount > 0)
{
if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount)
{
quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount);
}
else
{
quantity = priceInfo.MaxAvailableAmount;
}
}
else if (priceInfo.MinAvailableAmount > 0)
{
quantity = priceInfo.MinAvailableAmount;
}
stock.Add(new PurchasedItem(prefab, quantity));
}
}
return stock;
}
public static List<PurchasedItem> LoadStoreStock(XElement storeElement)
{
var stock = new List<PurchasedItem>();
if (storeElement == null) { return stock; }
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
{
var id = stockElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var qty = stockElement.GetAttributeInt("qty", 0);
if (qty < 1) { continue; }
stock.Add(new PurchasedItem(prefab, qty));
}
return stock;
}
/// <summary>
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
/// </summary>
@@ -526,48 +650,70 @@ namespace Barotrauma
}
}
public int GetAdjustedItemBuyPrice(PriceInfo priceInfo)
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// /// <param name="considerDailySpecials">If false, the price won't be affected by <see cref="DailySpecialPriceModifier"/></param>
public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true)
{
// TODO: Check priceInfo.CanBeBought
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
var price = priceInfo.Price;
float price = priceInfo.Price;
// Adjust by random price modifier
price = ((100 + StorePriceModifier) / 100.0f) * price;
// Adjust by daily special status
if (considerDailySpecials && DailySpecials.Contains(item))
{
price = DailySpecialPriceModifier * price;
}
// Adjust by current location reputation
if (Reputation.Value > 0.0f)
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price;
}
else
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price;
}
// Item price should never go below 1 mk
return Math.Max(price, 1);
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
/// <summary>
/// If item.GetPriceInfo() returns null, this will return 0
/// </summary>
public int GetAdjustedItemBuyPrice(ItemPrefab item) => GetAdjustedItemBuyPrice(item?.GetPriceInfo(this));
public int GetAdjustedItemSellPrice(PriceInfo priceInfo)
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// <param name="considerRequestedGoods">If false, the price won't be affected by <see cref="RequestGoodPriceModifier"/></param>
public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
{
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
var price = (int)(StoreSellPriceModifier * priceInfo.Price);
float price = StoreSellPriceModifier * priceInfo.Price;
// Adjust by random price modifier
price = ((100 - StorePriceModifier) / 100.0f) * price;
// Adjust by current store balance
price = ActiveStoreBalanceStatus.SellPriceModifier * price;
// Adjust by requested good status
if (considerRequestedGoods && RequestedGoods.Contains(item))
{
price = RequestGoodPriceModifier * price;
}
// Adjust by current location reputation
if (Reputation.Value > 0.0f)
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price;
}
else
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price;
}
// Item price should never go below 1 mk
return Math.Max(price, 1);
}
/// <summary>
/// If item.GetPriceInfo() returns null, this will return 0
/// </summary>
public int GetAdjustedItemSellPrice(ItemPrefab item) => GetAdjustedItemSellPrice(item?.GetPriceInfo(this));
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
public int GetAdjustedMechanicalCost(int cost)
{
@@ -575,12 +721,12 @@ namespace Barotrauma
return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
}
/// <summary>
/// If 'force' is true, the stock will be recreated even if it has been created previously already.
/// This is used when (at least) when the type of the location changes.
/// </summary>
/// <param name="force">If true, the store will be recreated if it already exists.</param>
public void CreateStore(bool force = false)
{
// In multiplayer, stores should be created by the server and loaded from save data by clients
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (!force && StoreStock != null) { return; }
if (StoreStock != null)
@@ -604,10 +750,16 @@ namespace Barotrauma
StoreCurrentBalance = StoreInitialBalance;
StoreStock = CreateStoreStock();
}
GenerateRandomPriceModifier();
CreateStoreSpecials();
}
public void UpdateStore()
{
// In multiplayer, stores should be updated by the server and loaded from save data by clients
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (StoreStock == null)
{
CreateStore();
@@ -619,6 +771,8 @@ namespace Barotrauma
StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
}
GenerateRandomPriceModifier();
var stock = StoreStock;
var stockToRemove = new List<PurchasedItem>();
foreach (PurchasedItem item in stock)
@@ -642,6 +796,56 @@ namespace Barotrauma
}
stockToRemove.ForEach(i => stock.Remove(i));
StoreStock = stock;
if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval)
{
CreateStoreSpecials();
}
}
private void GenerateRandomPriceModifier()
{
StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange);
}
private void CreateStoreSpecials()
{
DailySpecials.Clear();
var availableStock = new Dictionary<ItemPrefab, float>();
foreach (var stockItem in StoreStock)
{
if (stockItem.Quantity < 1) { continue; }
var weight = 1.0f;
var priceInfo = stockItem.ItemPrefab.GetPriceInfo(this);
if (priceInfo != null)
{
if (!priceInfo.CanBeSpecial) { continue; }
var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount;
weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
if (weight < 0.0f) { continue; }
}
availableStock.Add(stockItem.ItemPrefab, weight);
}
for (int i = 0; i < DailySpecialsCount; i++)
{
if (availableStock.None()) { break; }
var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
if (item == null) { break; }
DailySpecials.Add(item);
availableStock.Remove(item);
}
RequestedGoods.Clear();
for (int i = 0; i < RequestedGoodsCount; i++)
{
var item = ItemPrefab.Prefabs.GetRandom(p =>
p.CanBeSold && !RequestedGoods.Contains(p) &&
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial);
if (item == null) { break; }
RequestedGoods.Add(item);
}
StepsSinceSpecialsUpdated = 0;
}
public void AddToStock(List<SoldItem> items)
@@ -686,6 +890,20 @@ namespace Barotrauma
}
}
public static StoreBalanceStatus GetStoreBalanceStatus(int balance)
{
StoreBalanceStatus nextStatus = DefaultBalanceStatus;
foreach (var balanceStatus in StoreBalanceStatuses)
{
if (balanceStatus.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance &&
((float)balance / StoreInitialBalance) < balanceStatus.PercentageOfInitialBalance)
{
nextStatus = balanceStatus;
}
}
return nextStatus;
}
public XElement Save(Map map, XElement parentElement)
{
var locationElement = new XElement("location",
@@ -695,13 +913,24 @@ namespace Barotrauma
new XAttribute("discovered", Discovered),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier));
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange));
LevelData.Save(locationElement);
if (TypeChangeTimer > 0)
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
locationElement.Add(new XAttribute("changetimer", TypeChangeTimer));
if (ProximityTimer.ContainsKey(Type.CanChangeTo[i]))
{
locationElement.Add(new XAttribute("proximitytimer" + i, ProximityTimer[Type.CanChangeTo[i]]));
}
}
if (PendingLocationTypeChange != null)
{
locationElement.Add(new XAttribute("pendinglocationtypechange", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.First)));
locationElement.Add(new XAttribute("pendinglocationtypechangetimer", PendingLocationTypeChange.Second));
}
if (takenItems.Any())
{
locationElement.Add(new XAttribute(
@@ -715,7 +944,11 @@ namespace Barotrauma
if (StoreStock != null)
{
var storeElement = new XElement("store", new XAttribute("balance", StoreCurrentBalance));
var storeElement = new XElement("store",
new XAttribute("balance", StoreCurrentBalance),
new XAttribute("pricemodifier", StorePriceModifier),
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
foreach (PurchasedItem item in StoreStock)
{
if (item?.ItemPrefab == null) { continue; }
@@ -723,6 +956,29 @@ namespace Barotrauma
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity)));
}
if (DailySpecials.Any())
{
var dailySpecialElement = new XElement("dailyspecials");
foreach (var item in DailySpecials)
{
dailySpecialElement.Add(new XElement("item",
new XAttribute("id", item.Identifier)));
}
storeElement.Add(dailySpecialElement);
}
if (RequestedGoods.Any())
{
var requestedGoodsElement = new XElement("requestedgoods");
foreach (var item in RequestedGoods)
{
requestedGoodsElement.Add(new XElement("item",
new XAttribute("id", item.Identifier)));
}
storeElement.Add(requestedGoodsElement);
}
locationElement.Add(storeElement);
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
@@ -33,6 +34,19 @@ namespace Barotrauma
public LocationConnection(Location location1, Location location2)
{
if (location1 == null)
{
throw new ArgumentException("Invalid location connection: location1 was null");
}
if (location2 == null)
{
throw new ArgumentException("Invalid location connection: location2 was null");
}
if (location1 == location2)
{
throw new ArgumentException("Invalid location connection: location1 was the same as location2");
}
Locations = new Location[] { location1, location2 };
Length = Vector2.Distance(location1.MapPosition, location2.MapPosition);
}
@@ -1,4 +1,5 @@
using System;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -9,35 +10,85 @@ namespace Barotrauma
{
public readonly string ChangeToType;
public readonly float Probability;
public readonly int RequiredDuration;
public readonly float ProximityProbabilityIncrease;
public readonly int RequiredProximityForProbabilityIncrease;
public readonly bool RequireDiscovered;
public List<string> Messages = new List<string>();
//the change can't happen if there's a location of the given type next to this one
/// <summary>
/// The change can only happen if there's at least one of the given types of locations near this one
/// </summary>
public readonly List<string> RequiredLocations;
/// <summary>
/// How close the location needs to be to one of the RequiredLocations for the change to occur
/// </summary>
public readonly int RequiredProximity;
/// <summary>
/// Base probability per turn for the location to change if near one of the RequiredLocations
/// </summary>
public readonly float Probability;
/// <summary>
/// How close the location needs to be to one of the RequiredLocations for the probability to increase
/// </summary>
public readonly int RequiredProximityForProbabilityIncrease;
/// <summary>
/// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations
/// </summary>
public readonly float ProximityProbabilityIncrease;
/// <summary>
/// The change can't happen if there's one or more of the given types of locations near this one
/// </summary>
public readonly List<string> DisallowedAdjacentLocations;
//the change can only happen if there's at least one of the given types of locations next to this one
public readonly List<string> RequiredAdjacentLocations;
/// <summary>
/// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled
/// </summary>
public readonly int DisallowedProximity;
public readonly Point RequiredDurationRange;
public LocationTypeChange(string currentType, XElement element)
{
ChangeToType = element.GetAttributeString("type", "");
Probability = element.GetAttributeFloat("probability", 1.0f);
RequiredDuration = element.GetAttributeInt("requiredduration", 0);
RequireDiscovered = element.GetAttributeBool("requirediscovered", false);
RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList();
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1);
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f)
{
if (!RequiredLocations.Any())
{
DebugConsole.AddWarning(
$"Invalid location type change in location type \"{currentType}\". "+
"Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set.");
}
if (Probability >= 1.0f)
{
DebugConsole.AddWarning(
$"Invalid location type change in location type \"{currentType}\". " +
"Probability is configured to increase when near some other type of location, but the base probability is already 100%");
}
}
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList();
DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1);
RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero);
//backwards compatibility
if (element.Attribute("requiredduration") != null)
{
RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0));
}
string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
@@ -50,15 +101,31 @@ namespace Barotrauma
public float DetermineProbability(Location location)
{
float totalProbability = Probability;
if (AnyWithinRequiredProximity(location)) { totalProbability += ProximityProbabilityIncrease; }
return totalProbability;
if (RequireDiscovered && !location.Discovered) { return 0.0f; }
if (RequiredLocations.Any() && !AnyWithinDistance(location, RequiredProximity, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); }))
{
return 0.0f;
}
if (DisallowedAdjacentLocations.Any() && AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
{
return 0.0f;
}
float probability = Probability;
if (location.ProximityTimer.ContainsKey(this))
{
if (AnyWithinDistance(location, RequiredProximityForProbabilityIncrease, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); }))
{
return probability += ProximityProbabilityIncrease * location.ProximityTimer[this];
}
}
return probability;
}
private bool AnyWithinRequiredProximity(Location location, int currentDistance = 0, HashSet<Location> checkedLocations = null)
public bool AnyWithinDistance(Location location, int maxDistance, Func<Location, bool> predicate, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > RequiredProximityForProbabilityIncrease) { return false; }
if (currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier)) { return true; }
if (currentDistance > maxDistance) { return false; }
if (currentDistance > 0 && predicate(location)) { return true; }
checkedLocations ??= new HashSet<Location>();
checkedLocations.Add(location);
@@ -68,7 +135,7 @@ namespace Barotrauma
var otherLocation = connection.OtherLocation(location);
if (!checkedLocations.Contains(otherLocation))
{
if (AnyWithinRequiredProximity(otherLocation, currentDistance + 1, checkedLocations)) { return true; }
if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; }
}
}
@@ -78,7 +145,7 @@ namespace Barotrauma
private int CountWithinRequiredProximity(Location location, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > RequiredProximityForProbabilityIncrease) { return 0; }
int count = currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier) ? 1 : 0;
int count = currentDistance > 0 && RequiredLocations.Contains(location.Type.Identifier) ? 1 : 0;
checkedLocations ??= new HashSet<Location>();
checkedLocations.Add(location);
@@ -96,6 +96,7 @@ namespace Barotrauma
{
case "connection":
Point locationIndices = subElement.GetAttributePoint("locations", new Point(0, 1));
if (locationIndices.X == locationIndices.Y) { continue; }
var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
{
Passed = subElement.GetAttributeBool("passed", false),
@@ -185,8 +186,8 @@ namespace Barotrauma
}
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
CurrentLocation.CreateStore();
CurrentLocation.Discovered = true;
CurrentLocation.CreateStore();
InitProjectSpecific();
}
@@ -247,7 +248,7 @@ namespace Barotrauma
for (int i = 0; i < 2; i++)
{
if (newLocations[i] != null) continue;
if (newLocations[i] != null) { continue; }
Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
@@ -320,7 +321,15 @@ namespace Barotrauma
{
connection.Locations[1] = Locations[i];
}
Locations[i].Connections.Add(connection);
if (connection.Locations[0] != connection.Locations[1])
{
Locations[i].Connections.Add(connection);
}
else
{
Connections.Remove(connection);
}
}
Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]);
Locations.RemoveAt(j);
@@ -548,6 +557,12 @@ namespace Barotrauma
CurrentLocation.CreateStore();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.CampaignMetadata is { } metadata)
{
metadata.SetValue("campaign.location.id", CurrentLocationIndex);
metadata.SetValue("campaign.location.name", CurrentLocation.Name);
}
}
public void SetLocation(int index)
@@ -678,92 +693,105 @@ namespace Barotrauma
{
foreach (Location location in Locations)
{
if (furthestDiscoveredLocation == null ||
location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
if (location.Discovered)
{
furthestDiscoveredLocation = location;
if (furthestDiscoveredLocation == null ||
location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
}
}
}
foreach (Location location in Locations)
{
if (location.MapPosition.X < furthestDiscoveredLocation.MapPosition.X)
if (location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
continue;
}
if (location == CurrentLocation || location == SelectedLocation) { continue; }
//find which types of locations this one can change to
var cct = location.Type.CanChangeTo;
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
List<int> readyTypeChanges = new List<int>();
for (int i = 0; i < cct.Count; i++)
ProgressLocationTypeChanges(location);
if (location.Discovered)
{
LocationTypeChange typeChange = cct[i];
if (typeChange.RequireDiscovered && !location.Discovered) { continue; }
//check if there are any adjacent locations that would prevent the change
bool disallowedFound = false;
foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations)
{
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase)))
{
disallowedFound = true;
break;
}
}
if (disallowedFound) { continue; }
//check that there's a required adjacent location present
bool requiredFound = false;
foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations)
{
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase)))
{
requiredFound = true;
break;
}
}
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) { continue; }
allowedTypeChanges.Add(typeChange);
if (location.TypeChangeTimer >= typeChange.RequiredDuration)
{
readyTypeChanges.Add(i);
}
location.UpdateStore();
}
}
}
List<float> readyTypeProbabilities = readyTypeChanges.Select(i => cct[i].DetermineProbability(location)).ToList();
//select a random type change
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => readyTypeProbabilities[i]))
private void ProgressLocationTypeChanges(Location location)
{
location.TimeSinceLastTypeChange++;
if (location.PendingLocationTypeChange != null)
{
if (location.PendingLocationTypeChange.First.DetermineProbability(location) <= 0.0f)
{
var selectedTypeChangeIndex =
ToolBox.SelectWeightedRandom(
readyTypeChanges,
readyTypeChanges.Select(i => readyTypeProbabilities[i]).ToList(),
Rand.RandSync.Unsynced);
var selectedTypeChange = cct[selectedTypeChangeIndex];
if (selectedTypeChange != null)
{
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationType(location, prevName, selectedTypeChange);
location.TypeChangeTimer = -1;
}
}
if (allowedTypeChanges.Count > 0)
{
location.TypeChangeTimer++;
//remove pending type change if it's no longer allowed
location.PendingLocationTypeChange = null;
}
else
{
location.TypeChangeTimer = 0;
location.PendingLocationTypeChange.Second--;
if (location.PendingLocationTypeChange.Second <= 0)
{
ChangeLocationType(location, location.PendingLocationTypeChange.First);
}
return;
}
location.UpdateStore();
}
//find which types of locations this one can change to
Dictionary<LocationTypeChange, float> allowedTypeChanges = new Dictionary<LocationTypeChange, float>();
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
{
float probability = typeChange.DetermineProbability(location);
if (probability <= 0.0f) { continue; }
allowedTypeChanges.Add(typeChange, probability);
}
//select a random type change
if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
{
var selectedTypeChange =
ToolBox.SelectWeightedRandom(
allowedTypeChanges.Keys.ToList(),
allowedTypeChanges.Values.ToList(),
Rand.RandSync.Unsynced);
if (selectedTypeChange != null)
{
if (selectedTypeChange.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange = new Pair<LocationTypeChange, int>(
selectedTypeChange,
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y));
}
else
{
ChangeLocationType(location, selectedTypeChange);
}
return;
}
}
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
{
if (typeChange.AnyWithinDistance(
location,
typeChange.RequiredProximityForProbabilityIncrease,
(otherLocation) => { return typeChange.RequiredLocations.Contains(otherLocation.Type.Identifier); }))
{
if (!location.ProximityTimer.ContainsKey(typeChange)) { location.ProximityTimer[typeChange] = 0; }
location.ProximityTimer[typeChange] += 1;
}
else
{
location.ProximityTimer.Remove(typeChange);
}
}
}
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
@@ -811,7 +839,18 @@ namespace Barotrauma
return distance;
}
partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change);
private void ChangeLocationType(Location location, LocationTypeChange change)
{
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationTypeProjSpecific(location, prevName, change);
location.ProximityTimer.Remove(change);
location.TimeSinceLastTypeChange = 0;
location.PendingLocationTypeChange = null;
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
partial void ClearAnimQueue();
/// <summary>
@@ -847,8 +886,19 @@ namespace Barotrauma
{
case "location":
Location location = Locations[subElement.GetAttributeInt("i", 0)];
location.TypeChangeTimer = subElement.GetAttributeInt("changetimer", 0);
location.ProximityTimer.Clear();
for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
{
location.ProximityTimer.Add(location.Type.CanChangeTo[i], subElement.GetAttributeInt("changetimer" + i, 0));
}
int locationTypeChangeIndex = subElement.GetAttributeInt("pendinglocationtypechange", -1);
if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < location.Type.CanChangeTo.Count - 1)
{
location.PendingLocationTypeChange = new Pair<LocationTypeChange, int>(
location.Type.CanChangeTo[locationTypeChangeIndex],
subElement.GetAttributeInt("pendinglocationtypechangetimer", 0));
}
location.TimeSinceLastTypeChange = subElement.GetAttributeInt("timesincelasttypechange", 0);
location.Discovered = subElement.GetAttributeBool("discovered", false);
if (location.Discovered)
{
@@ -861,7 +911,6 @@ namespace Barotrauma
}
}
string locationType = subElement.GetAttributeString("type", "");
string prevLocationName = location.Name;
LocationType prevLocationType = location.Type;
@@ -872,10 +921,14 @@ namespace Barotrauma
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
if (change != null)
{
ChangeLocationType(location, prevLocationName, change);
ChangeLocationTypeProjSpecific(location, prevLocationName, change);
location.TimeSinceLastTypeChange = 0;
}
}
location.LoadStore(subElement);
location.LoadMissions(subElement);
break;
case "connection":
int connectionIndex = subElement.GetAttributeInt("i", 0);
@@ -249,10 +249,6 @@ namespace Barotrauma
{
get { return ""; }
}
private bool ignoreByAI;
public bool IgnoreByAI => ignoreByAI;
public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore;
public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id)
{
@@ -624,6 +620,21 @@ namespace Barotrauma
continue;
}
if (t == typeof(Structure))
{
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier);
if (structurePrefab == null)
{
ItemPrefab itemPrefab = ItemPrefab.Find(name, identifier);
if (itemPrefab != null)
{
t = typeof(Item);
}
}
}
try
{
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) });
@@ -1386,10 +1386,10 @@ namespace Barotrauma
{
gotoTarget = outpost.GetHulls(true).GetRandom();
}
characterInfo.TeamID = Character.TeamType.FriendlyNPC;
characterInfo.TeamID = CharacterTeamType.FriendlyNPC;
var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
npc.AnimController.FindHull(gotoTarget.WorldPosition, true);
npc.TeamID = Character.TeamType.FriendlyNPC;
npc.TeamID = CharacterTeamType.FriendlyNPC;
if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier))
{
outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List<Character>());
@@ -1404,9 +1404,9 @@ namespace Barotrauma
npc.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
}
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server);
foreach (Item item in npc.Inventory.Items)
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
{
if (item != null) { item.SpawnedInOutpost = true; }
item.SpawnedInOutpost = true;
}
npc.GiveIdCardTags(gotoTarget as WayPoint);
if (npc.AIController is HumanAIController humanAI)
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -13,6 +12,14 @@ namespace Barotrauma
public readonly int MinAvailableAmount;
//maximum number of items available at a given store
public readonly int MaxAvailableAmount;
/// <summary>
/// Used when both <see cref="MinAvailableAmount"/> and <see cref="MaxAvailableAmount"/> are set to 0.
/// </summary>
public const int DefaultAmount = 5;
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public readonly bool CanBeSpecial;
/// <summary>
/// Support for the old style of determining item prices
@@ -23,16 +30,21 @@ namespace Barotrauma
{
Price = element.GetAttributeInt("buyprice", 0);
CanBeBought = true;
MinAvailableAmount = GetMinAmount(element);
MaxAvailableAmount = GetMaxAmount(element);
var minAmount = GetMinAmount(element);
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
var maxAmount = GetMaxAmount(element);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount);
}
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0)
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true)
{
Price = price;
CanBeBought = canBeBought;
MinAvailableAmount = minAmount;
MaxAvailableAmount = maxAmount;
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, minAmount);
CanBeSpecial = canBeSpecial;
}
public static List<Tuple<string, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
@@ -42,6 +54,7 @@ namespace Barotrauma
var soldByDefault = element.GetAttributeBool("soldbydefault", true);
var minAmount = GetMinAmount(element);
var maxAmount = GetMaxAmount(element);
var canBeSpecial = element.GetAttributeBool("canbespecial", true);
var priceInfos = new List<Tuple<string, PriceInfo>>();
foreach (XElement childElement in element.GetChildElements("price"))
@@ -51,13 +64,15 @@ namespace Barotrauma
priceInfos.Add(new Tuple<string, PriceInfo>(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(),
new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold,
minAmount: sold ? GetMinAmount(childElement, minAmount) : 0,
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0)));
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial: canBeSpecial)));
}
var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
minAmount: canBeBoughtAtOtherLocations ? minAmount : 0,
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0);
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial: canBeSpecial);
return priceInfos;
}
@@ -14,12 +14,11 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class WallSection : ISpatialEntity
partial class WallSection : IIgnorable
{
public Rectangle rect;
public float damage;
public Gap gap;
private bool ignoreByAI;
public Structure Wall { get; }
public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this));
@@ -28,7 +27,8 @@ namespace Barotrauma
public Submarine Submarine => Wall.Submarine;
public Rectangle WorldRect => Submarine == null ? rect :
new Rectangle((int)(rect.X + Submarine.Position.X), (int)(rect.Y + Submarine.Position.Y), rect.Width, rect.Height);
public bool IgnoreByAI => ignoreByAI;
public bool IgnoreByAI => OrderedToBeIgnored;
public bool OrderedToBeIgnored { get; set; }
public WallSection(Rectangle rect, Structure wall, float damage = 0.0f)
{
@@ -37,8 +37,6 @@ namespace Barotrauma
this.damage = damage;
Wall = wall;
}
public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore;
}
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
@@ -843,7 +841,7 @@ namespace Barotrauma
public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false)
{
if (!Sections.Any()) return -1;
if (Sections.None()) { return -1; }
if (world && Submarine != null)
{
@@ -857,7 +855,7 @@ namespace Barotrauma
displayPos.X += WallSectionSize - Sections[0].rect.Width;
}
int index = (IsHorizontal) ?
int index = IsHorizontal ?
(int)Math.Floor((displayPos.X - rect.X) / WallSectionSize) :
(int)Math.Floor((rect.Y - displayPos.Y) / WallSectionSize);
@@ -1082,6 +1080,7 @@ namespace Barotrauma
if (attacker != null && damageDiff != 0.0f)
{
HumanAIController.StructureDamaged(this, damageDiff, attacker);
OnHealthChangedProjSpecific(attacker, damageDiff);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -1089,7 +1088,7 @@ namespace Barotrauma
{
attacker.Info.IncreaseSkillLevel("mechanical",
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f),
SectionPosition(sectionIndex, true));
SectionPosition(sectionIndex));
}
}
}
@@ -1301,6 +1300,7 @@ namespace Barotrauma
SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion);
}
bool hasDamage = false;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -1317,7 +1317,9 @@ namespace Barotrauma
}
else
{
s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f);
float damage = subElement.GetAttributeFloat("damage", 0.0f);
s.Sections[index].damage = damage;
hasDamage |= damage > 0.0f;
}
break;
case "upgrade":
@@ -1339,8 +1341,8 @@ namespace Barotrauma
}
}
if (element.GetAttributeBool("flippedx", false)) s.FlipX(false);
if (element.GetAttributeBool("flippedy", false)) s.FlipY(false);
if (element.GetAttributeBool("flippedx", false)) { s.FlipX(false); }
if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); }
//structures with a body drop a shadow by default
if (element.Attribute("usedropshadow") == null)
@@ -1353,6 +1355,11 @@ namespace Barotrauma
s.NoAITarget = prefab.NoAITarget;
}
if (hasDamage)
{
s.UpdateSections();
}
return s;
}
@@ -25,7 +25,7 @@ namespace Barotrauma
{
public SubmarineInfo Info { get; private set; }
public Character.TeamType TeamID = Character.TeamType.None;
public CharacterTeamType TeamID = CharacterTeamType.None;
public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f);
//position of the "actual submarine" which is rendered wherever the SubmarineBody is
@@ -254,7 +254,7 @@ namespace Barotrauma
get
{
if (Level.Loaded == null || subBody == null) { return false; }
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth;
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth & RealWorldDepth > RealWorldCrushDepth;
}
}
@@ -325,7 +325,7 @@ namespace Barotrauma
Info.Type = SubmarineType.Wreck;
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = Character.TeamType.None;
TeamID = CharacterTeamType.None;
string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this);
ReplaceIDCardTagRequirements("wreck_id", defaultTag);
@@ -554,7 +554,7 @@ namespace Barotrauma
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
}
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - 10);
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
return spawnPos - diffFromDockedBorders;
}
@@ -1332,7 +1332,7 @@ namespace Barotrauma
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = Character.TeamType.FriendlyNPC;
TeamID = CharacterTeamType.FriendlyNPC;
foreach (MapEntity me in MapEntity.mapEntityList)
{
@@ -1495,7 +1495,19 @@ namespace Barotrauma
if (e is Item item)
{
if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; }
#if CLIENT
if (Screen.Selected != GameMain.SubEditorScreen)
{
if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; }
}
else
{
e.Submarine = this;
}
#else
if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; }
#endif
}
else
{
@@ -1517,6 +1529,10 @@ namespace Barotrauma
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
Name = Path.GetFileNameWithoutExtension(filePath)
};
#if CLIENT
//remove reference to the preview image from the old info, so we don't dispose it (the new info still uses the texture)
Info.PreviewImage = null;
#endif
Info.Dispose(); Info = newInfo;
return newInfo.SaveAs(filePath, previewImage);
}
@@ -1691,6 +1707,7 @@ namespace Barotrauma
}
}
}
node.Waypoint.FindHull();
}
}
@@ -1705,6 +1722,7 @@ namespace Barotrauma
nodes.Clear();
obstructedNodes.Remove(otherSub);
}
OutdoorNodes.ForEach(n => n.Waypoint.FindHull());
}
}
}
@@ -450,16 +450,19 @@ namespace Barotrauma
#endif
if (Level.Loaded == null) { return; }
float submarineDepth = submarine.RealWorldDepth;
if (submarineDepth < Level.Loaded.RealWorldCrushDepth) { return; }
if (!Submarine.AtDamageDepth) { return; }
depthDamageTimer -= deltaTime;
if (depthDamageTimer > 0.0f) { return; }
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != submarine || wall.CrushDepth > submarineDepth) { continue; }
if (wall.Submarine != submarine) { continue; }
float pastCrushDepth = submarineDepth - wall.CrushDepth;
float wallCrushDepth = wall.CrushDepth;
if (submarine.Info.SubmarineClass == SubmarineClass.DeepDiver) { wallCrushDepth *= 1.2f; }
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
if (pastCrushDepth < 0) { return; }
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f);
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
@@ -108,8 +108,11 @@ namespace Barotrauma
{
if (hash == null)
{
XDocument doc = OpenFile(FilePath);
StartHashDocTask(doc);
if (hashTask == null)
{
XDocument doc = OpenFile(FilePath);
StartHashDocTask(doc);
}
hashTask.Wait();
hashTask = null;
}
@@ -118,6 +121,11 @@ namespace Barotrauma
}
}
public bool CalculatingHash
{
get { return hashTask != null && !hashTask.IsCompleted; }
}
public Vector2 Dimensions
{
get;
@@ -373,6 +381,10 @@ namespace Barotrauma
public void Dispose()
{
#if CLIENT
PreviewImage?.Remove();
PreviewImage = null;
#endif
if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); }
}
@@ -522,12 +534,13 @@ namespace Barotrauma
for (int i = savedSubmarines.Count - 1; i >= 0; i--)
{
if (File.Exists(savedSubmarines[i].FilePath) &&
savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) &&
(Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) ||
contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath())))
if (File.Exists(savedSubmarines[i].FilePath))
{
continue;
bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath);
bool isInContentPackage = contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath());
if (isDownloadedSub) { continue; }
if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; }
}
savedSubmarines[i].Dispose();
}
@@ -18,7 +18,7 @@ namespace Barotrauma
public static bool ShowWayPoints = true, ShowSpawnPoints = true;
public const float LadderWaypointInterval = 100.0f;
public const float LadderWaypointInterval = 70.0f;
protected SpawnType spawnType;
private string[] idCardTags;
@@ -99,6 +99,13 @@ namespace Barotrauma
{
SpawnType = SpawnType.Path;
}
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
}
#endif
}
@@ -179,42 +186,55 @@ namespace Barotrauma
}
}
float diffFromHullEdge = 50;
float minDist = 150.0f;
float minDist = 100.0f;
float heightFromFloor = 110.0f;
float hullMinHeight = 100;
foreach (Hull hull in Hull.hullList)
{
if (hull.Rect.Height < 150) { continue; }
WayPoint prevWaypoint = null;
// Ignore hulls that a human couldn't fit in.
// Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually.
if (hull.Rect.Height < hullMinHeight) { continue; }
// Don't create waypoints if there's no floor.
Vector2 floorPos = new Vector2(hull.SimPosition.X, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50));
Body floor = Submarine.PickBody(hull.SimPosition, floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine));
if (floor == null) { continue; }
// Make sure that the waypoints don't go higher than the halfway of the room.
float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2;
if (hull.Rect.Width < diffFromHullEdge * 3.0f)
{
new WayPoint(
new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
continue;
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
else
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); }
prevWaypoint = wayPoint;
WayPoint prevWaypoint = null;
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); }
prevWaypoint = wayPoint;
}
if (prevWaypoint == null)
{
// Ensure that we always create at least one waypoint per hull.
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
}
}
float outSideWaypointInterval = 200.0f;
float outSideWaypointInterval = 100.0f;
if (submarine.Info.Type != SubmarineType.OutpostModule)
{
int outsideWaypointDist = 100;
List<WayPoint> outsideWaypoints = new List<WayPoint>();
Rectangle borders = Hull.GetBorders();
borders.X -= outsideWaypointDist;
borders.Y += outsideWaypointDist;
borders.Width += outsideWaypointDist * 2;
borders.Height += outsideWaypointDist * 2;
int originalWidth = borders.Width;
int originalHeight = borders.Height;
borders.X -= Math.Min(500, originalWidth / 4);
borders.Y += Math.Min(500, originalHeight / 4);
borders.Width += Math.Min(1500, originalWidth / 2);
borders.Height += Math.Min(1000, originalHeight / 2);
borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
if (borders.Width <= outSideWaypointInterval * 2)
@@ -238,6 +258,8 @@ namespace Barotrauma
new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition,
SpawnType.Path, submarine);
outsideWaypoints.Add(wayPoint);
if (x == borders.X + outSideWaypointInterval)
{
cornerWaypoint[i, 0] = wayPoint;
@@ -260,18 +282,107 @@ namespace Barotrauma
new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition,
SpawnType.Path, submarine);
outsideWaypoints.Add(wayPoint);
if (y == borders.Y - borders.Height)
{
wayPoint.ConnectTo(cornerWaypoint[1, i]);
}
else
{
wayPoint.ConnectTo(WayPoint.WayPointList[WayPointList.Count - 2]);
wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
}
}
wayPoint.ConnectTo(cornerWaypoint[0, i]);
}
Vector2 center = ConvertUnits.ToSimUnits(submarine.HiddenSubPosition);
float halfHeight = ConvertUnits.ToSimUnits(borders.Height / 2);
// Try to move the waypoints so that they are near the walls, roughly following the shape of the sub.
foreach (WayPoint wp in outsideWaypoints)
{
float xDiff = center.X - wp.SimPosition.X;
Vector2 targetPos = new Vector2(center.X - xDiff * 0.5f, center.Y);
Body wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
if (wall == null)
{
// Try again, and shoot to the center now. It happens with some subs that the first, offset raycast don't hit the walls.
targetPos = new Vector2(center.X - xDiff, center.Y);
wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
}
if (wall != null)
{
float distanceFromWall = 1;
if (xDiff > 0 && !submarine.Info.HasTag(SubmarineTag.Shuttle))
{
// We don't want to move the waypoints near the tail too close to the engine.
float yDist = Math.Abs(center.Y - wp.SimPosition.Y);
distanceFromWall = MathHelper.Lerp(1, 3, MathUtils.InverseLerp(halfHeight, 0, yDist));
}
Vector2 newPos = Submarine.LastPickedPosition + Submarine.LastPickedNormal * distanceFromWall;
wp.rect = new Rectangle(ConvertUnits.ToDisplayUnits(newPos).ToPoint(), wp.rect.Size);
wp.FindHull();
}
}
// Remove unwanted points
var removals = new List<WayPoint>();
WayPoint previous = null;
float tooClose = outSideWaypointInterval / 2;
foreach (WayPoint wp in outsideWaypoints)
{
if (wp.CurrentHull != null ||
Submarine.PickBody(wp.SimPosition, wp.SimPosition + Vector2.Normalize(center - wp.SimPosition) * 0.1f, collisionCategory: Physics.CollisionWall | Physics.CollisionItem, customPredicate: f => !(f.Body.UserData is Submarine), allowInsideFixture: true) != null)
{
// Remove waypoints that got inside/too near the sub.
removals.Add(wp);
previous = wp;
continue;
}
foreach (WayPoint otherWp in outsideWaypoints)
{
if (otherWp == wp) { continue; }
if (removals.Contains(otherWp)) { continue; }
float sqrDist = Vector2.DistanceSquared(wp.Position, otherWp.Position);
// Remove waypoints that are too close to each other.
if (!removals.Contains(previous) && sqrDist < tooClose * tooClose)
{
removals.Add(wp);
}
}
previous = wp;
}
foreach (WayPoint wp in removals)
{
outsideWaypoints.Remove(wp);
wp.Remove();
}
// Connect loose ends (TODO: this sometimes fails, creating the connection to a wrong node)
for (int i = 0; i < outsideWaypoints.Count; i++)
{
WayPoint current = outsideWaypoints[i];
if (current.linkedTo.Count > 1) { continue; }
WayPoint next = null;
int maxConnections = 2;
float tooFar = outSideWaypointInterval * 5;
for (int j = 0; j < maxConnections; j++)
{
if (current.linkedTo.Count >= maxConnections) { break; }
tooFar /= current.linkedTo.Count;
// First try to find a loose end
next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.linkedTo.Count < 2);
// Then accept any connection that not connected to the existing connection
next ??= current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e)));
if (next != null)
{
current.ConnectTo(next);
}
}
if (current.linkedTo.Count == 1)
{
DebugConsole.ThrowError($"Couldn't automatically link waypoint {current.ID}. You should do it manually.");
}
}
}
List<Structure> stairList = new List<Structure>();
@@ -298,13 +409,13 @@ namespace Barotrauma
{
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = stairPoints[i].FindClosest(dir, true, new Vector2(-30.0f, 30f));
if (closest == null) continue;
WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70));
if (closest == null) { continue; }
stairPoints[i].ConnectTo(closest);
}
}
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position)/2, SpawnType.Path, submarine);
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine);
stairPoints[0].ConnectTo(stairPoints[2]);
stairPoints[2].ConnectTo(stairPoints[1]);
}
@@ -314,21 +425,44 @@ namespace Barotrauma
var ladders = item.GetComponent<Ladder>();
if (ladders == null) { continue; }
Vector2 bottomPoint = new Vector2(item.Rect.Center.X, item.Rect.Top - item.Rect.Height + 10);
List<WayPoint> ladderPoints = new List<WayPoint>
{
new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height + heightFromFloor), SpawnType.Path, submarine)
new WayPoint(bottomPoint, SpawnType.Path, submarine),
};
WayPoint prevPoint = ladderPoints[0];
Vector2 prevPos = prevPoint.SimPosition;
List<Body> ignoredBodies = new List<Body>();
for (float y = ladderPoints[0].Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
// Lowest point is only meaningful for hanging ladders inside the sub, but it shouldn't matter in other cases either.
// Start point is where the bots normally grasp the ladder when they stand on ground.
WayPoint lowestPoint = ladderPoints[0];
WayPoint prevPoint = lowestPoint;
Vector2 prevPos = prevPoint.SimPosition;
Body ground = Submarine.PickBody(lowestPoint.SimPosition, lowestPoint.SimPosition - Vector2.UnitY, ignoredBodies,
collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform | Physics.CollisionStairs,
customPredicate: f => !(f.Body.UserData is Submarine));
float startHeight = ground != null ? ConvertUnits.ToDisplayUnits(ground.Position.Y) : bottomPoint.Y;
startHeight += heightFromFloor;
WayPoint startPoint = lowestPoint;
Vector2 nextPos = new Vector2(item.Rect.Center.X, startHeight);
// Don't create the start point if it's too close to the lowest point or if it's outside of the sub.
// If we skip creating the start point, the lowest point is used instead.
if (lowestPoint == null || Math.Abs(startPoint.Position.Y - startHeight) > 40 && Hull.FindHull(nextPos) != null)
{
startPoint = new WayPoint(nextPos, SpawnType.Path, submarine);
ladderPoints.Add(startPoint);
if (lowestPoint != null)
{
startPoint.ConnectTo(lowestPoint);
}
prevPoint = startPoint;
prevPos = prevPoint.SimPosition;
}
for (float y = startPoint.Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
{
//first check if there's a door in the way
//(we need to create a waypoint linked to the door for NPCs to open it)
Body pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)),
ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)),
prevPos, ignoredBodies, Physics.CollisionWall, false,
(Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent<Door>() != null);
@@ -341,7 +475,7 @@ namespace Barotrauma
{
//no door, check for walls
pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false,
ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, null, false,
(Fixture f) => f.Body.UserData is Structure);
}
@@ -374,75 +508,94 @@ namespace Barotrauma
}
}
if (prevPoint.rect.Y < item.Rect.Y - 10.0f)
// Cap
if (prevPoint.rect.Y < item.Rect.Y - 40)
{
WayPoint newPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
ladderPoints.Add(newPoint);
newPoint.ConnectTo(prevPoint);
WayPoint wayPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
ladderPoints.Add(wayPoint);
wayPoint.ConnectTo(prevPoint);
}
//connect ladder waypoints to hull points at the right and left side
// Connect ladder waypoints to hull points at the right and left side
foreach (WayPoint ladderPoint in ladderPoints)
{
ladderPoint.Ladders = ladders;
//don't connect if the waypoint is at a gap (= at the boundary of hulls and/or at a hatch)
if (ladderPoint.ConnectedGap != null) continue;
bool isHatch = ladderPoint.ConnectedGap != null && !ladderPoint.ConnectedGap.IsRoomToRoom;
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 50f));
if (closest == null) continue;
WayPoint closest = null;
if (isHatch)
{
closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderPoints);
}
else
{
closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 70), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints);
}
if (closest == null) { continue; }
ladderPoint.ConnectTo(closest);
}
}
}
foreach (Gap gap in Gap.GapList)
// Another pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders)
foreach (Item item in Item.ItemList)
{
if (!gap.IsHorizontal) continue;
//too small to walk through
if (gap.Rect.Height < 150.0f) continue;
var wayPoint = new WayPoint(
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, submarine, gap);
for (int dir = -1; dir <= 1; dir += 2)
{
float tolerance = gap.IsRoomToRoom ? 50.0f : outSideWaypointInterval / 2.0f;
WayPoint closest = wayPoint.FindClosest(
dir, true, new Vector2(-tolerance, tolerance),
gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
var ladders = item.GetComponent<Ladder>();
if (ladders == null) { continue; }
var wps = WayPointList.Where(wp => wp.Ladders == ladders).OrderByDescending(wp => wp.Rect.Y);
WayPoint cap = wps.First();
WayPoint above = cap.FindClosest(1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
above?.ConnectTo(cap);
WayPoint bottom = wps.Last();
WayPoint below = bottom.FindClosest(-1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
below?.ConnectTo(bottom);
}
foreach (Gap gap in Gap.GapList)
{
if (gap.IsHorizontal || gap.IsRoomToRoom || !gap.linkedTo.Any(l => l is Hull)) { continue; }
//too small to walk through
if (gap.Rect.Width < 100.0f) { continue; }
var wayPoint = new WayPoint(
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2), SpawnType.Path, submarine, gap);
float tolerance = outSideWaypointInterval / 2.0f;
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(
dir, false, new Vector2(-tolerance, tolerance),
gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
if (gap.IsHorizontal)
{
wayPoint.ConnectTo(closest);
// Too small to walk through
if (gap.Rect.Height < hullMinHeight) { continue; }
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
// The closest waypoint can be quite far if the gap is at an exterior door.
Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000);
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
}
else
{
// Create waypoints on vertical gaps on the outer walls, also hatches.
if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull)) { continue; }
// Too small to swim through
if (gap.Rect.Width < 50.0f) { continue; }
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2);
// Some hatches are created in the block above where we handle the ladder waypoints. So we need to check for duplicates.
if (WayPointList.Any(wp => wp.ConnectedGap == gap)) { continue; }
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100));
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
for (dir = -1; dir <= 1; dir += 2)
{
closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
}
}
@@ -462,7 +615,36 @@ namespace Barotrauma
return true;
}
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null)
private WayPoint FindClosestOutside(IEnumerable<WayPoint> waypointList, float tolerance, Body ignoredBody = null, IEnumerable<WayPoint> ignored = null, Func<WayPoint, bool> filter = null)
{
float closestDist = 0;
WayPoint closest = null;
foreach (WayPoint wp in waypointList)
{
if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
// Ignore if already linked
if (linkedTo.Contains(wp)) { continue; }
if (ignored != null && ignored.Contains(wp)) { continue; }
if (filter != null && !filter(wp)) { continue; }
float sqrDist = Vector2.DistanceSquared(Position, wp.Position);
if (closest == null || sqrDist < closestDist)
{
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
{
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
{
continue;
}
}
closestDist = sqrDist;
closest = wp;
}
}
return closest;
}
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null, IEnumerable<WayPoint> ignored = null, Func<WayPoint, bool> filter = null)
{
if (dir != -1 && dir != 1) { return null; }
@@ -473,33 +655,45 @@ namespace Barotrauma
{
if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
float xDiff = wp.Position.X - Position.X;
float yDiff = wp.Position.Y - Position.Y;
float xDist = Math.Abs(xDiff);
float yDist = Math.Abs(yDiff);
if (tolerance.X < xDist) { continue; }
if (tolerance.Y < yDist) { continue; }
float dist = 0.0f;
float diff = 0.0f;
if (horizontalSearch)
{
if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) { continue; }
diff = wp.Position.X - Position.X;
dist = Math.Abs(diff) + Math.Abs(wp.Position.Y - Position.Y) / 5.0f;
diff = xDiff;
dist = xDist + yDist / 5.0f;
}
else
{
if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) { continue; }
diff = wp.Position.Y - Position.Y;
dist = Math.Abs(diff) + Math.Abs(wp.Position.X - Position.X) / 5.0f;
diff = yDiff;
dist = yDist + xDist / 5.0f;
//prefer ladder waypoints when moving vertically
if (wp.Ladders != null) { dist *= 0.5f; }
}
if (Math.Sign(diff) != dir) { continue; }
// Ignore if already linked
if (linkedTo.Contains(wp)) { continue; }
if (ignored != null && ignored.Contains(wp)) { continue; }
if (filter != null && !filter(wp)) { continue; }
if (closest == null || dist < closestDist)
{
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, true, true, false);
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
{
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
{
continue;
}
}
closestDist = dist;
closest = wp;
}