Unstable 1.8.4.0
This commit is contained in:
@@ -623,7 +623,8 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
List<BallastFloraBranch> list = branches[hull];
|
||||
if (!list.Any(HasAcidEmitter))
|
||||
{
|
||||
BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced();
|
||||
BallastFloraBranch? randomBranch = branches[hull].GetRandomUnsynced();
|
||||
if (randomBranch == null) { continue; }
|
||||
randomBranch.SpawningItem = true;
|
||||
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab);
|
||||
|
||||
@@ -86,7 +86,12 @@ namespace Barotrauma
|
||||
public readonly UInt64 CreationIndex;
|
||||
public string ErrorLine
|
||||
=> $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Which content package is this entity from (if it's something like an item or a character that's loaded from a package, otherwise we assume it's the vanilla package).
|
||||
/// </summary>
|
||||
public virtual ContentPackage ContentPackage => GameMain.VanillaContent;
|
||||
|
||||
public Entity(Submarine submarine, ushort id)
|
||||
{
|
||||
this.Submarine = submarine;
|
||||
|
||||
@@ -239,6 +239,8 @@ namespace Barotrauma
|
||||
OnlyInside = element.GetAttributeBool("onlyinside", false);
|
||||
OnlyOutside = element.GetAttributeBool("onlyoutside", false);
|
||||
|
||||
DistanceFalloff = element.GetAttributeBool(nameof(DistanceFalloff), true);
|
||||
|
||||
flash = element.GetAttributeBool("flash", showEffects);
|
||||
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
|
||||
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
|
||||
@@ -315,9 +317,9 @@ namespace Barotrauma
|
||||
Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f));
|
||||
Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration);
|
||||
}
|
||||
foreach (Item item in Item.ItemList)
|
||||
foreach (Sonar sonar in Sonar.SonarList)
|
||||
{
|
||||
item.GetComponent<Sonar>()?.RegisterExplosion(this, worldPosition);
|
||||
sonar.RegisterExplosion(this, worldPosition);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -436,19 +438,23 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
|
||||
if (!item.Indestructible)
|
||||
{
|
||||
float distFactor =
|
||||
DistanceFalloff ?
|
||||
1.0f - dist / displayRange :
|
||||
1.0f;
|
||||
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
|
||||
if (item.Prefab.DamagedByExplosions ||
|
||||
(item.Prefab.DamagedByContainedItemExplosions && item.ContainedItems.Contains(damageSource)))
|
||||
{
|
||||
float distFactor =
|
||||
DistanceFalloff ?
|
||||
1.0f - dist / displayRange :
|
||||
1.0f;
|
||||
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
|
||||
|
||||
Vector2 explosionPos = worldPosition;
|
||||
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
|
||||
Vector2 explosionPos = worldPosition;
|
||||
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
|
||||
|
||||
damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover);
|
||||
item.Condition -= damageAmount * distFactor;
|
||||
damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover);
|
||||
item.Condition -= damageAmount * distFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,18 @@ namespace Barotrauma
|
||||
|
||||
public readonly float GlowEffectT;
|
||||
|
||||
private readonly List<Gap> overlappingGaps = new List<Gap>();
|
||||
|
||||
/// <summary>
|
||||
/// Do we need to recheck which gaps are overlapping with this one, and how much they should reduce this gap's flow?
|
||||
/// </summary>
|
||||
private bool overlappingGapsDirty;
|
||||
|
||||
/// <summary>
|
||||
/// How much overlapping gaps reduce the flow rate of this one?
|
||||
/// </summary>
|
||||
private float overlappingGapFlowRateReduction;
|
||||
|
||||
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
|
||||
private float open;
|
||||
|
||||
@@ -67,22 +79,29 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (float.IsNaN(value)) { return; }
|
||||
float prevValue = open;
|
||||
if (value > open)
|
||||
{
|
||||
openedTimer = 1.0f;
|
||||
}
|
||||
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
|
||||
|
||||
open = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
if (!MathUtils.NearlyEqual(open, prevValue))
|
||||
{
|
||||
if (value > open && value >= 1.0f)
|
||||
overlappingGapsDirty = true;
|
||||
FlagOverlappingGapsDirty();
|
||||
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: true);
|
||||
}
|
||||
else if (value < open && open >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: false);
|
||||
if (open > prevValue && open >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: true);
|
||||
}
|
||||
else if (open < prevValue && prevValue >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
open = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
|
||||
static void InformWaypointsAboutGapState(Gap gap, bool open)
|
||||
{
|
||||
@@ -205,7 +224,7 @@ namespace Barotrauma
|
||||
Physics.CollisionWall,
|
||||
Physics.CollisionCharacter,
|
||||
findNewContacts: false);
|
||||
outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})";
|
||||
outsideCollisionBlocker.UserData = this;
|
||||
outsideCollisionBlocker.Enabled = false;
|
||||
#if CLIENT
|
||||
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
|
||||
@@ -338,7 +357,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (hulls[i] == null) { continue; }
|
||||
linkedTo.Add(hulls[i]);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); }
|
||||
foreach (var gap in hulls[i].ConnectedGaps)
|
||||
{
|
||||
gap.overlappingGapsDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +387,12 @@ namespace Barotrauma
|
||||
deltaTime *= updateCount;
|
||||
updateCount = 0;
|
||||
|
||||
if (overlappingGapsDirty)
|
||||
{
|
||||
RefreshOverlappingGaps();
|
||||
overlappingGapsDirty = false;
|
||||
}
|
||||
|
||||
flowForce = Vector2.Zero;
|
||||
outsideColliderRaycastTimer -= deltaTime;
|
||||
|
||||
@@ -431,7 +460,7 @@ namespace Barotrauma
|
||||
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = Size / 100.0f * open;
|
||||
float sizeModifier = Size / 100.0f * open * (1.0f - overlappingGapFlowRateReduction);
|
||||
|
||||
//horizontal gap (such as a regular door)
|
||||
if (IsHorizontal)
|
||||
@@ -597,7 +626,7 @@ namespace Barotrauma
|
||||
{
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = Size * open * open;
|
||||
float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction);
|
||||
|
||||
float delta = 500.0f * sizeModifier * deltaTime;
|
||||
|
||||
@@ -790,6 +819,52 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RefreshOverlappingGaps()
|
||||
{
|
||||
overlappingGapFlowRateReduction = 0.0f;
|
||||
overlappingGaps.Clear();
|
||||
foreach (var linked in linkedTo)
|
||||
{
|
||||
if (linked is not Hull hull) { continue; }
|
||||
foreach (var connectedGap in hull.ConnectedGaps)
|
||||
{
|
||||
if (connectedGap == this) { continue; }
|
||||
//let the "more open" gap reduce this gap's flow rate
|
||||
//or if they're both equally open, let the one that was created first handle it
|
||||
//(note that we can't use Entity.ID here because gaps on walls don't have IDs)
|
||||
if (connectedGap.open > open ||
|
||||
(connectedGap.open == open && connectedGap.CreationIndex < CreationIndex))
|
||||
{
|
||||
Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect);
|
||||
if (intersection.Width > 0 && intersection.Height > 0)
|
||||
{
|
||||
//reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is
|
||||
float relativeOverlap = IsHorizontal ?
|
||||
intersection.Height / (float)rect.Height :
|
||||
intersection.Width / (float)rect.Width;
|
||||
overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open;
|
||||
}
|
||||
}
|
||||
if (overlappingGapFlowRateReduction >= 1.0f)
|
||||
{
|
||||
overlappingGapFlowRateReduction = 1.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark all gaps that are currently known to overlap with this one as needing a refresh of overlapping gaps
|
||||
/// </summary>
|
||||
private void FlagOverlappingGapsDirty()
|
||||
{
|
||||
foreach (var overlappingGap in overlappingGaps)
|
||||
{
|
||||
overlappingGap.overlappingGapsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ShallowRemove()
|
||||
{
|
||||
base.ShallowRemove();
|
||||
|
||||
@@ -303,7 +303,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) { return; }
|
||||
waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
|
||||
if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; }
|
||||
if (waterVolume <= Volume)
|
||||
{
|
||||
//recalculate pressure, but only if there's less water than the volume, above that point the "overpressure" logic kicks in
|
||||
Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
|
||||
}
|
||||
if (waterVolume > 0.0f)
|
||||
{
|
||||
update = true;
|
||||
@@ -542,7 +546,7 @@ namespace Barotrauma
|
||||
var clone = new Hull(rect, Submarine);
|
||||
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
|
||||
{
|
||||
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
|
||||
if (!property.Value.Attributes.OfType<Serialize>().Any()) { continue; }
|
||||
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
|
||||
}
|
||||
#if CLIENT
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Barotrauma
|
||||
|
||||
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true);
|
||||
|
||||
|
||||
public readonly struct AttackEventData
|
||||
{
|
||||
public readonly ISpatialEntity Attacker;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -8,6 +12,112 @@ namespace Barotrauma
|
||||
Vector2 WorldPosition { get; }
|
||||
Vector2 SimPosition { get; }
|
||||
Submarine Submarine { get; }
|
||||
|
||||
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
if (seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
|
||||
}
|
||||
if (target is Character targetCharacter)
|
||||
{
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (!target.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
//find the limbs that are furthest from the target's position (from the viewer's point of view)
|
||||
Limb leftExtremity = null, rightExtremity = null;
|
||||
float leftMostDot = 0.0f, rightMostDot = 0.0f;
|
||||
Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
|
||||
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
|
||||
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
|
||||
foreach (Limb limb in target.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
|
||||
float leftDot = Vector2.Dot(limbDir, leftDir);
|
||||
if (leftDot > leftMostDot)
|
||||
{
|
||||
leftMostDot = leftDot;
|
||||
leftExtremity = limb;
|
||||
continue;
|
||||
}
|
||||
float rightDot = Vector2.Dot(limbDir, rightDir);
|
||||
if (rightDot > rightMostDot)
|
||||
{
|
||||
rightMostDot = rightDot;
|
||||
rightExtremity = limb;
|
||||
}
|
||||
}
|
||||
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
|
||||
if (checkFacing && seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
|
||||
}
|
||||
//both inside the same sub (or both outside)
|
||||
//OR the we're inside, the other character outside
|
||||
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
|
||||
{
|
||||
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//we're outside, the other character inside
|
||||
else if (seeingEntity.Submarine == null)
|
||||
{
|
||||
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//both inside different subs
|
||||
else
|
||||
{
|
||||
return
|
||||
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
|
||||
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
|
||||
bool IsBlocking(Fixture f)
|
||||
{
|
||||
var body = f.Body;
|
||||
if (body == null) { return false; }
|
||||
if (body.UserData is Structure wall)
|
||||
{
|
||||
if (!wall.CastShadow && seeThroughWindows) { return false; }
|
||||
return wall != target;
|
||||
}
|
||||
else if (body.UserData is Item item)
|
||||
{
|
||||
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
|
||||
{
|
||||
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
|
||||
}
|
||||
return item != target;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IIgnorable : ISpatialEntity
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace Barotrauma
|
||||
|
||||
Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant());
|
||||
Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty);
|
||||
float scale = entityElement.GetAttributeFloat("scale", 1.0f);
|
||||
float rotation = MathHelper.ToRadians(entityElement.GetAttributeFloat("rotation", 0.0f));
|
||||
if (!entityElement.GetAttributeBool("hideinassemblypreview", false))
|
||||
{
|
||||
@@ -180,7 +181,7 @@ namespace Barotrauma
|
||||
if (ContentPackage is { Files: { Length: 1 } }
|
||||
&& ContentPackageManager.LocalPackages.Contains(ContentPackage))
|
||||
{
|
||||
Directory.Delete(ContentPackage.Dir, recursive: true);
|
||||
Directory.Delete(ContentPackage.Dir, recursive: true, catchUnauthorizedAccessExceptions: false);
|
||||
ContentPackageManager.LocalPackages.Refresh();
|
||||
ContentPackageManager.EnabledPackages.DisableRemovedMods();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -21,6 +21,8 @@ namespace Barotrauma
|
||||
public float ActualMaxDifficulty => maxDifficulty;
|
||||
public float AdjustedMaxDifficulty => maxDifficulty - 0.1f;
|
||||
|
||||
public readonly float ExperienceFromMissionRewards;
|
||||
|
||||
|
||||
public readonly ImmutableHashSet<int> AllowedZones;
|
||||
|
||||
@@ -50,6 +52,10 @@ namespace Barotrauma
|
||||
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
|
||||
MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0);
|
||||
maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
|
||||
float baseExperience = 0.09f;
|
||||
float difficultyRewardMultiplier = 0.25f;
|
||||
float calculateDefaultExperience = baseExperience + MinDifficulty * difficultyRewardMultiplier / 100;
|
||||
ExperienceFromMissionRewards = element.GetAttributeFloat("ExperienceFromMissionRewards", calculateDefaultExperience);
|
||||
|
||||
var submarineAvailabilityOverrides = new HashSet<SubmarineAvailability>();
|
||||
if (element.GetChildElement("submarines") is ContentXElement availabilityElement)
|
||||
|
||||
@@ -217,8 +217,13 @@ namespace Barotrauma
|
||||
/// Makes the cell rounder by subdividing the edges and offsetting them at the middle
|
||||
/// </summary>
|
||||
/// <param name="minEdgeLength">How small the individual subdivided edges can be (smaller values produce rounder shapes, but require more geometry)</param>
|
||||
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f)
|
||||
/// <param name="minThickness">How "thin" irregularity is allowed to make parts of the cell. Very high irregularity values can lead to thin "spikes" or even parts where the "spike's" thickness becomes negative and wall segments intersect each other.</param>
|
||||
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f, float minThickness = 0.0f)
|
||||
{
|
||||
//we need to make sure the vertices of the wall are still ordered counter-clockwise -
|
||||
//if we deform some vertices so much the cell becomes concave, rendering the triangles will break (parts of the inside of the wall will render outside the edges)
|
||||
var compareCCW = new CompareCCW(cell.Center);
|
||||
|
||||
List<GraphEdge> tempEdges = new List<GraphEdge>();
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
@@ -231,8 +236,10 @@ namespace Barotrauma
|
||||
Vector2 edgeDiff = edge.Point2 - edge.Point1;
|
||||
Vector2 edgeDir = Vector2.Normalize(edgeDiff);
|
||||
|
||||
float maxExtrusion = float.PositiveInfinity;
|
||||
const float minPassageWidth = 200.0f;
|
||||
//If the edge is next to an empty cell and there's another solid cell at the other side of the empty one,
|
||||
//don't touch this edge. Otherwise we may end up closing off small passages between cells.
|
||||
//we need to calculate how far we can extrude the edge so it doesn't end up closing off small passages between cells.
|
||||
var adjacentEmptyCell = edge.AdjacentCell(cell);
|
||||
if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; }
|
||||
if (adjacentEmptyCell != null)
|
||||
@@ -252,8 +259,15 @@ namespace Barotrauma
|
||||
}
|
||||
if (adjacentEdge != null)
|
||||
{
|
||||
tempEdges.Add(edge);
|
||||
continue;
|
||||
maxExtrusion =
|
||||
new[]
|
||||
{
|
||||
Vector2.Distance(edge.Point1, adjacentEdge.Point1),
|
||||
Vector2.Distance(edge.Point1, adjacentEdge.Point2),
|
||||
Vector2.Distance(edge.Point1, adjacentEdge.Point2),
|
||||
Vector2.Distance(edge.Point2, adjacentEdge.Point1),
|
||||
}.Min();
|
||||
maxExtrusion = Math.Max(0, maxExtrusion - minPassageWidth);
|
||||
}
|
||||
}
|
||||
List<Vector2> edgePoints = new List<Vector2>();
|
||||
@@ -274,12 +288,53 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
|
||||
//value that's 0 at edges, 0.5 at center
|
||||
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
|
||||
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient);
|
||||
Vector2 extrudedPoint =
|
||||
//make the value "curve" from 0 to 1 at the center, instead of going linearly from 0 to 1
|
||||
centerF = MathF.Sin(centerF * MathHelper.Pi);
|
||||
|
||||
//magic number intended to make old rounding values behave roughly the same with the new formula
|
||||
//previously the extrusion increased linearly towards the center, forming a "spike" like "/\"
|
||||
//now it follows a sine curve, which makes lower values produce rounder results
|
||||
const float RoundingScale = 0.25f;
|
||||
|
||||
//magic number intended to make old variance values behave roughly the same with the new formula
|
||||
//previously a value of 1 would allow a maximum extrusion of 50% of the edge's length at the center,
|
||||
//now we extrude by the variance at any point on the edge (not more in the center)
|
||||
const float RandomVarianceScale = 0.25f;
|
||||
float randomVariance = irregularity * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
|
||||
|
||||
float extrusionAmount = edgeLength * ((roundingAmount * RoundingScale * centerF) + randomVariance * RandomVarianceScale);
|
||||
extrusionAmount = Math.Min(extrusionAmount, maxExtrusion);
|
||||
|
||||
Vector2 nonExtrudedPoint =
|
||||
edge.Point1 +
|
||||
edgeDiff * (i / (float)pointCount) +
|
||||
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
|
||||
edgeDiff * (i / (float)pointCount);
|
||||
|
||||
Vector2 nextPoint =
|
||||
edge.Point1 +
|
||||
edgeDiff * ((i + 1) / (float)pointCount);
|
||||
|
||||
//"extruding" inwards, need to make sure we don't make the edge poke through the cell from the other side
|
||||
if (extrusionAmount < 0 && minThickness > 0.0f)
|
||||
{
|
||||
foreach (GraphEdge otherEdge in cell.Edges)
|
||||
{
|
||||
if (otherEdge == edge) { continue; }
|
||||
float margin = minThickness * Math.Sign(extrusionAmount);
|
||||
if (MathUtils.GetLineIntersection(
|
||||
nonExtrudedPoint, nonExtrudedPoint + edgeNormal * (extrusionAmount + margin),
|
||||
otherEdge.Point1, otherEdge.Point2, areLinesInfinite: false, out Vector2 intersection))
|
||||
{
|
||||
extrusionAmount = Math.Min(extrusionAmount, Vector2.Distance(edge.Point1, intersection)) - margin;
|
||||
//make sure we don't "overshoot", fix the inwards extrusion by instead extruding too much outwards
|
||||
//(can happen on small cells in caves for example)
|
||||
extrusionAmount = Math.Min(extrusionAmount, edge.Length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 extrudedPoint = nonExtrudedPoint + edgeNormal * extrusionAmount;
|
||||
|
||||
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
|
||||
bool isInside = false;
|
||||
@@ -306,14 +361,29 @@ namespace Barotrauma
|
||||
}
|
||||
if (isInside) { break; }
|
||||
}
|
||||
if (isInside) { continue; }
|
||||
|
||||
if (!isInside)
|
||||
{
|
||||
edgePoints.Add(extrudedPoint);
|
||||
}
|
||||
//if adding the point would deform the edge so much that the normal of the new edge would point
|
||||
//in the opposite direction from the undeformed edge's normal, don't allow adding the point
|
||||
//(that would lead to the edge being "inside out", the wall texture and objects on the wall pointing inwards)
|
||||
bool isNormalInverted =
|
||||
Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, edgePoints.Last(), extrudedPoint)) < 0 ||
|
||||
//check that the edge at the other side of the new point doesn't get inverted either
|
||||
Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, extrudedPoint, edge.Point2)) < 0;
|
||||
if (isNormalInverted) { continue; }
|
||||
|
||||
//make sure extruding the point doesn't change the vertex order
|
||||
//(they're assumed to be sorted counter-clockwise, and if they're not, the triangles will generate incorrectly)
|
||||
bool vertexOrderChanged =
|
||||
compareCCW.Compare(edgePoints.Last(), nonExtrudedPoint) != compareCCW.Compare(edgePoints.Last(), extrudedPoint) ||
|
||||
compareCCW.Compare(nonExtrudedPoint, nextPoint) != compareCCW.Compare(extrudedPoint, nextPoint);
|
||||
if (vertexOrderChanged) { continue; }
|
||||
|
||||
edgePoints.Add(extrudedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < edgePoints.Count - 1; i++)
|
||||
{
|
||||
tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
|
||||
@@ -376,19 +446,7 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 minVert = tempVertices[0];
|
||||
Vector2 maxVert = tempVertices[0];
|
||||
foreach (var vert in tempVertices)
|
||||
{
|
||||
minVert = new Vector2(
|
||||
Math.Min(minVert.X, vert.X),
|
||||
Math.Min(minVert.Y, vert.Y));
|
||||
maxVert = new Vector2(
|
||||
Math.Max(maxVert.X, vert.X),
|
||||
Math.Max(maxVert.Y, vert.Y));
|
||||
}
|
||||
Vector2 center = (minVert + maxVert) / 2;
|
||||
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center));
|
||||
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
|
||||
|
||||
if (bodyPoints.Count < 2) { continue; }
|
||||
|
||||
@@ -411,7 +469,7 @@ namespace Barotrauma
|
||||
if (cell.CellType == CellType.Empty) { continue; }
|
||||
|
||||
cellBody.UserData = cell;
|
||||
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center));
|
||||
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
|
||||
|
||||
for (int i = 0; i < triangles.Count; i++)
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Barotrauma
|
||||
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
|
||||
{
|
||||
AddDamage(attack.StructureDamage, worldPosition);
|
||||
AddDamage(attack.LevelWallDamage, worldPosition);
|
||||
return new AttackResult(attack.StructureDamage);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,15 +5,17 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.RuinGeneration;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LevelData
|
||||
{
|
||||
[Flags]
|
||||
public enum LevelType
|
||||
{
|
||||
LocationConnection,
|
||||
Outpost
|
||||
LocationConnection = 1,
|
||||
Outpost = 2
|
||||
}
|
||||
|
||||
public readonly LevelType Type;
|
||||
@@ -47,6 +49,19 @@ namespace Barotrauma
|
||||
|
||||
public SubmarineInfo ForceWreck;
|
||||
|
||||
public RuinGenerationParams ForceRuinGenerationParams;
|
||||
|
||||
public enum ThalamusSpawn
|
||||
{
|
||||
Random,
|
||||
Forced,
|
||||
Disabled
|
||||
}
|
||||
|
||||
public static SubmarineInfo ConsoleForceWreck;
|
||||
public static SubmarineInfo ConsoleForceBeaconStation;
|
||||
public static ThalamusSpawn ForceThalamus = ThalamusSpawn.Random;
|
||||
|
||||
public bool AllowInvalidOutpost;
|
||||
|
||||
public readonly Point Size;
|
||||
@@ -73,10 +88,15 @@ namespace Barotrauma
|
||||
|
||||
public readonly Dictionary<EventSet, int> FinishedEvents = new Dictionary<EventSet, int>();
|
||||
|
||||
/// <summary>
|
||||
/// For backwards compatibility (previously "exhausting" one event set exhausted all of them (now we use <see cref="exhaustedEventSets"/> instead).
|
||||
/// </summary>
|
||||
private bool allEventsExhausted;
|
||||
|
||||
/// <summary>
|
||||
/// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
|
||||
/// </summary>
|
||||
public bool EventsExhausted { get; set; }
|
||||
private HashSet<Identifier> exhaustedEventSets = new HashSet<Identifier>();
|
||||
|
||||
/// <summary>
|
||||
/// The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the top of the level!
|
||||
@@ -207,7 +227,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
|
||||
exhaustedEventSets = element.GetAttributeIdentifierArray(nameof(exhaustedEventSets), Array.Empty<Identifier>()).ToHashSet();
|
||||
//backwards compatibility: previously we didn't track which individual event sets have been exhausted
|
||||
allEventsExhausted = element.GetAttributeBool("EventsExhausted", false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -216,10 +238,11 @@ namespace Barotrauma
|
||||
public LevelData(LocationConnection locationConnection)
|
||||
{
|
||||
Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed;
|
||||
bool connectionIsBiomeTransition = locationConnection.Locations[0].Biome.Identifier != locationConnection.Locations[1].Biome.Identifier;
|
||||
Biome = locationConnection.Biome;
|
||||
Type = LevelType.LocationConnection;
|
||||
Difficulty = locationConnection.Difficulty;
|
||||
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier);
|
||||
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier, biomeTransition: connectionIsBiomeTransition);
|
||||
|
||||
float sizeFactor = MathUtils.InverseLerp(
|
||||
MapGenerationParams.Instance.SmallLevelConnectionLength,
|
||||
@@ -264,7 +287,7 @@ namespace Barotrauma
|
||||
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
|
||||
}
|
||||
|
||||
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, bool requireOutpost = false)
|
||||
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, Identifier biomeId = default, bool requireOutpost = false, bool pvpOnly = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(seed))
|
||||
{
|
||||
@@ -273,14 +296,21 @@ namespace Barotrauma
|
||||
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
LevelType type = generationParams == null ?
|
||||
(requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) :
|
||||
generationParams.Type;
|
||||
LevelType type = generationParams?.Type ??
|
||||
(requireOutpost
|
||||
? LevelType.Outpost
|
||||
: LevelType.LocationConnection);
|
||||
|
||||
float selectedDifficulty = difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient);
|
||||
|
||||
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type, selectedDifficulty); }
|
||||
var biome =
|
||||
Biome biome = null;
|
||||
if (!biomeId.IsEmpty && biomeId != "Random")
|
||||
{
|
||||
Biome.Prefabs.TryGet(biomeId, out biome);
|
||||
}
|
||||
generationParams ??= LevelGenerationParams.GetRandom(seed, type, selectedDifficulty, pvpOnly: pvpOnly, biomeId: biomeId);
|
||||
|
||||
biome ??=
|
||||
Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ??
|
||||
Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
|
||||
@@ -306,6 +336,32 @@ namespace Barotrauma
|
||||
return levelData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the event set as "exhausted". Exhausted sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
|
||||
/// </summary>
|
||||
public void ExhaustEventSet(EventSet eventSet)
|
||||
{
|
||||
exhaustedEventSets.Add(eventSet.Identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has the event set been "exhausted"? Exhausted sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
|
||||
/// </summary>
|
||||
public bool IsEventSetExhausted(EventSet eventSet)
|
||||
{
|
||||
if (allEventsExhausted) { return true; }
|
||||
return exhaustedEventSets.Contains(eventSet.Identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all "exhausted" event sets, allowing them to appear in the level again.
|
||||
/// </summary>
|
||||
public void ResetExhaustedEventSets()
|
||||
{
|
||||
allEventsExhausted = false;
|
||||
exhaustedEventSets.Clear();
|
||||
}
|
||||
|
||||
public void ReassignGenerationParams(string seed)
|
||||
{
|
||||
GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier);
|
||||
@@ -341,7 +397,10 @@ namespace Barotrauma
|
||||
new XAttribute("size", XMLExtensions.PointToString(Size)),
|
||||
new XAttribute("generationparams", GenerationParams.Identifier),
|
||||
new XAttribute("initialdepth", InitialDepth),
|
||||
new XAttribute(nameof(EventsExhausted).ToLower(), EventsExhausted));
|
||||
new XAttribute("exhaustedeventsets", allEventsExhausted));
|
||||
|
||||
newElement.Add(
|
||||
new XAttribute(nameof(exhaustedEventSets), string.Join(',', exhaustedEventSets.Select(e => e.Value))));
|
||||
|
||||
if (HasBeaconStation)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly static PrefabCollection<LevelGenerationParams> LevelParams = new PrefabCollection<LevelGenerationParams>();
|
||||
|
||||
public LocalizedString DisplayName { get; private set; }
|
||||
public LocalizedString Description { get; private set; }
|
||||
|
||||
public string Name => Identifier.Value;
|
||||
|
||||
public Identifier OldIdentifier { get; }
|
||||
@@ -68,12 +71,18 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, "If the given level is only used in PvP modes"), Editable]
|
||||
public bool IsPvPLevel { get; set; }
|
||||
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
|
||||
public float Commonness
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, "If the level is a transition from the previous biome to this one."), Editable]
|
||||
public bool TransitionFromPreviousBiome { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be above or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
|
||||
public float MinLevelDifficulty
|
||||
@@ -227,7 +236,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. "
|
||||
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
|
||||
public float CellRoundingAmount
|
||||
{
|
||||
@@ -238,7 +247,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. "
|
||||
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
|
||||
public float CellIrregularity
|
||||
{
|
||||
@@ -497,6 +506,9 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
public int MaxRuinCount { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability of spawning a ruin in the level. If the level can have multiple ruins, the probability is evaluated separately for each."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2)]
|
||||
public float RuinSpawnProbability { get; set; }
|
||||
|
||||
// TODO: Move the wreck parameters under a separate class?
|
||||
#region Wreck parameters
|
||||
@@ -512,17 +524,20 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int MaxCorpseCount { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float HuskProbability { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float ThalamusProbability { get; set; }
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float WreckHullFloodingChance { get; set; }
|
||||
|
||||
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float WreckFloodingHullMinWaterPercentage { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float WreckFloodingHullMaxWaterPercentage { get; set; }
|
||||
#endregion
|
||||
|
||||
@@ -587,6 +602,13 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "How deep inside the walls the wall texture extends to before fading to black."), Editable(minValue: 0.0f, maxValue: 10000.0f)]
|
||||
public float WallTextureExpandInwardsAmount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Header("Colors")]
|
||||
[Serialize("27,30,36", IsPropertySaveable.Yes), Editable]
|
||||
public Color AmbientLightColor
|
||||
@@ -673,7 +695,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default)
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default, bool pvpOnly = false, bool biomeTransition = false)
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
@@ -688,7 +710,41 @@ namespace Barotrauma
|
||||
lp.Type == type &&
|
||||
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
|
||||
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
|
||||
if (biomeId.IsEmpty)
|
||||
|
||||
if (biomeTransition)
|
||||
{
|
||||
var biomeTransitionParams = matchingLevelParams.Where(lp =>
|
||||
lp.TransitionFromPreviousBiome && lp.AllowedBiomeIdentifiers.Contains(biomeId));
|
||||
|
||||
if (biomeTransitionParams.Any())
|
||||
{
|
||||
return ToolBox.SelectWeightedRandom(biomeTransitionParams, p => p.Commonness, Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => !lp.TransitionFromPreviousBiome);
|
||||
}
|
||||
|
||||
if (pvpOnly)
|
||||
{
|
||||
var pvpOnlyLevels = matchingLevelParams.Where(static lp => lp.IsPvPLevel);
|
||||
|
||||
if (pvpOnlyLevels.Any())
|
||||
{
|
||||
matchingLevelParams = pvpOnlyLevels;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning("No PvP specific level generation presets found - using all level generation presets instead.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingLevelParams = matchingLevelParams.Where(static lp => !lp.IsPvPLevel);
|
||||
}
|
||||
|
||||
if (biomeId.IsEmpty || biomeId == "Random")
|
||||
{
|
||||
//we don't want end levels when generating a completely random level (e.g. in mission mode)
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome));
|
||||
@@ -746,6 +802,20 @@ namespace Barotrauma
|
||||
allowedBiomeIdentifiers.Remove("any".ToIdentifier());
|
||||
AllowedBiomeIdentifiers = allowedBiomeIdentifiers.ToImmutableHashSet();
|
||||
|
||||
DisplayName = TextManager.Get($"levelname.{Identifier}");
|
||||
Description = TextManager.Get($"leveldescription.{Identifier}");
|
||||
|
||||
var nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
|
||||
var descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", Identifier.Empty);
|
||||
if (!nameIdentifier.IsEmpty)
|
||||
{
|
||||
DisplayName = TextManager.Get(nameIdentifier);
|
||||
}
|
||||
if (!descriptionIdentifier.IsEmpty)
|
||||
{
|
||||
Description = TextManager.Get(descriptionIdentifier);
|
||||
}
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
|
||||
+21
-10
@@ -20,7 +20,7 @@ namespace Barotrauma
|
||||
private List<LevelObject> updateableObjects;
|
||||
private List<LevelObject>[,] objectGrid;
|
||||
|
||||
const float ParallaxStrength = 0.0001f;
|
||||
public const float ParallaxStrength = 0.0001f;
|
||||
|
||||
public float GlobalForceDecreaseTimer
|
||||
{
|
||||
@@ -178,12 +178,21 @@ namespace Barotrauma
|
||||
{
|
||||
float minDistance = level.Size.X * 0.2f;
|
||||
|
||||
bool allowAtStart = prefab.AllowAtStart;
|
||||
bool allowAtEnd = prefab.AllowAtEnd;
|
||||
if (GameMain.GameSession?.GameMode is PvPMode)
|
||||
{
|
||||
//in PvP mode, the object must be allowed at both the start and end to be placed at either end
|
||||
//since the 2nd team starts at the end of the level, it'd be unfair to allow e.g. ballast flora to spawn at the end of the level but not the start
|
||||
allowAtEnd = allowAtStart = allowAtEnd && allowAtStart;
|
||||
}
|
||||
|
||||
suitableSpawnPositions.Add(prefab,
|
||||
availableSpawnPositions.Where(sp =>
|
||||
sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) &&
|
||||
sp.Length >= prefab.MinSurfaceWidth &&
|
||||
(prefab.AllowAtStart || !level.IsCloseToStart(sp.GraphEdge.Center, minDistance)) &&
|
||||
(prefab.AllowAtEnd || !level.IsCloseToEnd(sp.GraphEdge.Center, minDistance)) &&
|
||||
(allowAtStart || !level.IsCloseToStart(sp.GraphEdge.Center, minDistance)) &&
|
||||
(allowAtEnd || !level.IsCloseToEnd(sp.GraphEdge.Center, minDistance)) &&
|
||||
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
|
||||
|
||||
spawnPositionWeights.Add(prefab,
|
||||
@@ -407,11 +416,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z * ParallaxStrength;
|
||||
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z * ParallaxStrength;
|
||||
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z;
|
||||
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z;
|
||||
|
||||
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z * ParallaxStrength - level.BottomPos;
|
||||
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z * ParallaxStrength - level.BottomPos;
|
||||
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos;
|
||||
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos;
|
||||
|
||||
if (newObject.Triggers != null)
|
||||
{
|
||||
@@ -446,6 +455,8 @@ namespace Barotrauma
|
||||
#endif
|
||||
objects.Add(newObject);
|
||||
if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); }
|
||||
//add some variance to the Z position to prevent z-fighting
|
||||
//(based on the x and y position of the object, scaled to be visually insignificant)
|
||||
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
|
||||
|
||||
int xStart = (int)Math.Floor(minX / GridSize);
|
||||
@@ -557,7 +568,7 @@ namespace Barotrauma
|
||||
return availableSpawnPositions;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
public void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
GlobalForceDecreaseTimer += deltaTime;
|
||||
if (GlobalForceDecreaseTimer > 1000000.0f)
|
||||
@@ -603,10 +614,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
UpdateProjSpecific(deltaTime, cam);
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
partial void UpdateProjSpecific(float deltaTime, Camera cam);
|
||||
|
||||
private void OnObjectTriggered(LevelObject triggeredObject, LevelTrigger trigger, Entity triggerer)
|
||||
{
|
||||
|
||||
+12
-5
@@ -1,7 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -136,6 +135,14 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
[Serialize(3000.0f, IsPropertySaveable.Yes, description: "Objects fade out to the background color of the level the further they are from the camera. This value is the depth at which the object becomes \"maximally\" faded out."), Editable]
|
||||
public float FadeOutDepth
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f),
|
||||
Serialize(0.0f, IsPropertySaveable.Yes, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")]
|
||||
/// <summary>
|
||||
@@ -339,11 +346,11 @@ namespace Barotrauma
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
//use the maximum width of the sprite as the minimum surface width if no value is given
|
||||
if (element != null && !element.Attributes("minsurfacewidth").Any())
|
||||
//use (a bit less than) the maximum width of the sprite as the minimum surface width if no value is given
|
||||
if (element != null && element.GetAttribute("minsurfacewidth") == null)
|
||||
{
|
||||
if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize;
|
||||
if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize);
|
||||
if (Sprites.Any()) { MinSurfaceWidth = Sprites[0].size.X * MaxSize * 0.8f; }
|
||||
if (DeformableSprite != null) { MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize * 0.8f); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -55,6 +57,8 @@ namespace Barotrauma
|
||||
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
|
||||
|
||||
private readonly TriggererType triggeredBy;
|
||||
private readonly Identifier triggerSpeciesOrGroup;
|
||||
private readonly PropertyConditional.LogicalComparison conditionals;
|
||||
|
||||
private readonly float randomTriggerInterval;
|
||||
private readonly float randomTriggerProbability;
|
||||
@@ -255,7 +259,16 @@ namespace Barotrauma
|
||||
string triggeredByStr = element.GetAttributeString("triggeredby", "Character");
|
||||
if (!Enum.TryParse(triggeredByStr, out triggeredBy))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
|
||||
Identifier speciesOrGroup = triggeredByStr.ToIdentifier();
|
||||
if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup)))
|
||||
{
|
||||
triggerSpeciesOrGroup = speciesOrGroup;
|
||||
triggeredBy = TriggererType.Character;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
|
||||
}
|
||||
}
|
||||
if (PhysicsBody != null)
|
||||
{
|
||||
@@ -293,6 +306,8 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
conditionals = PropertyConditional.LoadConditionals(element);
|
||||
|
||||
forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval);
|
||||
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
|
||||
@@ -341,7 +356,7 @@ namespace Barotrauma
|
||||
{
|
||||
Entity entity = GetEntity(fixtureB);
|
||||
if (entity == null) { return false; }
|
||||
if (!IsTriggeredByEntity(entity, triggeredBy, mustBeOutside: true)) { return false; }
|
||||
if (!IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup: triggerSpeciesOrGroup, conditionals: conditionals, mustBeOutside: true)) { return false; }
|
||||
if (!triggerers.Contains(entity))
|
||||
{
|
||||
if (!IsTriggered)
|
||||
@@ -354,12 +369,22 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsTriggeredByEntity(Entity entity, TriggererType triggeredBy, bool mustBeOutside = false, (bool mustBe, Submarine sub) mustBeOnSpecificSub = default)
|
||||
public static bool IsTriggeredByEntity(
|
||||
Entity entity,
|
||||
TriggererType triggeredBy,
|
||||
Identifier triggerSpeciesOrGroup,
|
||||
PropertyConditional.LogicalComparison conditionals,
|
||||
(bool mustBe, Submarine sub) mustBeOnSpecificSub = default,
|
||||
bool mustBeOutside = false)
|
||||
{
|
||||
if (entity is Character character)
|
||||
{
|
||||
if (mustBeOutside && character.CurrentHull != null) { return false; }
|
||||
if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; }
|
||||
if (!triggerSpeciesOrGroup.IsEmpty)
|
||||
{
|
||||
if (character.SpeciesName != triggerSpeciesOrGroup && character.Group != triggerSpeciesOrGroup) { return false; }
|
||||
}
|
||||
if (character.IsHuman)
|
||||
{
|
||||
if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; }
|
||||
@@ -379,6 +404,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; }
|
||||
}
|
||||
if (conditionals != null && entity is ISerializableEntity serializableEntity)
|
||||
{
|
||||
if (!PropertyConditional.CheckConditionals(serializableEntity, conditionals.Conditionals, conditionals.LogicalOperator)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -497,7 +526,7 @@ namespace Barotrauma
|
||||
triggeredTimer = stayTriggeredDelay;
|
||||
if (!wasAlreadyTriggered)
|
||||
{
|
||||
if (!IsTriggeredByEntity(triggerer, triggeredBy, mustBeOutside: true)) { return; }
|
||||
if (!IsTriggeredByEntity(triggerer, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOutside: true)) { return; }
|
||||
if (!triggerers.Contains(triggerer))
|
||||
{
|
||||
if (!IsTriggered)
|
||||
@@ -580,6 +609,21 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (PhysicsBody != null)
|
||||
{
|
||||
if (currentForceFluctuation <= 0.0f && statusEffects.None() && attacks.None())
|
||||
{
|
||||
//no force atm, and no status effects or attacks the trigger could apply
|
||||
// -> we can disable the collider and get a minor physics performance improvement
|
||||
PhysicsBody.Enabled = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
PhysicsBody.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Entity triggerer in triggerers)
|
||||
{
|
||||
if (triggerer.Removed) { continue; }
|
||||
@@ -652,13 +696,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyStatusEffects(List<StatusEffect> statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List<ISerializableEntity> targets)
|
||||
public static void ApplyStatusEffects(List<StatusEffect> statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List<ISerializableEntity> targets, Item targetItem = null)
|
||||
{
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
if (effect.type == ActionType.OnBroken) { return; }
|
||||
Vector2? position = null;
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = worldPosition; }
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
position = worldPosition;
|
||||
if (targetItem != null)
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, triggerer, targetItem.AllPropertyObjects, position);
|
||||
}
|
||||
}
|
||||
if (triggerer is Character character)
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, triggerer, character, position);
|
||||
@@ -728,6 +779,8 @@ namespace Barotrauma
|
||||
if (distFactor < 0.0f) return;
|
||||
}
|
||||
|
||||
if (MathUtils.NearlyEqual(currentForceFluctuation, 0.0f)) { return; }
|
||||
|
||||
switch (ForceMode)
|
||||
{
|
||||
case TriggerForceMode.Force:
|
||||
|
||||
@@ -3,6 +3,7 @@ using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -62,6 +63,8 @@ namespace Barotrauma
|
||||
private int nameFormatIndex;
|
||||
private Identifier nameIdentifier;
|
||||
|
||||
public int NameFormatIndex => nameFormatIndex;
|
||||
|
||||
/// <summary>
|
||||
/// For backwards compatibility: a non-localizable name from the old text files.
|
||||
/// </summary>
|
||||
@@ -73,6 +76,17 @@ namespace Barotrauma
|
||||
|
||||
public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// How many "world steps" (<see cref="Map.ProgressWorld(CampaignMode)"/> must pass for the stores to be reset in the location?
|
||||
/// Mainly an optimization
|
||||
/// </summary>
|
||||
public const int ClearStoresDelay = 10;
|
||||
|
||||
/// <summary>
|
||||
/// How many "world steps" (<see cref="Map.ProgressWorld(CampaignMode)"/> have passed since this location was last visited?
|
||||
/// </summary>
|
||||
public int WorldStepsSinceVisited;
|
||||
|
||||
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
|
||||
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
|
||||
public int LocationTypeChangeCooldown;
|
||||
@@ -80,7 +94,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Is some mission blocking this location from changing its type, or have location type changes been forcibly disabled on the location?
|
||||
/// </summary>
|
||||
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges);
|
||||
public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => !m.Completed && m.Prefab.BlockLocationTypeChanges);
|
||||
|
||||
public bool DisallowLocationTypeChanges;
|
||||
|
||||
@@ -100,6 +114,11 @@ namespace Barotrauma
|
||||
|
||||
public Faction SecondaryFaction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Not used by the vanilla game. Can be used by code mods to change the color of the location icon on the campaign map.
|
||||
/// </summary>
|
||||
public Color? OverrideIconColor;
|
||||
|
||||
public Reputation Reputation => Faction?.Reputation;
|
||||
|
||||
public bool IsFactionHostile => Faction?.Reputation.NormalizedValue < Reputation.HostileThreshold;
|
||||
@@ -121,8 +140,17 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public int PriceModifier { get; set; }
|
||||
public Location Location { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum effect positive reputation can have on store prices (e.g. 0.5 = 50% discount with max reputation).
|
||||
/// </summary>
|
||||
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation).
|
||||
/// </summary>
|
||||
private float MinReputationModifier => Location.StoreMinReputationModifier;
|
||||
|
||||
private StoreInfo(Location location)
|
||||
{
|
||||
Location = location;
|
||||
@@ -186,9 +214,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
|
||||
public static PurchasedItem CreateInitialStockItem(Location location, ItemPrefab itemPrefab, PriceInfo priceInfo)
|
||||
{
|
||||
int quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1);
|
||||
//simulate stores stocking up if the location hasn't been visited in a while
|
||||
quantity = Math.Min(quantity + location.WorldStepsSinceVisited, priceInfo.MaxAvailableAmount);
|
||||
return new PurchasedItem(itemPrefab, quantity, buyer: null);
|
||||
}
|
||||
|
||||
@@ -198,7 +228,7 @@ namespace Barotrauma
|
||||
foreach (var prefab in ItemPrefab.Prefabs)
|
||||
{
|
||||
if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; }
|
||||
stock.Add(CreateInitialStockItem(prefab, priceInfo));
|
||||
stock.Add(CreateInitialStockItem(Location, prefab, priceInfo));
|
||||
}
|
||||
return stock;
|
||||
}
|
||||
@@ -251,6 +281,7 @@ namespace Barotrauma
|
||||
}
|
||||
availableStock.Add(stockItem.ItemPrefab, weight);
|
||||
}
|
||||
|
||||
DailySpecials.Clear();
|
||||
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
|
||||
for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
|
||||
@@ -261,14 +292,16 @@ namespace Barotrauma
|
||||
DailySpecials.Add(item);
|
||||
availableStock.Remove(item);
|
||||
}
|
||||
|
||||
RequestedGoods.Clear();
|
||||
for (int i = 0; i < Location.RequestedGoodsCount; i++)
|
||||
{
|
||||
var item = ItemPrefab.Prefabs.GetRandom(p =>
|
||||
p.CanBeSold && !RequestedGoods.Contains(p) &&
|
||||
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
|
||||
if (item == null) { break; }
|
||||
RequestedGoods.Add(item);
|
||||
|
||||
var selectedPrefab = ItemPrefab.Prefabs.GetRandom(prefab =>
|
||||
prefab.CanBeSold && !RequestedGoods.Contains(prefab) &&
|
||||
prefab.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
|
||||
if (selectedPrefab == null) { break; }
|
||||
RequestedGoods.Add(selectedPrefab);
|
||||
}
|
||||
Location.StepsSinceSpecialsUpdated = 0;
|
||||
}
|
||||
@@ -284,7 +317,7 @@ namespace Barotrauma
|
||||
{
|
||||
priceInfo ??= item?.GetPriceInfo(this);
|
||||
if (priceInfo == null) { return 0; }
|
||||
float price = priceInfo.Price;
|
||||
float price = Location.StoreBuyPriceModifier * priceInfo.Price;
|
||||
// Adjust by random price modifier
|
||||
price = (100 + PriceModifier) / 100.0f * price;
|
||||
price *= priceInfo.BuyingPriceMultiplier;
|
||||
@@ -293,6 +326,11 @@ namespace Barotrauma
|
||||
{
|
||||
price = Location.DailySpecialPriceModifier * price;
|
||||
}
|
||||
// Adjust by requested good status (to avoid the store selling items that it requests potentially for less than it pays for them)
|
||||
if (RequestedGoods.Contains(item))
|
||||
{
|
||||
price = Location.RequestGoodBuyPriceModifier * price;
|
||||
}
|
||||
// Adjust by current reputation
|
||||
price *= GetReputationModifier(true);
|
||||
|
||||
@@ -320,7 +358,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="considerRequestedGoods">If false, the price won't be affected by <see cref="Barotrauma.Location.RequestGoodSellPriceModifier"/></param>
|
||||
public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
|
||||
{
|
||||
priceInfo ??= item?.GetPriceInfo(this);
|
||||
@@ -331,7 +369,7 @@ namespace Barotrauma
|
||||
// Adjust by requested good status
|
||||
if (considerRequestedGoods && RequestedGoods.Contains(item))
|
||||
{
|
||||
price = Location.RequestGoodPriceModifier * price;
|
||||
price = Location.RequestGoodSellPriceModifier * price;
|
||||
}
|
||||
// Adjust by location reputation
|
||||
price *= GetReputationModifier(false);
|
||||
@@ -340,7 +378,7 @@ namespace Barotrauma
|
||||
if (characters.Any())
|
||||
{
|
||||
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
|
||||
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
|
||||
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag)));
|
||||
}
|
||||
|
||||
// Price should never go below 1 mk
|
||||
@@ -370,7 +408,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
return MathHelper.Lerp(1.0f, 1.0f + MinReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -381,7 +419,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
return MathHelper.Lerp(1.0f, 1.0f - MinReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,12 +430,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<Identifier, StoreInfo> Stores { get; set; }
|
||||
public Dictionary<Identifier, StoreInfo> Stores { get; private set; }
|
||||
|
||||
private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
|
||||
private float StoreMinReputationModifier => Type.StoreMinReputationModifier;
|
||||
private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
|
||||
private float StoreBuyPriceModifier => Type.StoreBuyPriceModifier;
|
||||
private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
|
||||
private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
|
||||
private float RequestGoodBuyPriceModifier => Type.RequestGoodBuyPriceModifier;
|
||||
private float RequestGoodSellPriceModifier => Type.RequestGoodPriceModifier;
|
||||
public int StoreInitialBalance => Type.StoreInitialBalance;
|
||||
private int StorePriceModifierRange => Type.StorePriceModifierRange;
|
||||
|
||||
@@ -575,24 +616,11 @@ namespace Barotrauma
|
||||
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
|
||||
}
|
||||
|
||||
LoadChangingProperties(element, campaign);
|
||||
|
||||
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
|
||||
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
|
||||
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
|
||||
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
|
||||
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
|
||||
StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0);
|
||||
|
||||
var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
if (!factionIdentifier.IsEmpty)
|
||||
{
|
||||
Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
|
||||
}
|
||||
var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
|
||||
if (!secondaryFactionIdentifier.IsEmpty)
|
||||
{
|
||||
SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
|
||||
}
|
||||
Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty);
|
||||
if (biomeId != Identifier.Empty)
|
||||
{
|
||||
@@ -684,6 +712,24 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the values of properties that can change mid-campaign and need to be updated when a client receives a new campaign save from the server
|
||||
/// </summary>
|
||||
public void LoadChangingProperties(XElement element, CampaignMode campaign)
|
||||
{
|
||||
PriceMultiplier = element.GetAttributeFloat(nameof(PriceMultiplier), 1.0f);
|
||||
MechanicalPriceMultiplier = element.GetAttributeFloat(nameof(MechanicalPriceMultiplier), 1.0f);
|
||||
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
|
||||
StepsSinceSpecialsUpdated = element.GetAttributeInt(nameof(StepsSinceSpecialsUpdated), 0);
|
||||
WorldStepsSinceVisited = element.GetAttributeInt(nameof(WorldStepsSinceVisited), 0);
|
||||
|
||||
var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
|
||||
|
||||
var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
|
||||
SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
|
||||
}
|
||||
|
||||
public void LoadLocationTypeChange(XElement locationElement)
|
||||
{
|
||||
TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
|
||||
@@ -780,13 +826,20 @@ namespace Barotrauma
|
||||
if (Type.Faction == Identifier.Empty) { Faction = null; }
|
||||
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
|
||||
}
|
||||
|
||||
UnlockInitialMissions(Rand.RandSync.Unsynced);
|
||||
|
||||
if (!IsCriticallyRadiated())
|
||||
{
|
||||
UnlockInitialMissions(Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
if (createStores)
|
||||
{
|
||||
CreateStores(force: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearStores();
|
||||
}
|
||||
}
|
||||
|
||||
public void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
|
||||
@@ -1076,12 +1129,17 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public LocationType GetLocationType()
|
||||
public LocationType GetLocationTypeToDisplay(out Identifier overrideDescriptionIdentifier)
|
||||
{
|
||||
overrideDescriptionIdentifier = Identifier.Empty;
|
||||
if (IsCriticallyRadiated() && !Type.ReplaceInRadiation.IsEmpty)
|
||||
{
|
||||
if (LocationType.Prefabs.TryGet(Type.ReplaceInRadiation, out LocationType newLocationType))
|
||||
{
|
||||
if (!newLocationType.DescriptionInRadiation.IsEmpty)
|
||||
{
|
||||
overrideDescriptionIdentifier = newLocationType.DescriptionInRadiation;
|
||||
}
|
||||
return newLocationType;
|
||||
}
|
||||
else
|
||||
@@ -1092,6 +1150,11 @@ namespace Barotrauma
|
||||
return Type;
|
||||
}
|
||||
|
||||
public LocationType GetLocationTypeToDisplay()
|
||||
{
|
||||
return GetLocationTypeToDisplay(out _);
|
||||
}
|
||||
|
||||
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
|
||||
@@ -1177,9 +1240,22 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
|
||||
public static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
|
||||
{
|
||||
if (type?.NameFormats == null || !type.NameFormats.Any())
|
||||
if (LocationType.Prefabs.TryGet(locationTypeIdentifier, out LocationType locationType))
|
||||
{
|
||||
return GetName(locationType, nameFormatIndex, nameId);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find the location type {locationTypeIdentifier}.\n" + Environment.StackTrace.CleanUpPath());
|
||||
return new RawLString(nameId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
|
||||
{
|
||||
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
|
||||
{
|
||||
return TextManager.Get(nameId);
|
||||
}
|
||||
@@ -1197,8 +1273,11 @@ namespace Barotrauma
|
||||
{
|
||||
UpdateStoreIdentifiers();
|
||||
Stores?.Clear();
|
||||
|
||||
bool hasStores = false;
|
||||
foreach (var storeElement in locationElement.GetChildElements("store"))
|
||||
{
|
||||
hasStores = true;
|
||||
Stores ??= new Dictionary<Identifier, StoreInfo>();
|
||||
var identifier = storeElement.GetAttributeIdentifier("identifier", "");
|
||||
if (identifier.IsEmpty)
|
||||
@@ -1229,13 +1308,16 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
// Backwards compatibility: create new stores for any identifiers not present in the save data
|
||||
foreach (var id in StoreIdentifiers)
|
||||
if (hasStores)
|
||||
{
|
||||
AddNewStore(id);
|
||||
foreach (var id in StoreIdentifiers)
|
||||
{
|
||||
AddNewStore(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this);
|
||||
public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.DepthInRadiation(this) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
|
||||
@@ -1305,6 +1387,9 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create stores and stocks for the location. If the location already has stores, the method will not do anything unless the "force" argument is true. />
|
||||
/// </summary>
|
||||
/// <param name="force">If true, the stores will be recreated if they already exists.</param>
|
||||
public void CreateStores(bool force = false)
|
||||
{
|
||||
@@ -1358,13 +1443,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateStores()
|
||||
public void UpdateStores(bool createStoresIfNotCreated = true)
|
||||
{
|
||||
// In multiplayer, stores should be updated by the server and loaded from save data by clients
|
||||
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
if (Stores == null)
|
||||
{
|
||||
CreateStores();
|
||||
if (createStoresIfNotCreated) { CreateStores(); }
|
||||
return;
|
||||
}
|
||||
var storesToRemove = new HashSet<Identifier>();
|
||||
@@ -1384,13 +1469,13 @@ namespace Barotrauma
|
||||
|
||||
foreach (var itemPrefab in ItemPrefab.Prefabs)
|
||||
{
|
||||
var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab);
|
||||
var existingStock = stock.FirstOrDefault(s => s.ItemPrefabIdentifier == itemPrefab.Identifier);
|
||||
if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo))
|
||||
{
|
||||
if (existingStock == null)
|
||||
{
|
||||
//can be bought from the location, but not in stock - some new item added by an update or mod?
|
||||
stock.Add(StoreInfo.CreateInitialStockItem(itemPrefab, priceInfo));
|
||||
stock.Add(StoreInfo.CreateInitialStockItem(this, itemPrefab, priceInfo));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1470,6 +1555,16 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all information about stores from the location (can be used to avoid storing unnecessary
|
||||
/// store info about locations that haven't been visited in a long time). The stores are automatically
|
||||
/// recreated when the player enters the location.
|
||||
/// </summary>
|
||||
public void ClearStores()
|
||||
{
|
||||
Stores = null;
|
||||
}
|
||||
|
||||
public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
|
||||
{
|
||||
if (items == null) { return; }
|
||||
@@ -1522,7 +1617,7 @@ namespace Barotrauma
|
||||
ChangeType(campaign, OriginalType);
|
||||
PendingLocationTypeChange = null;
|
||||
}
|
||||
CreateStores(force: true);
|
||||
ClearStores();
|
||||
ClearMissions();
|
||||
LevelData?.EventHistory?.Clear();
|
||||
UnlockInitialMissions();
|
||||
@@ -1543,7 +1638,8 @@ namespace Barotrauma
|
||||
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
|
||||
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
|
||||
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
|
||||
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
|
||||
new XAttribute(nameof(StepsSinceSpecialsUpdated), StepsSinceSpecialsUpdated),
|
||||
new XAttribute(nameof(WorldStepsSinceVisited), WorldStepsSinceVisited));
|
||||
|
||||
if (!rawName.IsNullOrEmpty())
|
||||
{
|
||||
@@ -1700,3 +1796,4 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@ namespace Barotrauma
|
||||
|
||||
public readonly CharacterTeamType OutpostTeam;
|
||||
|
||||
/// <summary>
|
||||
/// Is this location type considered valid for e.g. events and missions that are should be available in "any outpost"
|
||||
/// </summary>
|
||||
public bool IsAnyOutpost;
|
||||
|
||||
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
|
||||
|
||||
public readonly ImmutableArray<Identifier> MissionIdentifiers;
|
||||
@@ -86,6 +91,8 @@ namespace Barotrauma
|
||||
|
||||
public Identifier ReplaceInRadiation { get; }
|
||||
|
||||
public Identifier DescriptionInRadiation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, forces the location to be assigned to this faction. Set to "None" if you don't want the location to be assigned to any faction.
|
||||
/// </summary>
|
||||
@@ -113,9 +120,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public float StoreMaxReputationModifier { get; } = 0.1f;
|
||||
public float StoreMinReputationModifier { get; } = 1.0f;
|
||||
public float StoreSellPriceModifier { get; } = 0.3f;
|
||||
public float StoreBuyPriceModifier { get; } = 1f;
|
||||
public float DailySpecialPriceModifier { get; } = 0.5f;
|
||||
public float RequestGoodPriceModifier { get; } = 2f;
|
||||
public float RequestGoodBuyPriceModifier { get; } = 5f;
|
||||
public int StoreInitialBalance { get; } = 5000;
|
||||
/// <summary>
|
||||
/// In percentages
|
||||
@@ -155,11 +165,14 @@ namespace Barotrauma
|
||||
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", Array.Empty<string>()).ToList();
|
||||
|
||||
ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty);
|
||||
DescriptionInRadiation = element.GetAttributeIdentifier(nameof(DescriptionInRadiation), "locationdescription.abandonedirradiated");
|
||||
|
||||
forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty);
|
||||
|
||||
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
|
||||
|
||||
IsAnyOutpost = element.GetAttributeBool(nameof(IsAnyOutpost), def: HasOutpost);
|
||||
|
||||
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
|
||||
Enum.TryParse(teamStr, out OutpostTeam);
|
||||
|
||||
@@ -179,7 +192,7 @@ namespace Barotrauma
|
||||
try
|
||||
{
|
||||
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
|
||||
names.AddRange(File.ReadAllLines(path.Value).ToList());
|
||||
names.AddRange(File.ReadAllLines(path.Value, catchUnauthorizedAccessExceptions: false).ToList());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -257,9 +270,12 @@ namespace Barotrauma
|
||||
break;
|
||||
case "store":
|
||||
StoreMaxReputationModifier = subElement.GetAttributeFloat("maxreputationmodifier", StoreMaxReputationModifier);
|
||||
StoreBuyPriceModifier = subElement.GetAttributeFloat("buypricemodifier", StoreBuyPriceModifier);
|
||||
StoreMinReputationModifier = subElement.GetAttributeFloat("minreputationmodifier", StoreMaxReputationModifier);
|
||||
StoreSellPriceModifier = subElement.GetAttributeFloat("sellpricemodifier", StoreSellPriceModifier);
|
||||
DailySpecialPriceModifier = subElement.GetAttributeFloat("dailyspecialpricemodifier", DailySpecialPriceModifier);
|
||||
RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier);
|
||||
RequestGoodBuyPriceModifier = subElement.GetAttributeFloat("requestgoodbuypricemodifier", RequestGoodBuyPriceModifier);
|
||||
StoreInitialBalance = subElement.GetAttributeInt("initialbalance", StoreInitialBalance);
|
||||
StorePriceModifierRange = subElement.GetAttributeInt("pricemodifierrange", StorePriceModifierRange);
|
||||
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
|
||||
|
||||
@@ -328,7 +328,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (LocationType.Prefabs.TryGet("outpost".ToIdentifier(), out LocationType outpostLocationType))
|
||||
{
|
||||
otherLocation.ChangeType(campaign, outpostLocationType);
|
||||
otherLocation.ChangeType(campaign, outpostLocationType, createStores: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,17 +449,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
LocationType forceLocationType = null;
|
||||
if (!possibleStartOutpostCreated)
|
||||
{
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
float threshold = zoneWidth * 0.1f;
|
||||
if (position.X < threshold)
|
||||
{
|
||||
LocationType.Prefabs.TryGet("outpost", out forceLocationType);
|
||||
possibleStartOutpostCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (forceLocationType == null)
|
||||
{
|
||||
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
|
||||
@@ -604,6 +593,10 @@ namespace Barotrauma
|
||||
{
|
||||
connectionsBetweenZones[zone1].Add(connection);
|
||||
}
|
||||
if (connectionsBetweenZones[zone1].None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Potential error during map generation: no connections between zones {zone1} and {zone2} found. Traversing through to the end of the map may be impossible.");
|
||||
}
|
||||
}
|
||||
|
||||
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
|
||||
@@ -673,8 +666,9 @@ namespace Barotrauma
|
||||
connection.Locations[0] :
|
||||
connection.Locations[1];
|
||||
|
||||
//if there's only one connection (= the connection between biomes), create a new connection to the closest location to the right
|
||||
if (rightMostLocation.Connections.Count == 1)
|
||||
//if all of the other connected locations are to the left (= if there's no path forwards from the outpost),
|
||||
//create a new connection to the closest location to the right
|
||||
if (rightMostLocation.Connections.All(c => c.OtherLocation(rightMostLocation).MapPosition.X < rightMostLocation.MapPosition.X))
|
||||
{
|
||||
Location closestLocation = null;
|
||||
float closestDist = float.PositiveInfinity;
|
||||
@@ -714,6 +708,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
//ensure there's an outpost (a valid starting location) at the very left side of the map
|
||||
Location startLocation = Locations.MinBy(l => l.MapPosition.X);
|
||||
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
|
||||
{
|
||||
startLocation.ChangeType(campaign, startLocationType, createStores: false);
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
@@ -731,7 +732,6 @@ namespace Barotrauma
|
||||
location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
}
|
||||
location.CreateStores(force: true);
|
||||
}
|
||||
|
||||
foreach (LocationConnection connection in Connections)
|
||||
@@ -763,7 +763,7 @@ namespace Barotrauma
|
||||
|
||||
partial void GenerateLocationConnectionVisuals(LocationConnection connection);
|
||||
|
||||
private int GetZoneIndex(float xPos)
|
||||
public int GetZoneIndex(float xPos)
|
||||
{
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
|
||||
@@ -1019,11 +1019,11 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
CurrentLocation = SelectedLocation;
|
||||
CurrentLocation.CreateStores();
|
||||
Discover(CurrentLocation);
|
||||
Visit(CurrentLocation);
|
||||
SelectedLocation = null;
|
||||
|
||||
CurrentLocation.CreateStores();
|
||||
OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
|
||||
|
||||
if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
|
||||
@@ -1211,7 +1211,19 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData.EventsExhausted = false;
|
||||
if (location.Visited)
|
||||
{
|
||||
location.WorldStepsSinceVisited++;
|
||||
if (location.WorldStepsSinceVisited > Location.ClearStoresDelay)
|
||||
{
|
||||
location.ClearStores();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
location.ClearStores();
|
||||
}
|
||||
location.LevelData.ResetExhaustedEventSets();
|
||||
if (location.Discovered)
|
||||
{
|
||||
if (furthestDiscoveredLocation == null ||
|
||||
@@ -1223,7 +1235,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
connection.LevelData.EventsExhausted = false;
|
||||
connection.LevelData.ResetExhaustedEventSets();
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
@@ -1233,12 +1245,27 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
|
||||
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
|
||||
|
||||
if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
|
||||
bool shouldUpdateStores = location.Discovered;
|
||||
//don't allow the type of the current location or the destination to change (it'd be weird to arrive at a different type of location than the one you were travelling to)
|
||||
//biome gates should also remain unchanged
|
||||
bool shouldProcessLocationTypeChanges = location != CurrentLocation && location != SelectedLocation && !location.IsGateBetweenBiomes;
|
||||
if (shouldProcessLocationTypeChanges &&
|
||||
ProgressLocationTypeChanges(campaign, location))
|
||||
{
|
||||
location.UpdateStores();
|
||||
//don't update stores if the location type changed (that recreates the stores anyway)
|
||||
shouldUpdateStores = false;
|
||||
}
|
||||
|
||||
if (shouldUpdateStores)
|
||||
{
|
||||
location.UpdateStores(createStoresIfNotCreated: false);
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentLocation != null)
|
||||
{
|
||||
CurrentLocation.UpdateStores(createStoresIfNotCreated: true);
|
||||
CurrentLocation.WorldStepsSinceVisited = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1337,7 +1364,7 @@ namespace Barotrauma
|
||||
{
|
||||
location.ClearMissions();
|
||||
}
|
||||
location.ChangeType(campaign, newType);
|
||||
location.ChangeType(campaign, newType, createStores: false);
|
||||
ChangeLocationTypeProjSpecific(location, prevName, change);
|
||||
foreach (var requirement in change.Requirements)
|
||||
{
|
||||
@@ -1408,9 +1435,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void Visit(Location location)
|
||||
public void Visit(Location location, bool resetTimeSinceVisited = true)
|
||||
{
|
||||
if (location is null) { return; }
|
||||
if (resetTimeSinceVisited)
|
||||
{
|
||||
location.WorldStepsSinceVisited = 0;
|
||||
}
|
||||
if (locationsVisited.Contains(location)) { return; }
|
||||
locationsVisited.Add(location);
|
||||
RemoveFogOfWarProjSpecific(location);
|
||||
@@ -1509,12 +1540,14 @@ namespace Barotrauma
|
||||
}
|
||||
location.LoadLocationTypeChange(subElement);
|
||||
|
||||
location.LoadChangingProperties(subElement, campaign);
|
||||
|
||||
// Backwards compatibility: if the discovery status is defined in the location element,
|
||||
// the game was saved using when the discovery order still wasn't being tracked
|
||||
if (subElement.GetAttributeBool("discovered", false))
|
||||
{
|
||||
Discover(location);
|
||||
Visit(location);
|
||||
Visit(location, resetTimeSinceVisited: false);
|
||||
trackedLocationDiscoveryAndVisitOrder = false;
|
||||
}
|
||||
|
||||
@@ -1524,9 +1557,6 @@ namespace Barotrauma
|
||||
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
|
||||
location.ChangeType(campaign, newLocationType);
|
||||
|
||||
var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
|
||||
|
||||
if (showNotifications && prevLocationType != location.Type)
|
||||
{
|
||||
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
|
||||
@@ -1537,9 +1567,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
|
||||
location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
|
||||
|
||||
location.LoadStores(subElement);
|
||||
location.LoadMissions(subElement);
|
||||
|
||||
@@ -1562,16 +1589,24 @@ namespace Barotrauma
|
||||
break;
|
||||
case "discovered":
|
||||
bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false);
|
||||
int[] discoveredIndices = subElement.GetAttributeIntArray("indices", Array.Empty<int>());
|
||||
foreach (int discoveredIndex in discoveredIndices)
|
||||
{
|
||||
Discover(Locations[discoveredIndex]);
|
||||
}
|
||||
//backwards compatibility
|
||||
foreach (var childElement in subElement.GetChildElements("location"))
|
||||
{
|
||||
if (GetLocation(childElement) is Location l)
|
||||
{
|
||||
Discover(l);
|
||||
//even older backwards compatibility: previously we didn't track whether you've "visited" empty locations,
|
||||
//nor the order in which locations are visited - we need to handle that here
|
||||
if (!trackedVisitedEmptyLocations)
|
||||
{
|
||||
if (!l.HasOutpost())
|
||||
{
|
||||
Visit(l);
|
||||
Visit(l, resetTimeSinceVisited: false);
|
||||
}
|
||||
trackedLocationDiscoveryAndVisitOrder = false;
|
||||
}
|
||||
@@ -1579,11 +1614,17 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
case "visited":
|
||||
int[] visitedIndices = subElement.GetAttributeIntArray("indices", Array.Empty<int>());
|
||||
foreach (int visitedIndex in visitedIndices)
|
||||
{
|
||||
Visit(Locations[visitedIndex], resetTimeSinceVisited: false);
|
||||
}
|
||||
//backwards compatibility
|
||||
foreach (var childElement in subElement.GetChildElements("location"))
|
||||
{
|
||||
if (GetLocation(childElement) is Location l)
|
||||
{
|
||||
Visit(l);
|
||||
Visit(l, resetTimeSinceVisited: false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1707,25 +1748,15 @@ namespace Barotrauma
|
||||
if (locationsDiscovered.Any())
|
||||
{
|
||||
var discoveryElement = new XElement("discovered",
|
||||
new XAttribute("trackedvisitedemptylocations", true));
|
||||
foreach (Location location in locationsDiscovered)
|
||||
{
|
||||
int index = Locations.IndexOf(location);
|
||||
var locationElement = new XElement("location", new XAttribute("i", index));
|
||||
discoveryElement.Add(locationElement);
|
||||
}
|
||||
new XAttribute("trackedvisitedemptylocations", true),
|
||||
new XAttribute("indices", string.Join(',', locationsDiscovered.Select(l => Locations.IndexOf(l)))));
|
||||
mapElement.Add(discoveryElement);
|
||||
}
|
||||
|
||||
if (locationsVisited.Any())
|
||||
{
|
||||
var visitElement = new XElement("visited");
|
||||
foreach (Location location in locationsVisited)
|
||||
{
|
||||
int index = Locations.IndexOf(location);
|
||||
var locationElement = new XElement("location", new XAttribute("i", index));
|
||||
visitElement.Add(locationElement);
|
||||
}
|
||||
var visitElement = new XElement("visited",
|
||||
new XAttribute("indices", string.Join(',', locationsVisited.Select(l => Locations.IndexOf(l)))));
|
||||
mapElement.Add(visitElement);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,23 +10,24 @@ namespace Barotrauma
|
||||
class MapGenerationParams : Prefab, ISerializableEntity
|
||||
{
|
||||
public static readonly PrefabSelector<MapGenerationParams> Params = new PrefabSelector<MapGenerationParams>();
|
||||
|
||||
public static MapGenerationParams Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return Params.ActivePrefab;
|
||||
}
|
||||
get { return Params.ActivePrefab; }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
public bool ShowLocations { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
public bool ShowLevelTypeNames { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
public bool ShowOverlay { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "When enabled, locations that have an active store (= a store whose stocks are kept track of in the save file) are displayed in green, locations with no active store (due to not having been visited in a long time, or not having a store in the first place) are displayed in blue, and everything else in yellow."), Editable]
|
||||
public bool ShowStoreInfo { get; set; }
|
||||
#else
|
||||
public readonly bool ShowLocations = true;
|
||||
public readonly bool ShowLevelTypeNames = false;
|
||||
@@ -49,8 +50,8 @@ namespace Barotrauma
|
||||
public float LargeLevelConnectionLength { get; set; }
|
||||
|
||||
[Serialize("20,20", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " +
|
||||
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
|
||||
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
|
||||
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
|
||||
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
|
||||
public Point VoronoiSiteInterval { get; set; }
|
||||
|
||||
[Serialize("5,5", IsPropertySaveable.Yes), Editable]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -23,8 +23,6 @@ namespace Barotrauma
|
||||
public readonly Map Map;
|
||||
public readonly RadiationParams Params;
|
||||
|
||||
private Affliction? radiationAffliction;
|
||||
|
||||
private float radiationTimer;
|
||||
|
||||
private float increasedAmount;
|
||||
@@ -62,7 +60,7 @@ namespace Barotrauma
|
||||
|
||||
int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated());
|
||||
|
||||
foreach (Location location in Map.Locations.Where(Contains))
|
||||
foreach (Location location in Map.Locations.Where(l => DepthInRadiation(l) > 0))
|
||||
{
|
||||
if (location.IsGateBetweenBiomes)
|
||||
{
|
||||
@@ -96,8 +94,6 @@ namespace Barotrauma
|
||||
increasedAmount = lastIncrease = amount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void UpdateRadiation(float deltaTime)
|
||||
{
|
||||
if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; }
|
||||
@@ -110,55 +106,66 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (radiationAffliction == null)
|
||||
{
|
||||
float radiationStrengthChange = AfflictionPrefab.RadiationSickness.Effects.FirstOrDefault()?.StrengthChange ?? 0.0f;
|
||||
radiationAffliction = new Affliction(
|
||||
AfflictionPrefab.RadiationSickness,
|
||||
(Params.RadiationDamageAmount - radiationStrengthChange) * Params.RadiationDamageDelay);
|
||||
}
|
||||
|
||||
radiationTimer = Params.RadiationDamageDelay;
|
||||
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsDead || character.Removed || !(character.CharacterHealth is { } health)) { continue; }
|
||||
|
||||
if (IsEntityRadiated(character))
|
||||
|
||||
float depthInRadiation = DepthInRadiation(character);
|
||||
if (depthInRadiation > 0)
|
||||
{
|
||||
var limb = character.AnimController.MainLimb;
|
||||
AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false);
|
||||
character.CharacterHealth.ApplyDamage(limb, attackResult);
|
||||
AfflictionPrefab afflictionPrefab;
|
||||
// Get the related affliction (if necessary, fall back to the traditional radiation sickness for slightly better backwards compatibility)
|
||||
afflictionPrefab = AfflictionPrefab.JovianRadiation ?? AfflictionPrefab.RadiationSickness;
|
||||
float currentAfflictionStrength = character.CharacterHealth.GetAfflictionStrengthByIdentifier(afflictionPrefab.Identifier);
|
||||
|
||||
// Get Jovian radiation strength, and cancel out the affliction's strength change (meant for decaying it)
|
||||
// (for simplicity, let's assume each Effect of the Affliction has the same strengthchange)
|
||||
float addedStrength = Params.RadiationDamageAmount - afflictionPrefab.Effects.FirstOrDefault()?.StrengthChange ?? 0.0f;
|
||||
|
||||
// Damage is applied periodically, so we must apply the total damage for the full period at once (after deducting strengthchange)
|
||||
addedStrength *= Params.RadiationDamageDelay;
|
||||
|
||||
// The JovianRadiation affliction has brackets of 25 strength determined by the multiplier (1x = 0-25, 2x = 25-50 etc.)
|
||||
int multiplier = (int)Math.Ceiling(depthInRadiation / Params.RadiationEffectMultipliedPerPixelDistance);
|
||||
float growthPotentialInBracket = (multiplier * 25) - currentAfflictionStrength;
|
||||
if (growthPotentialInBracket > 0)
|
||||
{
|
||||
addedStrength = Math.Min(addedStrength, growthPotentialInBracket);
|
||||
character.CharacterHealth.ApplyAffliction(
|
||||
character.AnimController?.MainLimb,
|
||||
afflictionPrefab.Instantiate(addedStrength));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(Location location)
|
||||
public float DepthInRadiation(Location location)
|
||||
{
|
||||
return Contains(location.MapPosition);
|
||||
return DepthInRadiation(location.MapPosition);
|
||||
}
|
||||
|
||||
private float DepthInRadiation(Vector2 pos)
|
||||
{
|
||||
return Amount - pos.X;
|
||||
}
|
||||
|
||||
public bool Contains(Vector2 pos)
|
||||
public float DepthInRadiation(Entity entity)
|
||||
{
|
||||
return pos.X < Amount;
|
||||
}
|
||||
|
||||
public bool IsEntityRadiated(Entity entity)
|
||||
{
|
||||
if (!Enabled) { return false; }
|
||||
if (!Enabled) { return 0; }
|
||||
if (Level.Loaded is { Type: LevelData.LevelType.LocationConnection, StartLocation: { } startLocation, EndLocation: { } endLocation } level)
|
||||
{
|
||||
if (Contains(startLocation) && Contains(endLocation)) { return true; }
|
||||
|
||||
float distance = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
|
||||
// Approximate how far between the level start and end points the entity is on the map
|
||||
float distanceNormalized = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
|
||||
var (startX, startY) = startLocation.MapPosition;
|
||||
var (endX, endY) = endLocation.MapPosition;
|
||||
Vector2 mapPos = new Vector2(startX + (endX - startX), startY + (endY - startY)) * distance;
|
||||
Vector2 mapPos = new Vector2(startX, startY) + (new Vector2(endX - startX, endY - startY) * distanceNormalized);
|
||||
|
||||
return Contains(mapPos);
|
||||
return DepthInRadiation(mapPos);
|
||||
}
|
||||
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
|
||||
@@ -9,37 +9,40 @@ namespace Barotrauma
|
||||
public string Name => nameof(RadiationParams);
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
|
||||
|
||||
[Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much radiation the world starts with.")]
|
||||
[Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much Jovian radiation the world starts with.")]
|
||||
public float StartingRadiation { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much radiation is added on each step.")]
|
||||
[Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much Jovian radiation is added on each step.")]
|
||||
public float RadiationStep { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 250f, isSaveable: IsPropertySaveable.No, "The interval at which Jovian radiation's effect multiplies in intensity, measured in map pixels. For example if this is 200, then 400 map pixels into the radiation its effect will be doubled.")]
|
||||
public float RadiationEffectMultipliedPerPixelDistance { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in radiation does it take for an outpost to be removed from the map.")]
|
||||
[Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in Jovian radiation does it take for an outpost to be removed from the map.")]
|
||||
public int CriticalRadiationThreshold { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to radiation.")]
|
||||
[Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to Jovian radiation.")]
|
||||
public int MinimumOutpostAmount { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the radiation increase animation goes.")]
|
||||
public float AnimationSpeed { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more radiation damage while in a radiated zone.")]
|
||||
[Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more of the Jovian radiation's effect while in the radiated zone.")]
|
||||
public float RadiationDamageDelay { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the radiation affliction increased by while in a radiated zone.")]
|
||||
[Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the Jovian radiation affliction increased by while in a radiated zone.")]
|
||||
public float RadiationDamageAmount { get; set; }
|
||||
|
||||
[Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of radiation.")]
|
||||
[Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of Jovian radiation.")]
|
||||
public float MaxRadiation { get; set; }
|
||||
|
||||
[Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area.")]
|
||||
|
||||
[Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the Jovian radiation increase animation goes in the map view.")]
|
||||
public float AnimationSpeed { get; set; }
|
||||
|
||||
[Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area in the map view.")]
|
||||
public Color RadiationAreaColor { get; set; }
|
||||
|
||||
[Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the radiation border sprites.")]
|
||||
[Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the Jovian radiation border sprites in the map view.")]
|
||||
public Color RadiationBorderTint { get; set; }
|
||||
|
||||
[Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation.")]
|
||||
[Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation in the map view.")]
|
||||
public float BorderAnimationSpeed { get; set; }
|
||||
|
||||
public RadiationParams(XElement element)
|
||||
|
||||
@@ -545,7 +545,24 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
//sort damageable walls by sprite depth:
|
||||
//necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise
|
||||
int i = 0;
|
||||
if (this is Structure { DrawDamageEffect: true } structure)
|
||||
{
|
||||
//insertion sort according to draw depth
|
||||
float drawDepth = structure.SpriteDepth;
|
||||
while (i < MapEntityList.Count)
|
||||
{
|
||||
float otherDrawDepth = (MapEntityList[i] as Structure)?.SpriteDepth ?? 1.0f;
|
||||
if (otherDrawDepth < drawDepth) { break; }
|
||||
i++;
|
||||
}
|
||||
MapEntityList.Insert(i, this);
|
||||
return;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < MapEntityList.Count)
|
||||
{
|
||||
i++;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -259,7 +260,7 @@ namespace Barotrauma
|
||||
if (string.IsNullOrWhiteSpace(AllowedUpgrades)) { return Enumerable.Empty<Identifier>(); }
|
||||
if (allowedUpgradeSet is null || cachedAllowedUpgrades != AllowedUpgrades)
|
||||
{
|
||||
allowedUpgradeSet = AllowedUpgrades.Split(",").ToIdentifiers().ToImmutableHashSet();
|
||||
allowedUpgradeSet = AllowedUpgrades.ToIdentifiers().ToImmutableHashSet();
|
||||
cachedAllowedUpgrades = AllowedUpgrades;
|
||||
}
|
||||
|
||||
@@ -301,12 +302,13 @@ namespace Barotrauma
|
||||
|
||||
protected void LoadDescription(ContentXElement element)
|
||||
{
|
||||
Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
|
||||
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
|
||||
string originalDescription = Description.Value;
|
||||
if (descriptionIdentifier != Identifier.Empty)
|
||||
const string descriptionIdentifierAttributeName = "descriptionidentifier";
|
||||
XAttribute descriptionIdenfifierAttribute = element.GetAttribute(descriptionIdentifierAttributeName);
|
||||
if (descriptionIdenfifierAttribute != null)
|
||||
{
|
||||
Identifier descriptionIdentifier = element.GetAttributeIdentifier(descriptionIdentifierAttributeName, Identifier.Empty);
|
||||
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}");
|
||||
}
|
||||
else if (nameIdentifier == Identifier.Empty)
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(XElement element)
|
||||
public virtual void Save(XElement element)
|
||||
{
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
}
|
||||
@@ -126,4 +126,46 @@ namespace Barotrauma
|
||||
WreckContainsThalamus = HasThalamus.No;
|
||||
}
|
||||
}
|
||||
|
||||
class EnemySubmarineInfo : ExtraSubmarineInfo
|
||||
{
|
||||
[Serialize(4000.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float Reward { get; set; }
|
||||
|
||||
[Serialize(50.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float PreferredDifficulty { get; set; }
|
||||
|
||||
private readonly HashSet<Identifier> missionTags = new HashSet<Identifier>();
|
||||
|
||||
public HashSet<Identifier> MissionTags => missionTags;
|
||||
|
||||
public EnemySubmarineInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element)
|
||||
{
|
||||
Name = $"{nameof(EnemySubmarineInfo)} ({submarineInfo.Name})";
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
foreach (var missionTag in element.GetAttributeIdentifierArray(nameof(MissionTags), Array.Empty<Identifier>()))
|
||||
{
|
||||
missionTags.Add(missionTag);
|
||||
}
|
||||
}
|
||||
|
||||
public EnemySubmarineInfo(SubmarineInfo submarineInfo) : base(submarineInfo)
|
||||
{
|
||||
Name = $"{nameof(EnemySubmarineInfo)} ({submarineInfo.Name})";
|
||||
}
|
||||
|
||||
public EnemySubmarineInfo(EnemySubmarineInfo original) : base(original)
|
||||
{
|
||||
foreach (var missionTag in original.missionTags)
|
||||
{
|
||||
missionTags.Add(missionTag);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Save(XElement element)
|
||||
{
|
||||
base.Save(element);
|
||||
element.Add(new XAttribute(nameof(MissionTags), string.Join(',', missionTags)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly static PrefabCollection<NPCSet> Sets = new PrefabCollection<NPCSet>();
|
||||
|
||||
private readonly ImmutableArray<HumanPrefab> Humans;
|
||||
public readonly ImmutableArray<HumanPrefab> Humans;
|
||||
|
||||
public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
|
||||
{
|
||||
Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file, Identifier)).ToImmutableArray();
|
||||
}
|
||||
|
||||
public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier, bool logError = true)
|
||||
public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier, bool logError = true, ContentPackage? contentPackageToLogInError = null)
|
||||
{
|
||||
HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault();
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (logError)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\".");
|
||||
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\".", contentPackage: contentPackageToLogInError);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should hallways of the minimum hallway length be always generated between modules, even if they could be placed directly against each other with no overlaps?"), Editable]
|
||||
public bool AlwaysGenerateHallways
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be destructible, regardless if damaging outposts is allowed by the server?"), Editable]
|
||||
public bool AlwaysDestructible
|
||||
{
|
||||
@@ -140,24 +147,44 @@ namespace Barotrauma
|
||||
|
||||
public ContentPath OutpostFilePath { get; set; }
|
||||
|
||||
public class ModuleCount
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "If set, a fully pre-built outpost with this tag will be used instead of generating the outpost."), Editable]
|
||||
public Identifier OutpostTag { get; set; }
|
||||
|
||||
public class ModuleCount : ISerializableEntity
|
||||
{
|
||||
public Identifier Identifier;
|
||||
public int Count;
|
||||
public int Order;
|
||||
|
||||
public Identifier RequiredFaction;
|
||||
[Serialize(0, IsPropertySaveable.Yes), Editable]
|
||||
public int Count { get; set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "Can be used to enforce the modules to be placed in a specific order, starting from the docking module (0 = first, 1 = second, etc)."), Editable]
|
||||
public int Order { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum difficulty of the current level for the module to appear in the outpost."), Editable]
|
||||
public float MinDifficulty { get; set; }
|
||||
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Maximum difficulty of the current level for the module to appear in the outpost."), Editable]
|
||||
public float MaxDifficulty { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Probability for this type of module to be included in the outpost."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
public float Probability { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes), Editable]
|
||||
public Identifier RequiredFaction { get; set; }
|
||||
|
||||
public string Name => Identifier.Value;
|
||||
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
|
||||
|
||||
public ModuleCount(ContentXElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", ""));
|
||||
Count = element.GetAttributeInt("count", 0);
|
||||
Order = element.GetAttributeInt("order", 0);
|
||||
RequiredFaction = element.GetAttributeIdentifier("requiredfaction", Identifier.Empty);
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
|
||||
public ModuleCount(Identifier id, int count)
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element: null);
|
||||
Identifier = id;
|
||||
Count = count;
|
||||
RequiredFaction = Identifier.Empty;
|
||||
@@ -176,36 +203,40 @@ namespace Barotrauma
|
||||
private class Entry
|
||||
{
|
||||
private readonly HumanPrefab humanPrefab = null;
|
||||
private readonly Identifier setIdentifier = Identifier.Empty;
|
||||
private readonly Identifier npcIdentifier = Identifier.Empty;
|
||||
public readonly Identifier SetIdentifier = Identifier.Empty;
|
||||
public readonly Identifier NpcIdentifier = Identifier.Empty;
|
||||
|
||||
public readonly Identifier FactionIdentifier = Identifier.Empty;
|
||||
|
||||
public readonly ContentPackage ContentPackage;
|
||||
|
||||
public Entry(HumanPrefab humanPrefab, Identifier factionIdentifier)
|
||||
public Entry(HumanPrefab humanPrefab, Identifier factionIdentifier, ContentPackage contentPackage)
|
||||
{
|
||||
this.humanPrefab = humanPrefab;
|
||||
this.FactionIdentifier = factionIdentifier;
|
||||
FactionIdentifier = factionIdentifier;
|
||||
ContentPackage = contentPackage;
|
||||
}
|
||||
|
||||
public Entry(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier)
|
||||
public Entry(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier, ContentPackage contentPackage)
|
||||
{
|
||||
this.setIdentifier = setIdentifier;
|
||||
this.npcIdentifier = npcIdentifier;
|
||||
this.FactionIdentifier = factionIdentifier;
|
||||
SetIdentifier = setIdentifier;
|
||||
NpcIdentifier = npcIdentifier;
|
||||
FactionIdentifier = factionIdentifier;
|
||||
ContentPackage = contentPackage;
|
||||
}
|
||||
|
||||
public HumanPrefab HumanPrefab
|
||||
=> humanPrefab ?? NPCSet.Get(setIdentifier, npcIdentifier);
|
||||
=> humanPrefab ?? NPCSet.Get(SetIdentifier, NpcIdentifier, contentPackageToLogInError: ContentPackage);
|
||||
}
|
||||
|
||||
private readonly List<Entry> entries = new List<Entry>();
|
||||
|
||||
public void Add(HumanPrefab humanPrefab, Identifier factionIdentifier)
|
||||
=> entries.Add(new Entry(humanPrefab, factionIdentifier));
|
||||
public void Add(HumanPrefab humanPrefab, Identifier factionIdentifier, ContentPackage contentPackage)
|
||||
=> entries.Add(new Entry(humanPrefab, factionIdentifier, contentPackage));
|
||||
|
||||
|
||||
public void Add(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier)
|
||||
=> entries.Add(new Entry(setIdentifier, npcIdentifier, factionIdentifier));
|
||||
public void Add(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier, ContentPackage contentPackage)
|
||||
=> entries.Add(new Entry(setIdentifier, npcIdentifier, factionIdentifier, contentPackage));
|
||||
|
||||
public IEnumerator<HumanPrefab> GetEnumerator()
|
||||
{
|
||||
@@ -224,7 +255,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (entry.FactionIdentifier == Identifier.Empty || factions.Any(f => f.Identifier == entry.FactionIdentifier))
|
||||
{
|
||||
yield return entry.HumanPrefab;
|
||||
if (entry.HumanPrefab != null)
|
||||
{
|
||||
yield return entry.HumanPrefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,6 +295,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath));
|
||||
OutpostTag = element.GetAttributeIdentifier(nameof(OutpostTag), Identifier.Empty);
|
||||
|
||||
var humanPrefabCollections = new List<NpcCollection>();
|
||||
foreach (var subElement in element.Elements())
|
||||
@@ -268,7 +303,23 @@ namespace Barotrauma
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "modulecount":
|
||||
moduleCounts.Add(new ModuleCount(subElement));
|
||||
var newModuleCount = new ModuleCount(subElement);
|
||||
if (moduleCounts.None() && newModuleCount.Probability < 1.0f)
|
||||
{
|
||||
DebugConsole.AddWarning(
|
||||
$"Potential error in outpost generation parameters \"{Identifier}\"." +
|
||||
$" The first module is set to spawn with a probability of {newModuleCount.Probability}%. The first module must always spawn, so the probability will be ignored.",
|
||||
contentPackage: ContentPackage);
|
||||
newModuleCount.Probability = 1.0f;
|
||||
}
|
||||
else if (newModuleCount.Probability <= 0.0f)
|
||||
{
|
||||
DebugConsole.AddWarning(
|
||||
$"Potential error in outpost generation parameters \"{Identifier}\"." +
|
||||
$" Probability of the module {newModuleCount.Identifier} is 0% (the module should never spawn, so there's no reason to include it in the generation parameters.",
|
||||
contentPackage: ContentPackage);
|
||||
}
|
||||
moduleCounts.Add(newModuleCount);
|
||||
break;
|
||||
case "npcs":
|
||||
var newCollection = new NpcCollection();
|
||||
@@ -278,11 +329,11 @@ namespace Barotrauma
|
||||
Identifier faction = npcElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
if (from != Identifier.Empty)
|
||||
{
|
||||
newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty), faction);
|
||||
newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty), faction, npcElement.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from), faction);
|
||||
newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from), faction, npcElement.ContentPackage);
|
||||
}
|
||||
}
|
||||
humanPrefabCollections.Add(newCollection);
|
||||
@@ -299,7 +350,7 @@ namespace Barotrauma
|
||||
return moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag)?.Count ?? 0;
|
||||
}
|
||||
|
||||
public void SetModuleCount(Identifier moduleFlag, int count)
|
||||
public void SetModuleCount(Identifier moduleFlag, int count, float? probability = null, float? minDifficulty = null, float? maxDifficulty = null)
|
||||
{
|
||||
if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; }
|
||||
if (count <= 0)
|
||||
@@ -311,12 +362,20 @@ namespace Barotrauma
|
||||
var moduleCount = moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag);
|
||||
if (moduleCount == null)
|
||||
{
|
||||
moduleCounts.Add(new ModuleCount(moduleFlag, count));
|
||||
}
|
||||
else
|
||||
{
|
||||
moduleCount.Count = count;
|
||||
moduleCount = new ModuleCount(moduleFlag, count);
|
||||
if (moduleCount.Probability <= 0.0f)
|
||||
{
|
||||
DebugConsole.AddWarning(
|
||||
$"Potential error in outpost generation parameters \"{Identifier}\"."+
|
||||
$" Probability of the module {moduleCount.Identifier} is 0 (the module should never spawn, so there's no reason to include it in the generation parameters.",
|
||||
contentPackage: ContentPackage);
|
||||
}
|
||||
moduleCounts.Add(moduleCount);
|
||||
}
|
||||
moduleCount.Count = count;
|
||||
if (probability.HasValue) { moduleCount.Probability = probability.Value; }
|
||||
if (minDifficulty.HasValue) { moduleCount.MinDifficulty = minDifficulty.Value; }
|
||||
if (maxDifficulty.HasValue) { moduleCount.MaxDifficulty = maxDifficulty.Value; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,12 +389,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(IEnumerable<FactionPrefab> factions, Rand.RandSync randSync)
|
||||
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(IEnumerable<FactionPrefab> factions, Submarine sub, Rand.RandSync randSync)
|
||||
{
|
||||
if (!humanPrefabCollections.Any()) { return Array.Empty<HumanPrefab>(); }
|
||||
|
||||
var collection = humanPrefabCollections.GetRandom(randSync);
|
||||
return collection.GetByFaction(factions).ToImmutableList();
|
||||
return collection
|
||||
.GetByFaction(factions)
|
||||
.Where(humanPrefab => !humanPrefab.RequireSpawnPointTag || WayPoint.WayPointList.Any(wp => wp.Submarine == sub && humanPrefab.GetSpawnPointTags().Any(tag => wp.Tags.Contains(tag))))
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
|
||||
|
||||
@@ -56,6 +56,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many times the generator retries generating an outpost with a different seed if it fails to generate a valid outpost with no overlaps.
|
||||
/// </summary>
|
||||
const int MaxOutpostGenerationRetries = 6;
|
||||
|
||||
public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false)
|
||||
{
|
||||
return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost);
|
||||
@@ -66,6 +71,8 @@ namespace Barotrauma
|
||||
return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost);
|
||||
}
|
||||
|
||||
private static SubmarineInfo usedForceOutpostModule;
|
||||
|
||||
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
|
||||
{
|
||||
var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
|
||||
@@ -84,9 +91,76 @@ namespace Barotrauma
|
||||
generationParams = newParams;
|
||||
}
|
||||
|
||||
locationType = location.GetLocationType();
|
||||
locationType = location.Type;
|
||||
}
|
||||
|
||||
Submarine sub = null;
|
||||
if (generationParams.OutpostTag.IsEmpty)
|
||||
{
|
||||
var forceOutpostModule = GameMain.GameSession?.ForceOutpostModule;
|
||||
sub = GenerateFromModules(generationParams, outpostModuleFiles, sub, locationType, location, onlyEntrance, allowInvalidOutpost);
|
||||
if (sub != null)
|
||||
{
|
||||
return sub;
|
||||
}
|
||||
else if (forceOutpostModule != null)
|
||||
{
|
||||
//failed to force the module, abort
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var outpostFiles = ContentPackageManager.EnabledPackages.All
|
||||
.SelectMany(p => p.GetFiles<OutpostFile>())
|
||||
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
|
||||
.OrderBy(f => f.UintIdentifier).ToList();
|
||||
|
||||
List<SubmarineInfo> outpostInfos = new List<SubmarineInfo>();
|
||||
foreach (var outpostFile in outpostFiles)
|
||||
{
|
||||
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
|
||||
}
|
||||
if (generationParams.OutpostTag.IsEmpty)
|
||||
{
|
||||
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (outpostInfos.Any(o => o.OutpostTags.Contains(generationParams.OutpostTag)))
|
||||
{
|
||||
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.Contains(generationParams.OutpostTag));
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find any outposts with the tag {generationParams.OutpostTag}. Choosing a random one instead...");
|
||||
}
|
||||
}
|
||||
if (!outpostInfos.Any())
|
||||
{
|
||||
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
|
||||
}
|
||||
var prebuiltOutpostInfo = outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
|
||||
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
|
||||
serverSettings.SelectedOutpostName != "Random")
|
||||
{
|
||||
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
|
||||
if (matchingOutpost != null)
|
||||
{
|
||||
prebuiltOutpostInfo = matchingOutpost;
|
||||
}
|
||||
}
|
||||
|
||||
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
|
||||
sub = new Submarine(prebuiltOutpostInfo);
|
||||
sub.Info.OutpostGenerationParams = generationParams;
|
||||
location?.RemoveTakenItems();
|
||||
EnableFactionSpecificEntities(sub, location);
|
||||
return sub;
|
||||
}
|
||||
|
||||
private static Submarine GenerateFromModules(OutpostGenerationParams generationParams, OutpostModuleFile[] outpostModuleFiles, Submarine sub, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
|
||||
{
|
||||
//load the infos of the outpost module files
|
||||
List<SubmarineInfo> outpostModules = new List<SubmarineInfo>();
|
||||
foreach (var outpostModuleFile in outpostModuleFiles)
|
||||
@@ -113,9 +187,8 @@ namespace Barotrauma
|
||||
|
||||
List<PlacedModule> selectedModules = new List<PlacedModule>();
|
||||
bool generationFailed = false;
|
||||
int remainingTries = 5;
|
||||
Submarine sub = null;
|
||||
while (remainingTries > -1 && outpostModules.Any())
|
||||
int remainingOutpostGenerationTries = MaxOutpostGenerationRetries;
|
||||
while (remainingOutpostGenerationTries > -1 && outpostModules.Any())
|
||||
{
|
||||
if (sub != null)
|
||||
{
|
||||
@@ -140,10 +213,10 @@ namespace Barotrauma
|
||||
GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
|
||||
GameMain.Server.EntityEventManager.UniqueEvents.RemoveRange(uniqueEventCount, GameMain.Server.EntityEventManager.UniqueEvents.Count - uniqueEventCount);
|
||||
#endif
|
||||
if (remainingTries <= 0)
|
||||
if (remainingOutpostGenerationTries <= 0)
|
||||
{
|
||||
generationFailed = true;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,9 +253,18 @@ namespace Barotrauma
|
||||
|
||||
//the first module is spawned separately, remove it from the list of pending modules
|
||||
Identifier initialModuleFlag = pendingModuleFlags.FirstOrDefault().IfEmpty("airlock".ToIdentifier());
|
||||
pendingModuleFlags.Remove(initialModuleFlag);
|
||||
pendingModuleFlags.Remove(initialModuleFlag);
|
||||
|
||||
bool hasForceOutpostWithInitialFlag = GameMain.GameSession?.ForceOutpostModule != null && GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(initialModuleFlag);
|
||||
var initialModule = hasForceOutpostWithInitialFlag ? GameMain.GameSession.ForceOutpostModule : GetRandomModule(outpostModules, initialModuleFlag, locationType);
|
||||
|
||||
if (hasForceOutpostWithInitialFlag)
|
||||
{
|
||||
DebugConsole.NewMessage($"Using Force outpost module as initial in Outpost generation: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}", Color.Yellow);
|
||||
usedForceOutpostModule = GameMain.GameSession.ForceOutpostModule;
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
}
|
||||
|
||||
var initialModule = GetRandomModule(outpostModules, initialModuleFlag, locationType);
|
||||
if (initialModule == null)
|
||||
{
|
||||
throw new Exception("Failed to generate an outpost (no airlock modules found).");
|
||||
@@ -192,7 +274,7 @@ namespace Barotrauma
|
||||
if (pendingModuleFlags.Contains("initialFlag".ToIdentifier())) { pendingModuleFlags.Remove(initialFlag); }
|
||||
}
|
||||
|
||||
if (remainingTries == 1)
|
||||
if (remainingOutpostGenerationTries == 1)
|
||||
{
|
||||
//generation has failed and only one attempt left, try removing duplicate modules
|
||||
pendingModuleFlags = pendingModuleFlags.Distinct().ToList();
|
||||
@@ -202,18 +284,30 @@ namespace Barotrauma
|
||||
selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag);
|
||||
|
||||
AppendToModule(
|
||||
selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags,
|
||||
selectedModules,
|
||||
locationType,
|
||||
selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags,
|
||||
selectedModules,
|
||||
locationType,
|
||||
allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams,
|
||||
allowDifferentLocationType: remainingTries == 1);
|
||||
allowDifferentLocationType: remainingOutpostGenerationTries == 1);
|
||||
|
||||
if (GameMain.GameSession?.ForceOutpostModule != null)
|
||||
{
|
||||
if (remainingOutpostGenerationTries > 0)
|
||||
{
|
||||
remainingOutpostGenerationTries--;
|
||||
continue;
|
||||
}
|
||||
DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pendingModuleFlags.Any(flag => flag != "none"))
|
||||
{
|
||||
if (!allowInvalidOutpost)
|
||||
{
|
||||
remainingTries--;
|
||||
if (remainingTries <= 0)
|
||||
remainingOutpostGenerationTries--;
|
||||
if (remainingOutpostGenerationTries <= 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
|
||||
}
|
||||
@@ -255,34 +349,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
EnableFactionSpecificEntities(sub, location);
|
||||
return sub;
|
||||
return sub;
|
||||
}
|
||||
remainingTries--;
|
||||
remainingOutpostGenerationTries--;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
|
||||
#else
|
||||
DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
|
||||
#endif
|
||||
|
||||
var outpostFiles = ContentPackageManager.EnabledPackages.All
|
||||
.SelectMany(p => p.GetFiles<OutpostFile>())
|
||||
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
|
||||
.OrderBy(f => f.UintIdentifier).ToArray();
|
||||
if (!outpostFiles.Any())
|
||||
{
|
||||
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
|
||||
}
|
||||
var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.ServerAndClient).Path.Value)
|
||||
{
|
||||
Type = SubmarineType.Outpost
|
||||
};
|
||||
sub = new Submarine(prebuiltOutpostInfo);
|
||||
sub.Info.OutpostGenerationParams = generationParams;
|
||||
location?.RemoveTakenItems();
|
||||
EnableFactionSpecificEntities(sub, location);
|
||||
return sub;
|
||||
DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
|
||||
return null;
|
||||
|
||||
List<MapEntity> loadEntities(Submarine sub)
|
||||
{
|
||||
@@ -293,13 +366,18 @@ namespace Barotrauma
|
||||
var selectedModule = selectedModules[i];
|
||||
sub.Info.GameVersion = selectedModule.Info.GameVersion;
|
||||
var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset);
|
||||
|
||||
|
||||
if (usedForceOutpostModule != null && usedForceOutpostModule == selectedModule.Info)
|
||||
{
|
||||
sub.ForcedOutpostModuleWayPoints = moduleEntities.OfType<WayPoint>().ToList();
|
||||
}
|
||||
|
||||
MapEntity.InitializeLoadedLinks(moduleEntities);
|
||||
|
||||
foreach (MapEntity entity in moduleEntities.ToList())
|
||||
{
|
||||
entity.OriginalModuleIndex = i;
|
||||
if (!(entity is Item item)) { continue; }
|
||||
if (entity is not Item item) { continue; }
|
||||
var door = item.GetComponent<Door>();
|
||||
if (door != null)
|
||||
{
|
||||
@@ -319,7 +397,15 @@ namespace Barotrauma
|
||||
{
|
||||
hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags);
|
||||
}
|
||||
|
||||
|
||||
if (Screen.Selected is { IsEditor: false })
|
||||
{
|
||||
foreach (Identifier layer in selectedModule.Info.LayersHiddenByDefault)
|
||||
{
|
||||
Submarine.SetLayerEnabled(layer, enabled: false, entities: moduleEntities);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hullEntities.Any())
|
||||
{
|
||||
selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint());
|
||||
@@ -363,14 +449,19 @@ namespace Barotrauma
|
||||
selectedModule.Offset =
|
||||
(selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) -
|
||||
selectedModule.ThisGap.WorldPosition;
|
||||
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
|
||||
if (generationParams.AlwaysGenerateHallways)
|
||||
{
|
||||
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
|
||||
if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null)
|
||||
{
|
||||
selectedModule.Offset += moveDir * generationParams.MinHallwayLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
entities[selectedModule] = moduleEntities;
|
||||
}
|
||||
|
||||
int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height)));
|
||||
|
||||
bool overlapsFound = true;
|
||||
int iteration = 0;
|
||||
while (overlapsFound)
|
||||
@@ -384,29 +475,35 @@ namespace Barotrauma
|
||||
GetSubsequentModules(placedModule, selectedModules, ref subsequentModules);
|
||||
List<PlacedModule> otherModules = selectedModules.Except(subsequentModules).ToList();
|
||||
|
||||
int remainingTries = 10;
|
||||
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0)
|
||||
int remainingOverlapPreventionTries = 10;
|
||||
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingOverlapPreventionTries > 0)
|
||||
{
|
||||
overlapsFound = true;
|
||||
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary<PlacedModule,Vector2> solution))
|
||||
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, generationParams.MinHallwayLength, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
|
||||
{
|
||||
foreach (KeyValuePair<PlacedModule, Vector2> kvp in solution)
|
||||
{
|
||||
kvp.Key.Offset += kvp.Value;
|
||||
}
|
||||
kvp.Key.Offset += kvp.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
remainingTries--;
|
||||
}
|
||||
remainingOverlapPreventionTries--;
|
||||
}
|
||||
if (remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
|
||||
ModuleBelowInitialModule(placedModule, selectedModules.First()))
|
||||
{
|
||||
overlapsFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
iteration++;
|
||||
if (iteration > 10)
|
||||
if (iteration > 10)
|
||||
{
|
||||
generationFailed = true;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,10 +535,10 @@ namespace Barotrauma
|
||||
//eww
|
||||
structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth);
|
||||
#if CLIENT
|
||||
foreach (var light in structure.Lights)
|
||||
{
|
||||
light.IsBackground = true;
|
||||
}
|
||||
foreach (var light in structure.Lights)
|
||||
{
|
||||
light.IsBackground = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -478,21 +575,42 @@ namespace Barotrauma
|
||||
private static List<Identifier> SelectModules(IEnumerable<SubmarineInfo> modules, Location location, OutpostGenerationParams generationParams)
|
||||
{
|
||||
int totalModuleCount = generationParams.TotalModuleCount;
|
||||
int totalModuleCountExcludingOptional = totalModuleCount - generationParams.ModuleCounts.Count(m => m.Probability < 1.0f);
|
||||
var pendingModuleFlags = new List<Identifier>();
|
||||
bool availableModulesFound = true;
|
||||
|
||||
Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Identifier;
|
||||
pendingModuleFlags.Add(initialModuleFlag);
|
||||
while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound)
|
||||
while (pendingModuleFlags.Count < totalModuleCountExcludingOptional && availableModulesFound)
|
||||
{
|
||||
availableModulesFound = false;
|
||||
foreach (var moduleCount in generationParams.ModuleCounts)
|
||||
{
|
||||
if (!moduleCount.RequiredFaction.IsEmpty &&
|
||||
location?.Faction?.Prefab.Identifier != moduleCount.RequiredFaction &&
|
||||
location?.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction)
|
||||
float? difficulty = Level.ForcedDifficulty ?? location?.LevelData?.Difficulty;
|
||||
if (difficulty.HasValue)
|
||||
{
|
||||
continue;
|
||||
if (difficulty.Value < moduleCount.MinDifficulty || difficulty.Value > moduleCount.MaxDifficulty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//if this is a module that we're trying to force into the outpost,
|
||||
//ignore probability and faction requirements
|
||||
if (GameMain.GameSession?.ForceOutpostModule == null ||
|
||||
!GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier))
|
||||
{
|
||||
if (moduleCount.Probability < 1.0f &&
|
||||
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > moduleCount.Probability)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!moduleCount.RequiredFaction.IsEmpty &&
|
||||
location?.Faction?.Prefab.Identifier != moduleCount.RequiredFaction &&
|
||||
location?.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier))
|
||||
{
|
||||
@@ -543,7 +661,8 @@ namespace Barotrauma
|
||||
/// <param name="selectedModules">The modules we've already selected to be used in the outpost.</param>
|
||||
/// <param name="locationType">The type of the location we're generating the outpost for.</param>
|
||||
/// <param name="tryReplacingCurrentModule">If we fail to append to the current module, should we try replacing it with something else and see if we can append to it then?</param>
|
||||
/// <param name="allowExtendBelowInitialModule">Is the module allowed to be placed further down than the initial module (usually the airlock module)?</param>
|
||||
/// <param name="allowExtendBelowInitialModule">Is the module allowed to be placed further down than the initial module (usually the airlock module)?
|
||||
/// Note that at this point we only determine which module to attach to which, but not the actual positions or bounds of the modules, so it's possible for a module to attach to the side of the airlock but still extend below the airlock if it's very tall for example.</param>
|
||||
/// <param name="allowDifferentLocationType">If we fail to find a module suitable for the location type, should we use a module that's meant for a different location type instead?</param>
|
||||
private static bool AppendToModule(PlacedModule currentModule,
|
||||
List<SubmarineInfo> availableModules,
|
||||
@@ -782,7 +901,7 @@ namespace Barotrauma
|
||||
Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset;
|
||||
if (Submarine.RectContains(rect, gapPos1) ||
|
||||
Submarine.RectContains(rect, gapPos2) ||
|
||||
MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _))
|
||||
MathUtils.GetLineWorldRectangleIntersection(gapPos1, gapPos2, rect, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -800,6 +919,21 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the lowest point of the module is below the lowest point of the initial (docking) module.
|
||||
/// This shouldn't happen, because it can cause modules to overlap with the docked sub.
|
||||
/// </summary>
|
||||
private static bool ModuleBelowInitialModule(PlacedModule module, PlacedModule initialModule)
|
||||
{
|
||||
Rectangle bounds = module.Bounds;
|
||||
bounds.Location += (module.Offset + module.MoveOffset).ToPoint();
|
||||
|
||||
Rectangle initialModuleBounds = initialModule.Bounds;
|
||||
initialModuleBounds.Location += (initialModule.Offset + initialModule.MoveOffset).ToPoint();
|
||||
|
||||
return bounds.Bottom < initialModuleBounds.Bottom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to find a way to move the modules in a way that stops the 2 specific modules from overlapping.
|
||||
/// Done by iterating through the modules and testing how much the subsequent modules (i.e. modules that are further from the initial outpost)
|
||||
@@ -811,7 +945,13 @@ namespace Barotrauma
|
||||
/// <param name="allmodules">All generated modules</param>
|
||||
/// <param name="solution">The solution to the overlap (if any). Key = placed module, value = distance to move the module</param>
|
||||
/// <returns>Was a solution found for resolving the overlap.</returns>
|
||||
private static bool FindOverlapSolution(IEnumerable<PlacedModule> movableModules, PlacedModule module1, PlacedModule module2, IEnumerable<PlacedModule> allmodules, out Dictionary<PlacedModule, Vector2> solution)
|
||||
private static bool FindOverlapSolution(
|
||||
IEnumerable<PlacedModule> movableModules,
|
||||
PlacedModule module1, PlacedModule module2,
|
||||
IEnumerable<PlacedModule> allmodules,
|
||||
float minMoveAmount,
|
||||
int maxMoveAmount,
|
||||
out Dictionary<PlacedModule, Vector2> solution)
|
||||
{
|
||||
solution = new Dictionary<PlacedModule, Vector2>();
|
||||
foreach (PlacedModule module in movableModules)
|
||||
@@ -825,15 +965,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (module.ThisGap.ConnectedDoor == null && module.PreviousGap.ConnectedDoor == null) { continue; }
|
||||
Vector2 moveDir = GetMoveDir(module.ThisGapPosition);
|
||||
Vector2 moveStep = moveDir * 50.0f;
|
||||
Vector2 currentMove = Vector2.Zero;
|
||||
float maxMoveAmount = 2000.0f;
|
||||
const float moveStep = 50.0f;
|
||||
Vector2 currentMove = moveDir * Math.Max(minMoveAmount, moveStep);
|
||||
|
||||
List<PlacedModule> subsequentModules2 = new List<PlacedModule>();
|
||||
GetSubsequentModules(module, movableModules, ref subsequentModules2);
|
||||
while (currentMove.LengthSquared() < maxMoveAmount * maxMoveAmount)
|
||||
{
|
||||
currentMove += moveStep;
|
||||
foreach (PlacedModule movedModule in subsequentModules2)
|
||||
{
|
||||
movedModule.MoveOffset = currentMove;
|
||||
@@ -850,6 +988,7 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
}
|
||||
currentMove += moveDir * moveStep;
|
||||
}
|
||||
foreach (PlacedModule movedModule in allmodules)
|
||||
{
|
||||
@@ -914,20 +1053,21 @@ namespace Barotrauma
|
||||
}
|
||||
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
|
||||
|
||||
var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
|
||||
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
|
||||
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
|
||||
if (!suitableModules.Any())
|
||||
{
|
||||
//no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
|
||||
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
|
||||
suitableModules = suitableModulesForAnyOutpost;
|
||||
//still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module
|
||||
if (!suitableModules.Any())
|
||||
{
|
||||
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
|
||||
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
|
||||
}
|
||||
//still not found! Try if we can find a generic module that's not meant to attach to the previous module
|
||||
if (!suitableModules.Any())
|
||||
{
|
||||
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
|
||||
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -945,10 +1085,31 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
|
||||
var suitableModule = ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
|
||||
|
||||
if (GameMain.GameSession?.ForceOutpostModule != null)
|
||||
{
|
||||
if (suitableModules.Any(module => module.OutpostModuleInfo.Name == GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name) ||
|
||||
suitableModulesForAnyOutpost.Any(module => module.OutpostModuleInfo.Name == GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name))
|
||||
{
|
||||
var forceOutpostModule = GameMain.GameSession.ForceOutpostModule;
|
||||
System.Diagnostics.Debug.WriteLine($"Inserting Force outpost module in Outpost generation: {forceOutpostModule.OutpostModuleInfo.Name}");
|
||||
GameMain.GameSession.ForceOutpostModule = null;
|
||||
usedForceOutpostModule = forceOutpostModule;
|
||||
return forceOutpostModule;
|
||||
}
|
||||
else if (GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag))
|
||||
{
|
||||
// if our force module has the same tag as the selected random one, return nothing
|
||||
// because we don't want another module of the same type to be hogging the only spot for that type
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return suitableModule;
|
||||
}
|
||||
|
||||
IEnumerable<SubmarineInfo> GetSuitable(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
|
||||
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
|
||||
{
|
||||
IEnumerable<SubmarineInfo> suitable = modules;
|
||||
if (requireCorrectLocationType)
|
||||
@@ -1199,7 +1360,14 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
|
||||
if (thisWayPoint == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\".");
|
||||
}
|
||||
if (previousWayPoint == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\".");
|
||||
}
|
||||
}
|
||||
|
||||
gapToRemove.ConnectedDoor?.Item.Remove();
|
||||
@@ -1215,7 +1383,7 @@ namespace Barotrauma
|
||||
var suitableHallwayModules = hallwayModules.Where(m =>
|
||||
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) &&
|
||||
m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s)));
|
||||
if (suitableHallwayModules.Count() == 0)
|
||||
if (suitableHallwayModules.None())
|
||||
{
|
||||
suitableHallwayModules = hallwayModules.Where(m =>
|
||||
!m.OutpostModuleInfo.AllowAttachToModules.Any() ||
|
||||
@@ -1359,19 +1527,41 @@ namespace Barotrauma
|
||||
(endWaypoint, startWaypoint) = (startWaypoint, endWaypoint);
|
||||
}
|
||||
|
||||
if (hallwayLength > 100 && isHorizontal)
|
||||
//if the hallway is longer than 100 pixels, generate some waypoints inside it
|
||||
//for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders
|
||||
const float distanceBetweenWaypoints = 100.0f;
|
||||
if (hallwayLength > distanceBetweenWaypoints)
|
||||
{
|
||||
//if the hallway is longer than 100 pixels, generate some waypoints inside it
|
||||
//for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders
|
||||
WayPoint prevWayPoint = startWaypoint;
|
||||
WayPoint firstWayPoint = null;
|
||||
for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f)
|
||||
if (isHorizontal)
|
||||
{
|
||||
var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub);
|
||||
firstWayPoint ??= newWayPoint;
|
||||
prevWayPoint.linkedTo.Add(newWayPoint);
|
||||
newWayPoint.linkedTo.Add(prevWayPoint);
|
||||
prevWayPoint = newWayPoint;
|
||||
for (float x = leftHull.Rect.Right + distanceBetweenWaypoints / 2; x < rightHull.Rect.X - distanceBetweenWaypoints / 2; x += distanceBetweenWaypoints)
|
||||
{
|
||||
var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub);
|
||||
firstWayPoint ??= newWayPoint;
|
||||
prevWayPoint.linkedTo.Add(newWayPoint);
|
||||
newWayPoint.linkedTo.Add(prevWayPoint);
|
||||
prevWayPoint = newWayPoint;
|
||||
}
|
||||
}
|
||||
else if (startWaypoint.Ladders == null)
|
||||
{
|
||||
float bottom = bottomHull.Rect.Y;
|
||||
float top = topHull.Rect.Y - topHull.Rect.Height;
|
||||
for (float y = bottom + distanceBetweenWaypoints; y < top - distanceBetweenWaypoints; y += distanceBetweenWaypoints)
|
||||
{
|
||||
var newWayPoint = new WayPoint(new Vector2(startWaypoint.Position.X, y), SpawnType.Path, sub);
|
||||
firstWayPoint ??= newWayPoint;
|
||||
prevWayPoint.linkedTo.Add(newWayPoint);
|
||||
newWayPoint.linkedTo.Add(prevWayPoint);
|
||||
prevWayPoint = newWayPoint;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
startWaypoint.linkedTo.Add(endWaypoint);
|
||||
endWaypoint.linkedTo.Add(startWaypoint);
|
||||
}
|
||||
if (firstWayPoint != null)
|
||||
{
|
||||
@@ -1387,9 +1577,9 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
startWaypoint.linkedTo.Add(endWaypoint);
|
||||
endWaypoint.linkedTo.Add(startWaypoint);
|
||||
endWaypoint.linkedTo.Add(startWaypoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return placedEntities;
|
||||
}
|
||||
@@ -1499,12 +1689,16 @@ namespace Barotrauma
|
||||
|
||||
static bool ShouldRemoveLinkedEntity(MapEntity e, bool doorInUse, PlacedModule module)
|
||||
{
|
||||
if (e is Item it && it.IsLadder)
|
||||
if (e is Item { IsLadder: true } ladderItem)
|
||||
{
|
||||
if (module.UsedGapPositions.HasFlag(OutpostModuleInfo.GapPosition.Top) || module.UsedGapPositions.HasFlag(OutpostModuleInfo.GapPosition.Bottom))
|
||||
int linkedToLadderCount = Door.DoorList.Count(otherDoor => otherDoor.Item.linkedTo.Contains(ladderItem));
|
||||
if (linkedToLadderCount > 1)
|
||||
{
|
||||
//if there's multiple doors linked to the ladder, never remove it
|
||||
//(the ladder is presumably not just for moving between two modules in that case, but might e.g. go through the whole module)
|
||||
return false;
|
||||
}
|
||||
return ladderItem.RemoveIfLinkedOutpostDoorInUse == doorInUse;
|
||||
}
|
||||
|
||||
if (e is Structure structure)
|
||||
@@ -1670,7 +1864,7 @@ namespace Barotrauma
|
||||
if (location?.Faction != null) { factions.Add(location.Faction.Prefab); }
|
||||
if (location?.SecondaryFaction != null) { factions.Add(location.SecondaryFaction.Prefab); }
|
||||
|
||||
var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, Rand.RandSync.ServerAndClient);
|
||||
var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, outpost, Rand.RandSync.ServerAndClient);
|
||||
foreach (HumanPrefab humanPrefab in humanPrefabs)
|
||||
{
|
||||
if (humanPrefab is null) { continue; }
|
||||
|
||||
@@ -46,6 +46,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private const float DefaultMaxAvailabilityRelativeToMin = 1.2f;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the item is only available in outposts with this faction.
|
||||
/// </summary>
|
||||
public Identifier RequiredFaction { get; private set; }
|
||||
|
||||
private readonly Dictionary<Identifier, float> minReputation = new Dictionary<Identifier, float>();
|
||||
|
||||
/// <summary>
|
||||
@@ -67,7 +72,7 @@ namespace Barotrauma
|
||||
MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultAmount), CargoManager.MaxQuantity);
|
||||
MaxAvailableAmount = MathHelper.Clamp(GetMaxAmount(element, defaultValue: (int)(MinAvailableAmount * DefaultMaxAvailabilityRelativeToMin)), MinAvailableAmount, CargoManager.MaxQuantity);
|
||||
RequiresUnlock = element.GetAttributeBool("requiresunlock", false);
|
||||
|
||||
RequiredFaction = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
|
||||
System.Diagnostics.Debug.Assert(MaxAvailableAmount >= MinAvailableAmount);
|
||||
}
|
||||
|
||||
@@ -115,6 +120,7 @@ namespace Barotrauma
|
||||
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
|
||||
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
|
||||
bool requiresUnlock = element.GetAttributeBool("requiresunlock", false);
|
||||
Identifier requiredFactionByDefault = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
|
||||
foreach (XElement childElement in element.GetChildElements("price"))
|
||||
{
|
||||
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
|
||||
@@ -137,7 +143,10 @@ namespace Barotrauma
|
||||
buyingPriceMultiplier: storeBuyingMultiplier,
|
||||
displayNonEmpty: displayNonEmpty,
|
||||
requiresUnlock: requiresUnlock,
|
||||
storeIdentifier: storeIdentifier);
|
||||
storeIdentifier: storeIdentifier)
|
||||
{
|
||||
RequiredFaction = childElement.GetAttributeIdentifier(nameof(RequiredFaction), requiredFactionByDefault)
|
||||
};
|
||||
priceInfo.LoadReputationRestrictions(childElement);
|
||||
priceInfos.Add(priceInfo);
|
||||
}
|
||||
@@ -150,7 +159,10 @@ namespace Barotrauma
|
||||
minLevelDifficulty: minLevelDifficulty,
|
||||
buyingPriceMultiplier: buyingPriceMultiplier,
|
||||
displayNonEmpty: displayNonEmpty,
|
||||
requiresUnlock: requiresUnlock);
|
||||
requiresUnlock: requiresUnlock)
|
||||
{
|
||||
RequiredFaction = requiredFactionByDefault
|
||||
};
|
||||
defaultPrice.LoadReputationRestrictions(element);
|
||||
return priceInfos;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ namespace Barotrauma
|
||||
const float LeakThreshold = 0.1f;
|
||||
const float BigGapThreshold = 0.7f;
|
||||
|
||||
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
|
||||
|
||||
#if CLIENT
|
||||
public SpriteEffects SpriteEffects = SpriteEffects.None;
|
||||
#endif
|
||||
@@ -96,10 +98,19 @@ namespace Barotrauma
|
||||
{
|
||||
get { return base.Prefab.Name.Value; }
|
||||
}
|
||||
|
||||
public bool HasBody
|
||||
{
|
||||
get { return Prefab.Body; }
|
||||
get { return Prefab.Body && !DisableCollision; }
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBodyByDefault)]
|
||||
/// <summary>
|
||||
/// Note that changing the value mid-round will not have an effect: this is only intended for disabling the collisions on a structure in the sub editor.
|
||||
/// </summary>
|
||||
public bool DisableCollision
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public List<Body> Bodies { get; private set; }
|
||||
@@ -252,7 +263,7 @@ namespace Barotrauma
|
||||
{
|
||||
CreateStairBodies();
|
||||
}
|
||||
else if (Prefab.Body)
|
||||
else if (HasBody)
|
||||
{
|
||||
CreateSections();
|
||||
UpdateSections();
|
||||
@@ -323,7 +334,7 @@ namespace Barotrauma
|
||||
{
|
||||
Rectangle oldRect = Rect;
|
||||
base.Rect = value;
|
||||
if (Prefab.Body)
|
||||
if (HasBody)
|
||||
{
|
||||
CreateSections();
|
||||
UpdateSections();
|
||||
@@ -499,11 +510,21 @@ namespace Barotrauma
|
||||
{
|
||||
CastShadow = Prefab.CastShadow;
|
||||
}
|
||||
if (element?.GetAttribute(nameof(Indestructible)) == null)
|
||||
{
|
||||
Indestructible = Prefab.ConfigElement.GetAttributeBool(nameof(Indestructible), false);
|
||||
}
|
||||
|
||||
//if the prefab normally has a body, but it has been disabled by DisableCollision,
|
||||
//we still want the item in the wall list to render it correctly
|
||||
if (Prefab.Body)
|
||||
{
|
||||
Bodies = new List<Body>();
|
||||
WallList.Add(this);
|
||||
}
|
||||
|
||||
if (HasBody)
|
||||
{
|
||||
Bodies = new List<Body>();
|
||||
CreateSections();
|
||||
UpdateSections();
|
||||
}
|
||||
@@ -583,7 +604,7 @@ namespace Barotrauma
|
||||
};
|
||||
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
|
||||
{
|
||||
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
|
||||
if (!property.Value.Attributes.OfType<Serialize>().Any()) { continue; }
|
||||
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
|
||||
}
|
||||
if (FlippedX) clone.FlipX(false);
|
||||
@@ -963,7 +984,7 @@ namespace Barotrauma
|
||||
|
||||
public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true, bool createWallDamageProjectiles = false)
|
||||
{
|
||||
if (!Prefab.Body || Prefab.Platform || Indestructible) { return; }
|
||||
if (!HasBody || Prefab.Platform || Indestructible) { return; }
|
||||
|
||||
if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; }
|
||||
|
||||
@@ -1109,7 +1130,7 @@ namespace Barotrauma
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = false)
|
||||
{
|
||||
if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); }
|
||||
if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); }
|
||||
if (!HasBody || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); }
|
||||
|
||||
Vector2 transformedPos = worldPosition;
|
||||
if (Submarine != null) { transformedPos -= Submarine.Position; }
|
||||
@@ -1171,7 +1192,7 @@ namespace Barotrauma
|
||||
bool createWallDamageProjectiles = false)
|
||||
{
|
||||
if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; }
|
||||
if (!Prefab.Body) { return; }
|
||||
if (!HasBody) { return; }
|
||||
if (!MathUtils.IsValid(damage)) { return; }
|
||||
|
||||
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
|
||||
@@ -1213,7 +1234,9 @@ namespace Barotrauma
|
||||
Sections[sectionIndex].gap = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
//do not create gaps on damaged walls in editors,
|
||||
//they're created at the start of a round and "pre-creating" them in the editors causes issues (see #12998)
|
||||
else if (Screen.Selected is not { IsEditor: true })
|
||||
{
|
||||
float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f;
|
||||
if (Sections[sectionIndex].gap == null)
|
||||
@@ -1721,7 +1744,7 @@ namespace Barotrauma
|
||||
//structures with a body drop a shadow by default
|
||||
if (element.GetAttribute(nameof(UseDropShadow)) == null)
|
||||
{
|
||||
s.UseDropShadow = prefab.Body;
|
||||
s.UseDropShadow = s.HasBody;
|
||||
}
|
||||
|
||||
if (element.GetAttribute(nameof(NoAITarget)) == null)
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.PerkBehaviors;
|
||||
using Voronoi2;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -26,6 +27,21 @@ namespace Barotrauma
|
||||
|
||||
public CharacterTeamType TeamID = CharacterTeamType.None;
|
||||
|
||||
public static ImmutableArray<SubItemSwapPerk> GetSubItemSwapPerksFromTeamPerks(ImmutableArray<DisembarkPerkPrefab> teamPerks)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<SubItemSwapPerk>();
|
||||
foreach (DisembarkPerkPrefab prefab in teamPerks)
|
||||
{
|
||||
foreach (var perk in prefab.PerkBehaviors)
|
||||
{
|
||||
if (perk is not SubItemSwapPerk subSwapPerk) { continue; }
|
||||
builder.Add(subSwapPerk);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f);
|
||||
//position of the "actual submarine" which is rendered wherever the SubmarineBody is
|
||||
//should be in an unreachable place
|
||||
@@ -46,6 +62,10 @@ namespace Barotrauma
|
||||
public static readonly Vector2 GridSize = new Vector2(16.0f, 16.0f);
|
||||
|
||||
public static readonly Submarine[] MainSubs = new Submarine[2];
|
||||
|
||||
/// <summary>
|
||||
/// Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
|
||||
/// </summary>
|
||||
public static Submarine MainSub
|
||||
{
|
||||
get { return MainSubs[0]; }
|
||||
@@ -123,6 +143,8 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
public List<WayPoint> ForcedOutpostModuleWayPoints = new List<WayPoint>();
|
||||
|
||||
public static List<Submarine> Loaded
|
||||
{
|
||||
get { return loaded; }
|
||||
@@ -206,6 +228,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the submarine above the upper boundary of the level ("outside bounds", where the submarine shouldn't be able to get to normally)?
|
||||
/// E.g. respawn shuttles are moved above the level when they despawn.
|
||||
/// </summary>
|
||||
public bool IsAboveLevel => Level.IsPositionAboveLevel(WorldPosition);
|
||||
|
||||
public bool AtEndExit
|
||||
{
|
||||
get
|
||||
@@ -324,6 +352,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRespawnShuttle =>
|
||||
GameMain.NetworkMember?.RespawnManager is { } respawnManager && respawnManager.RespawnShuttles.Contains(this);
|
||||
|
||||
private readonly List<WayPoint> exitPoints = new List<WayPoint>();
|
||||
public IReadOnlyList<WayPoint> ExitPoints { get { return exitPoints; } }
|
||||
|
||||
@@ -1143,11 +1174,23 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false)
|
||||
{
|
||||
SetLayerEnabled(layer, enabled, MapEntity.MapEntityList.Where(m => m.Submarine == this));
|
||||
#if SERVER
|
||||
if (sendNetworkEvent)
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void SetLayerEnabled(Identifier layer, bool enabled, IEnumerable<MapEntity> entities)
|
||||
{
|
||||
foreach (MapEntity entity in MapEntity.MapEntityList)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entity.Layer) || entity.Submarine != this || entity.Layer != layer) { continue; }
|
||||
if (string.IsNullOrEmpty(entity.Layer) || entity.Layer != layer) { continue; }
|
||||
entity.IsLayerHidden = !enabled;
|
||||
|
||||
if (entity is WayPoint wp)
|
||||
@@ -1163,34 +1206,37 @@ namespace Barotrauma
|
||||
}
|
||||
else if (entity is Item item)
|
||||
{
|
||||
foreach (var connectionPanel in item.GetComponents<ConnectionPanel>())
|
||||
{
|
||||
foreach (var connection in connectionPanel.Connections)
|
||||
{
|
||||
foreach (var wire in connection.Wires)
|
||||
{
|
||||
wire.Item.IsLayerHidden = entity.IsLayerHidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
if (entity.IsLayerHidden)
|
||||
{
|
||||
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
|
||||
foreach (var lightComponent in item.GetComponents<LightComponent>())
|
||||
{
|
||||
lightComponent.Light.Enabled = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
SetItemHidden(item, entity.IsLayerHidden);
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
if (sendNetworkEvent)
|
||||
|
||||
static void SetItemHidden(Item item, bool isHidden)
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled));
|
||||
}
|
||||
foreach (var containedItem in item.ContainedItems)
|
||||
{
|
||||
SetItemHidden(containedItem, isHidden);
|
||||
}
|
||||
foreach (var connectionPanel in item.GetComponents<ConnectionPanel>())
|
||||
{
|
||||
foreach (var connection in connectionPanel.Connections)
|
||||
{
|
||||
foreach (var wire in connection.Wires)
|
||||
{
|
||||
wire.Item.IsLayerHidden = isHidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
if (isHidden)
|
||||
{
|
||||
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
|
||||
foreach (var lightComponent in item.GetComponents<LightComponent>())
|
||||
{
|
||||
lightComponent.Light.Enabled = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
@@ -1208,7 +1254,7 @@ namespace Barotrauma
|
||||
if (Level.Loaded != null &&
|
||||
WorldPosition.Y < Level.MaxEntityDepth &&
|
||||
subBody.Body.Enabled &&
|
||||
(GameMain.NetworkMember?.RespawnManager == null || this != GameMain.NetworkMember.RespawnManager.RespawnShuttle))
|
||||
!IsRespawnShuttle)
|
||||
{
|
||||
subBody.Body.ResetDynamics();
|
||||
subBody.Body.Enabled = false;
|
||||
@@ -1412,11 +1458,8 @@ namespace Barotrauma
|
||||
foreach (Submarine sub in loaded)
|
||||
{
|
||||
if (ignoreOutposts && sub.Info.IsOutpost) { continue; }
|
||||
if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
|
||||
if (ignoreRespawnShuttle)
|
||||
{
|
||||
if (sub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
|
||||
}
|
||||
if (ignoreOutsideLevel && Level.Loaded != null && sub.IsAboveLevel) { continue; }
|
||||
if (ignoreRespawnShuttle && sub.IsRespawnShuttle) { continue; }
|
||||
if (teamType.HasValue && sub.TeamID != teamType) { continue; }
|
||||
float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition);
|
||||
if (closest == null || dist < closestDist)
|
||||
@@ -1479,7 +1522,14 @@ namespace Barotrauma
|
||||
if (entity.Submarine == null) { return false; }
|
||||
if (includingConnectedSubs)
|
||||
{
|
||||
return GetConnectedSubs().Any(s => s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type));
|
||||
// Performance-sensitive code -> implemented without Linq.
|
||||
foreach (Submarine s in connectedSubs)
|
||||
{
|
||||
if (s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1557,7 +1607,10 @@ namespace Barotrauma
|
||||
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
|
||||
}
|
||||
|
||||
public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
|
||||
public Submarine(SubmarineInfo info,
|
||||
bool showErrorMessages = true,
|
||||
Func<Submarine, List<MapEntity>> loadEntities = null,
|
||||
IdRemap linkedRemap = null) : base(null, NullEntityID)
|
||||
{
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
@@ -1651,6 +1704,10 @@ namespace Barotrauma
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
TeamID = CharacterTeamType.FriendlyNPC;
|
||||
foreach (var dockedSub in DockedTo)
|
||||
{
|
||||
dockedSub.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
|
||||
bool indestructible =
|
||||
GameMain.NetworkMember != null &&
|
||||
@@ -1837,7 +1894,7 @@ namespace Barotrauma
|
||||
|
||||
public bool CheckFuel()
|
||||
{
|
||||
float fuel = GetItems(true).Where(i => i.HasTag(Tags.Fuel)).Sum(i => i.Condition);
|
||||
float fuel = GetItems(true).Where(i => i.HasTag(Tags.ReactorFuel)).Sum(i => i.Condition);
|
||||
Info.LowFuel = fuel < 200;
|
||||
return !Info.LowFuel;
|
||||
}
|
||||
@@ -1859,6 +1916,8 @@ namespace Barotrauma
|
||||
element.Add(new XAttribute("class", Info.SubmarineClass.ToString()));
|
||||
}
|
||||
element.Add(new XAttribute("tags", Info.Tags.ToString()));
|
||||
element.Add(new XAttribute("outposttags", Info.OutpostTags.ConvertToString()));
|
||||
element.Add(new XAttribute("triggeroutpostmissionevents", Info.TriggerOutpostMissionEvents.ConvertToString()));
|
||||
element.Add(new XAttribute("gameversion", GameMain.Version.ToString()));
|
||||
|
||||
Rectangle dimensions = VisibleBorders;
|
||||
@@ -1887,8 +1946,8 @@ namespace Barotrauma
|
||||
{
|
||||
bool hasThalamus = false;
|
||||
|
||||
var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity).ToImmutableHashSet();
|
||||
var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct().ToImmutableHashSet();
|
||||
var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity);
|
||||
var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct();
|
||||
|
||||
foreach (ItemPrefab prefab in prefabsOnSub)
|
||||
{
|
||||
@@ -1917,35 +1976,14 @@ namespace Barotrauma
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap is not { } connectedItemsToSwap) { continue; }
|
||||
foreach (var (requiredTag, swapTo) in connectedItemsToSwap)
|
||||
if (item.PendingItemSwap?.SwappableItem == null) { continue; }
|
||||
var connectedItemsToSwap = item.GetConnectedItemsToSwap(item.PendingItemSwap.SwappableItem);
|
||||
foreach (var kvp in connectedItemsToSwap)
|
||||
{
|
||||
List<Item> itemsToSwap = new List<Item>();
|
||||
itemsToSwap.AddRange(item.linkedTo.Where(lt => (lt as Item)?.HasTag(requiredTag) ?? false).Cast<Item>());
|
||||
if (item.GetComponent<ConnectionPanel>() is ConnectionPanel connectionPanel)
|
||||
{
|
||||
foreach (Connection c in connectionPanel.Connections)
|
||||
{
|
||||
foreach (var connectedComponent in item.GetConnectedComponentsRecursive<ItemComponent>(c))
|
||||
{
|
||||
if (!itemsToSwap.Contains(connectedComponent.Item) && connectedComponent.Item.HasTag(requiredTag))
|
||||
{
|
||||
itemsToSwap.Add(connectedComponent.Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ItemPrefab itemPrefab = ItemPrefab.Find("", swapTo);
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to swap an item connected to \"{item.Name}\" into \"{swapTo}\".");
|
||||
continue;
|
||||
}
|
||||
foreach (Item itemToSwap in itemsToSwap)
|
||||
{
|
||||
itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap;
|
||||
if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; }
|
||||
}
|
||||
Item itemToSwap = kvp.Key;
|
||||
ItemPrefab swapTo = kvp.Value;
|
||||
itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap;
|
||||
if (itemToSwap.Prefab != swapTo) { itemToSwap.PendingItemSwap = swapTo; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2004,6 +2042,7 @@ namespace Barotrauma
|
||||
FilePath = filePath,
|
||||
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
|
||||
BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null,
|
||||
EnemySubmarineInfo = Info.EnemySubmarineInfo != null ? new EnemySubmarineInfo(Info.EnemySubmarineInfo) : null,
|
||||
WreckInfo = Info.WreckInfo != null ? new WreckInfo(Info.WreckInfo) : null,
|
||||
Name = Path.GetFileNameWithoutExtension(filePath)
|
||||
};
|
||||
@@ -2046,7 +2085,6 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
RoundSound.RemoveAllRoundSounds();
|
||||
GameMain.LightManager?.ClearLights();
|
||||
depthSortedDamageable.Clear();
|
||||
#endif
|
||||
var _loaded = new List<Submarine>(loaded);
|
||||
foreach (Submarine sub in _loaded)
|
||||
|
||||
@@ -504,10 +504,8 @@ namespace Barotrauma
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//character inside some sub, no need to displace
|
||||
if (c.Submarine != null) { continue; }
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
@@ -517,7 +515,7 @@ namespace Barotrauma
|
||||
|
||||
//cast a line from the position of the character to the same direction as the translation of the sub
|
||||
//and see where it intersects with the bounding box
|
||||
if (!MathUtils.GetLineRectangleIntersection(limb.WorldPosition,
|
||||
if (!MathUtils.GetLineWorldRectangleIntersection(limb.WorldPosition,
|
||||
limb.WorldPosition + translateDir * 100000.0f, worldBorders, out Vector2 intersection))
|
||||
{
|
||||
//should never happen when casting a line out from inside the bounding box
|
||||
@@ -525,13 +523,11 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//"+ translatedir" in order to move the character slightly away from the wall
|
||||
c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir);
|
||||
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,9 +581,7 @@ namespace Barotrauma
|
||||
|
||||
private void UpdateDepthDamage(float deltaTime)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
|
||||
#endif
|
||||
if (Level.Loaded == null) { return; }
|
||||
|
||||
//camera shake and sounds start playing 500 meters before crush depth
|
||||
@@ -598,7 +592,7 @@ namespace Barotrauma
|
||||
const float MaxWallDamageProbability = 1.0f;
|
||||
const float MinWallDamage = 50f;
|
||||
const float MaxWallDamage = 500.0f;
|
||||
const float MinCameraShake = 5f;
|
||||
const float MinCameraShake = 10f;
|
||||
const float MaxCameraShake = 50.0f;
|
||||
//delay at the start of the round during which you take no depth damage
|
||||
//(gives you a bit of time to react and return if you start the round in a level that's too deep)
|
||||
@@ -612,8 +606,11 @@ namespace Barotrauma
|
||||
damageSoundTimer -= deltaTime;
|
||||
if (damageSoundTimer <= 0.0f)
|
||||
{
|
||||
const float PressureSoundRange = -CosmeticEffectThreshold;
|
||||
//Ratio between 0 (where the 'approaching crush depth' indication starts) and 1 (at crush depth or past it)
|
||||
float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)) / PressureSoundRange, 0f, 1f);
|
||||
#if CLIENT
|
||||
SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f);
|
||||
SoundPlayer.PlayDamageSound("pressure", MathHelper.Lerp(0f, 100f, closenessToCrushDepthRatio), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f, gain: 1f + closenessToCrushDepthRatio * 2);
|
||||
#endif
|
||||
damageSoundTimer = Rand.Range(5.0f, 10.0f);
|
||||
}
|
||||
@@ -919,6 +916,13 @@ namespace Barotrauma
|
||||
{
|
||||
Debug.Assert(otherSub != submarine);
|
||||
|
||||
//submarine outside the level (despawned respawn shuttle?)
|
||||
//no need to apply impacts between colliding subs
|
||||
if (submarine.IsAboveLevel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 normal = impact.Normal;
|
||||
if (impact.Target.Body == otherSub.SubBody.Body.FarseerBody)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -123,16 +123,30 @@ namespace Barotrauma
|
||||
public OutpostModuleInfo OutpostModuleInfo { get; set; }
|
||||
public BeaconStationInfo BeaconStationInfo { get; set; }
|
||||
public WreckInfo WreckInfo { get; set; }
|
||||
public EnemySubmarineInfo EnemySubmarineInfo { get; set; }
|
||||
|
||||
public ExtraSubmarineInfo GetExtraSubmarineInfo => BeaconStationInfo ?? WreckInfo as ExtraSubmarineInfo;
|
||||
|
||||
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
|
||||
public ImmutableHashSet<Identifier> OutpostTags { get; set; } = ImmutableHashSet<Identifier>.Empty;
|
||||
|
||||
public ImmutableHashSet<Identifier> TriggerOutpostMissionEvents { get; set; } = ImmutableHashSet<Identifier>.Empty;
|
||||
|
||||
public bool IsOutpost => Type is SubmarineType.Outpost or SubmarineType.OutpostModule;
|
||||
|
||||
public bool IsWreck => Type == SubmarineType.Wreck;
|
||||
public bool IsBeacon => Type == SubmarineType.BeaconStation;
|
||||
public bool IsEnemySubmarine => Type == SubmarineType.EnemySubmarine;
|
||||
public bool IsPlayer => Type == SubmarineType.Player;
|
||||
public bool IsRuin => Type == SubmarineType.Ruin;
|
||||
|
||||
/// <summary>
|
||||
/// Ruin modules are of type SubmarineType.OutpostModule, until the ruin generator (or the test game mode) sets them as ruins.
|
||||
/// This is a helper workaround check intended to be used only in the context of the sub editor and the test game mode, where ruins aren't generated.
|
||||
/// </summary>
|
||||
public bool ShouldBeRuin =>
|
||||
Type is SubmarineType.Ruin or SubmarineType.OutpostModule &&
|
||||
OutpostModuleInfo.ModuleFlags.Any(f => f.StartsWith("ruin"));
|
||||
|
||||
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
|
||||
public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);
|
||||
|
||||
@@ -325,6 +339,8 @@ namespace Barotrauma
|
||||
Tags = original.Tags;
|
||||
OutpostGenerationParams = original.OutpostGenerationParams;
|
||||
LayersHiddenByDefault = original.LayersHiddenByDefault;
|
||||
OutpostTags = original.OutpostTags;
|
||||
TriggerOutpostMissionEvents = original.TriggerOutpostMissionEvents;
|
||||
if (original.OutpostModuleInfo != null)
|
||||
{
|
||||
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
|
||||
@@ -333,6 +349,10 @@ namespace Barotrauma
|
||||
{
|
||||
BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo);
|
||||
}
|
||||
else if (original.EnemySubmarineInfo != null)
|
||||
{
|
||||
EnemySubmarineInfo = new EnemySubmarineInfo(original.EnemySubmarineInfo);
|
||||
}
|
||||
else if (original.WreckInfo != null)
|
||||
{
|
||||
WreckInfo = new WreckInfo(original.WreckInfo);
|
||||
@@ -416,6 +436,16 @@ namespace Barotrauma
|
||||
}
|
||||
Tier = SubmarineElement.GetAttributeInt("tier", GetDefaultTier(Price));
|
||||
|
||||
OutpostTags = SubmarineElement.GetAttributeIdentifierImmutableHashSet(nameof(OutpostTags), ImmutableHashSet<Identifier>.Empty);
|
||||
|
||||
TriggerOutpostMissionEvents = SubmarineElement.GetAttributeIdentifierImmutableHashSet(nameof(TriggerOutpostMissionEvents), ImmutableHashSet<Identifier>.Empty);
|
||||
//backwards compatibility: previously the outpost deathmatch mission always triggered an event with the tag "deathmatchweapondrop"
|
||||
//now that's configured in the outpost itself, so let's make older outposts trigger it automatically
|
||||
if (GameVersion < new Version(1, 8, 0, 0) && OutpostTags.Contains("PvPOutpost"))
|
||||
{
|
||||
TriggerOutpostMissionEvents = TriggerOutpostMissionEvents.Add("deathmatchweapondrop".ToIdentifier());
|
||||
}
|
||||
|
||||
if (SubmarineElement?.Attribute("type") != null)
|
||||
{
|
||||
if (Enum.TryParse(SubmarineElement.GetAttributeString("type", ""), out SubmarineType type))
|
||||
@@ -429,6 +459,10 @@ namespace Barotrauma
|
||||
{
|
||||
BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement);
|
||||
}
|
||||
else if (Type == SubmarineType.EnemySubmarine)
|
||||
{
|
||||
EnemySubmarineInfo = new EnemySubmarineInfo(this, SubmarineElement);
|
||||
}
|
||||
else if (Type == SubmarineType.Wreck)
|
||||
{
|
||||
WreckInfo = new WreckInfo(this, SubmarineElement);
|
||||
@@ -612,6 +646,11 @@ namespace Barotrauma
|
||||
BeaconStationInfo.Save(newElement);
|
||||
BeaconStationInfo = new BeaconStationInfo(this, newElement);
|
||||
}
|
||||
else if (Type == SubmarineType.EnemySubmarine)
|
||||
{
|
||||
EnemySubmarineInfo.Save(newElement);
|
||||
EnemySubmarineInfo = new EnemySubmarineInfo(this, newElement);
|
||||
}
|
||||
else if (Type == SubmarineType.Wreck)
|
||||
{
|
||||
WreckInfo.Save(newElement);
|
||||
|
||||
@@ -73,6 +73,8 @@ namespace Barotrauma
|
||||
public Level.Tunnel Tunnel;
|
||||
public RuinGeneration.Ruin Ruin;
|
||||
|
||||
public Level.Cave Cave;
|
||||
|
||||
public SpawnType SpawnType
|
||||
{
|
||||
get { return spawnType; }
|
||||
@@ -226,7 +228,7 @@ namespace Barotrauma
|
||||
door.Body.Enabled = true;
|
||||
}
|
||||
}
|
||||
bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier());
|
||||
bool isRuin = submarine.Info.ShouldBeRuin;
|
||||
float diffFromHullEdge = 50;
|
||||
float minDist = 100.0f;
|
||||
float heightFromFloor = 110.0f;
|
||||
@@ -235,7 +237,7 @@ namespace Barotrauma
|
||||
var removals = new HashSet<WayPoint>();
|
||||
foreach (Hull hull in Hull.HullList)
|
||||
{
|
||||
if (isFlooded)
|
||||
if (isRuin)
|
||||
{
|
||||
diffFromHullEdge = 75;
|
||||
var hullWaypoints = new List<WayPoint>();
|
||||
@@ -405,7 +407,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float outSideWaypointInterval = 100.0f;
|
||||
if (!isFlooded && submarine.Info.Type != SubmarineType.OutpostModule)
|
||||
if (!isRuin && submarine.Info.Type != SubmarineType.OutpostModule)
|
||||
{
|
||||
List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>();
|
||||
|
||||
@@ -732,7 +734,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (gap.IsHorizontal)
|
||||
{
|
||||
if ( isFlooded)
|
||||
if ( isRuin)
|
||||
{
|
||||
// Too small to swim through
|
||||
if (gap.Rect.Height < 50) { continue; }
|
||||
@@ -744,13 +746,13 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
|
||||
if (isFlooded)
|
||||
if (isRuin)
|
||||
{
|
||||
pos.Y = gap.Rect.Y - gap.Rect.Height / 2;
|
||||
}
|
||||
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 && !isFlooded ? new Vector2(150, 70) : new Vector2(1000, 1000);
|
||||
Vector2 tolerance = gap.IsRoomToRoom && !isRuin ? 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);
|
||||
@@ -763,7 +765,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// Create waypoints on vertical gaps on the outer walls, also hatches.
|
||||
if (!isFlooded && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; }
|
||||
if (!isRuin && (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);
|
||||
@@ -772,14 +774,14 @@ namespace Barotrauma
|
||||
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, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100));
|
||||
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isRuin ? new Vector2(500, 500) : new Vector2(50, 100));
|
||||
if (closest != null)
|
||||
{
|
||||
wayPoint.ConnectTo(closest);
|
||||
}
|
||||
if (isFlooded)
|
||||
if (isRuin)
|
||||
{
|
||||
closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100));
|
||||
closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isRuin ? new Vector2(500, 500) : new Vector2(50, 100));
|
||||
if (closest != null)
|
||||
{
|
||||
wayPoint.ConnectTo(closest);
|
||||
@@ -944,7 +946,13 @@ namespace Barotrauma
|
||||
public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
|
||||
{
|
||||
List<WayPoint> subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine);
|
||||
subWayPoints.Shuffle();
|
||||
if (submarine.ForcedOutpostModuleWayPoints != null && submarine.ForcedOutpostModuleWayPoints.Any())
|
||||
{
|
||||
// narrow selection of spawn points to within the module
|
||||
subWayPoints = new List<WayPoint>(submarine.ForcedOutpostModuleWayPoints);
|
||||
submarine.ForcedOutpostModuleWayPoints.Clear();
|
||||
}
|
||||
subWayPoints.Shuffle(Rand.RandSync.Unsynced);
|
||||
|
||||
List<WayPoint> unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human);
|
||||
|
||||
@@ -1003,6 +1011,54 @@ namespace Barotrauma
|
||||
return assignedWayPoints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find appropriate spawnpoints in the outpost for the player crew (preferring job-specific spawnpoints in the initial/airlock module normally, and job and team -specific spawnpoints in the PvP mode).
|
||||
/// </summary>
|
||||
public static WayPoint[] SelectOutpostSpawnPoints(List<CharacterInfo> crew, CharacterTeamType teamID)
|
||||
{
|
||||
List<WayPoint> potentialSpawnPoints = WayPointList.FindAll(wp =>
|
||||
wp.SpawnType == SpawnType.Human &&
|
||||
wp.Submarine == Level.Loaded.StartOutpost);
|
||||
if (GameMain.GameSession.GameMode is PvPMode)
|
||||
{
|
||||
Identifier teamSpawnTag = ("deathmatch" + teamID).ToIdentifier();
|
||||
if (potentialSpawnPoints.Any(wp => wp.Tags.Contains(teamSpawnTag)))
|
||||
{
|
||||
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.Tags.Contains(teamSpawnTag));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp =>
|
||||
wp.CurrentHull?.OutpostModuleTags != null &&
|
||||
wp.CurrentHull.OutpostModuleTags.Contains(Barotrauma.Tags.Airlock));
|
||||
}
|
||||
if (potentialSpawnPoints.None()) { return potentialSpawnPoints.ToArray(); }
|
||||
|
||||
List<WayPoint> spawnPoints = new List<WayPoint>();
|
||||
for (int i = 0; i < crew.Count; i++)
|
||||
{
|
||||
var spawnPointsForJob = potentialSpawnPoints.Where(wp => wp.AssignedJob == crew[i].Job.Prefab);
|
||||
var spawnPointsForAnyJob = potentialSpawnPoints.Where(wp => wp.AssignedJob == null);
|
||||
if (spawnPointsForJob.Any())
|
||||
{
|
||||
//prefer job-specific spawnpoints
|
||||
spawnPoints.Add(spawnPointsForJob.GetRandomUnsynced());
|
||||
}
|
||||
else if (spawnPointsForAnyJob.Any())
|
||||
{
|
||||
//2nd option: a non-job-specific spawnpoint
|
||||
spawnPoints.Add(spawnPointsForAnyJob.GetRandomUnsynced());
|
||||
}
|
||||
else
|
||||
{
|
||||
//last option: whatever spawnpoint (no matter if it's not for this job)
|
||||
spawnPoints.Add(potentialSpawnPoints.GetRandomUnsynced());
|
||||
}
|
||||
}
|
||||
return spawnPoints.ToArray();
|
||||
}
|
||||
|
||||
public void FindHull()
|
||||
{
|
||||
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
|
||||
|
||||
Reference in New Issue
Block a user