Build 1.1.4.0
This commit is contained in:
@@ -36,6 +36,10 @@ namespace Barotrauma
|
||||
|
||||
public bool IdFreed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique, but non-persistent identifier.
|
||||
/// Stays the same if the entities are created in the exactly same order, but doesn't persist e.g. between the rounds.
|
||||
/// </summary>
|
||||
public readonly ushort ID;
|
||||
|
||||
public virtual Vector2 SimPosition => Vector2.Zero;
|
||||
|
||||
@@ -127,6 +127,7 @@ namespace Barotrauma
|
||||
hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false);
|
||||
}
|
||||
|
||||
Attack.DamageMultiplier = 1.0f;
|
||||
float displayRange = Attack.Range;
|
||||
if (damageSource is Item sourceItem)
|
||||
{
|
||||
@@ -157,6 +158,10 @@ 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)
|
||||
{
|
||||
item.GetComponent<Sonar>()?.RegisterExplosion(this, worldPosition);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (displayRange < 0.1f) { return; }
|
||||
@@ -201,7 +206,7 @@ namespace Barotrauma
|
||||
powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor;
|
||||
}
|
||||
}
|
||||
static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
|
||||
static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - MathF.Sqrt(distSqr) / displayRange;
|
||||
}
|
||||
|
||||
if (itemRepairStrength > 0.0f)
|
||||
@@ -210,7 +215,7 @@ namespace Barotrauma
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
|
||||
if (distSqr > displayRangeSqr) continue;
|
||||
if (distSqr > displayRangeSqr) { continue; }
|
||||
|
||||
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
|
||||
//repair repairable items
|
||||
@@ -266,7 +271,7 @@ namespace Barotrauma
|
||||
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
|
||||
{
|
||||
float distFactor = 1.0f - dist / displayRange;
|
||||
float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
|
||||
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
|
||||
|
||||
Vector2 explosionPos = worldPosition;
|
||||
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
|
||||
@@ -354,7 +359,7 @@ namespace Barotrauma
|
||||
if (affliction.DivideByLimbCount)
|
||||
{
|
||||
float limbCountFactor = distFactors.Count;
|
||||
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage")
|
||||
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType)
|
||||
{
|
||||
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
|
||||
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
|
||||
@@ -396,9 +401,12 @@ namespace Barotrauma
|
||||
if (attack.StatusEffects != null && attack.StatusEffects.Any())
|
||||
{
|
||||
attack.SetUser(attacker);
|
||||
var statusEffectTargets = new List<ISerializableEntity>() { c, limb };
|
||||
var statusEffectTargets = new List<ISerializableEntity>();
|
||||
foreach (StatusEffect statusEffect in attack.StatusEffects)
|
||||
{
|
||||
statusEffectTargets.Clear();
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffectTargets.Add(c); }
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) { statusEffectTargets.Add(limb); }
|
||||
statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets);
|
||||
statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets);
|
||||
statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets);
|
||||
|
||||
@@ -4,11 +4,12 @@ using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Gap : MapEntity
|
||||
partial class Gap : MapEntity, ISerializableEntity
|
||||
{
|
||||
public static List<Gap> GapList = new List<Gap>();
|
||||
|
||||
@@ -31,6 +32,8 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool IsDiagonal { get; }
|
||||
|
||||
public readonly float GlowEffectT;
|
||||
|
||||
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
|
||||
private float open;
|
||||
|
||||
@@ -51,7 +54,6 @@ namespace Barotrauma
|
||||
//can ambient light get through the gap even if it's not open
|
||||
public bool PassAmbientLight;
|
||||
|
||||
|
||||
//a collider outside the gap (for example an ice wall next to the sub)
|
||||
//used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub
|
||||
private Body outsideCollisionBlocker;
|
||||
@@ -63,8 +65,43 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (float.IsNaN(value)) { return; }
|
||||
if (value > open) { openedTimer = 1.0f; }
|
||||
if (value > open)
|
||||
{
|
||||
openedTimer = 1.0f;
|
||||
}
|
||||
if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull))
|
||||
{
|
||||
if (value > open && value >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: true);
|
||||
}
|
||||
else if (value < open && open >= 1.0f)
|
||||
{
|
||||
InformWaypointsAboutGapState(this, open: false);
|
||||
}
|
||||
}
|
||||
open = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
|
||||
static void InformWaypointsAboutGapState(Gap gap, bool open)
|
||||
{
|
||||
foreach (var wp in WayPoint.WayPointList)
|
||||
{
|
||||
if (IsWaypointRightAboveGap(gap, wp))
|
||||
{
|
||||
wp.OnGapStateChanged(open, gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp)
|
||||
{
|
||||
if (wp.SpawnType != SpawnType.Path) { return false; }
|
||||
if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; }
|
||||
if (wp.Position.Y < gap.Rect.Top) { return false; }
|
||||
if (wp.Position.X > gap.Rect.Right) { return false; }
|
||||
if (wp.Position.X < gap.Rect.Left) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +155,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name
|
||||
public override string Name => "Gap";
|
||||
|
||||
public readonly Dictionary<Identifier, SerializableProperty> properties;
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Gap";
|
||||
}
|
||||
get { return properties; }
|
||||
}
|
||||
|
||||
public Gap(Rectangle rectangle)
|
||||
@@ -150,10 +187,14 @@ namespace Barotrauma
|
||||
IsDiagonal = isDiagonal;
|
||||
open = 1.0f;
|
||||
|
||||
properties = SerializableProperty.GetProperties(this);
|
||||
|
||||
FindHulls();
|
||||
GapList.Add(this);
|
||||
InsertToList();
|
||||
|
||||
GlowEffectT = Rand.Range(0.0f, 1.0f);
|
||||
|
||||
float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2;
|
||||
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize,
|
||||
BodyType.Static,
|
||||
@@ -272,30 +313,51 @@ namespace Barotrauma
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false);
|
||||
if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true);
|
||||
if (hulls[i] == null) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); }
|
||||
}
|
||||
|
||||
if (hulls[0] == null && hulls[1] == null) { return; }
|
||||
if (hulls[0] != null || hulls[1] != null)
|
||||
{
|
||||
if (hulls[0] == null && hulls[1] != null)
|
||||
{
|
||||
(hulls[1], hulls[0]) = (hulls[0], hulls[1]);
|
||||
}
|
||||
|
||||
if (hulls[0] == null && hulls[1] != null)
|
||||
{
|
||||
Hull temp = hulls[0];
|
||||
hulls[0] = hulls[1];
|
||||
hulls[1] = temp;
|
||||
flowTargetHull = hulls[0];
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (hulls[i] == null) { continue; }
|
||||
linkedTo.Add(hulls[i]);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); }
|
||||
}
|
||||
}
|
||||
|
||||
flowTargetHull = hulls[0];
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (hulls[i] == null) { continue; }
|
||||
linkedTo.Add(hulls[i]);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this);
|
||||
}
|
||||
RefreshOutsideCollider();
|
||||
}
|
||||
|
||||
private int updateCount;
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
int updateInterval = 4;
|
||||
float flowMagnitude = flowForce.LengthSquared();
|
||||
if (flowMagnitude < 1.0f)
|
||||
{
|
||||
//very sparse updates if there's practically no water moving
|
||||
updateInterval = 8;
|
||||
}
|
||||
else if (linkedTo.Count == 2 && flowMagnitude > 10.0f)
|
||||
{
|
||||
//frequent updates if water is moving between hulls
|
||||
updateInterval = 1;
|
||||
}
|
||||
|
||||
updateCount++;
|
||||
if (updateCount < updateInterval) { return; }
|
||||
deltaTime *= updateCount;
|
||||
updateCount = 0;
|
||||
|
||||
flowForce = Vector2.Zero;
|
||||
|
||||
outsideColliderRaycastTimer -= deltaTime;
|
||||
@@ -593,7 +655,12 @@ namespace Barotrauma
|
||||
|
||||
public bool RefreshOutsideCollider()
|
||||
{
|
||||
if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false;
|
||||
if (outsideCollisionBlocker == null) { return false; }
|
||||
if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull)
|
||||
{
|
||||
outsideCollisionBlocker.Enabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outsideColliderRaycastTimer <= 0.0f)
|
||||
{
|
||||
@@ -740,8 +807,7 @@ namespace Barotrauma
|
||||
|
||||
public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
|
||||
{
|
||||
Rectangle rect = Rectangle.Empty;
|
||||
|
||||
Rectangle rect;
|
||||
if (element.GetAttribute("rect") != null)
|
||||
{
|
||||
rect = element.GetAttributeRect("rect", Rectangle.Empty);
|
||||
|
||||
@@ -1083,7 +1083,7 @@ namespace Barotrauma
|
||||
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
|
||||
{
|
||||
//gap blocked if the door is not open or the predicted state is not open
|
||||
if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
|
||||
if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
|
||||
{
|
||||
if (g.ConnectedDoor.OpenState < 0.1f)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
Vector2 WorldPosition { get; }
|
||||
float Health { get; }
|
||||
|
||||
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true);
|
||||
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true);
|
||||
|
||||
|
||||
public readonly struct AttackEventData
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -13,11 +14,14 @@ namespace Barotrauma
|
||||
public readonly LocalizedString Description;
|
||||
|
||||
public readonly bool IsEndBiome;
|
||||
public readonly int EndBiomeLocationCount;
|
||||
|
||||
public readonly float MinDifficulty;
|
||||
private readonly float maxDifficulty;
|
||||
public float ActualMaxDifficulty => maxDifficulty;
|
||||
public float AdjustedMaxDifficulty => maxDifficulty - 0.1f;
|
||||
|
||||
|
||||
public readonly ImmutableHashSet<int> AllowedZones;
|
||||
|
||||
private readonly SubmarineAvailability? submarineAvailability;
|
||||
@@ -41,6 +45,8 @@ namespace Barotrauma
|
||||
element.GetAttributeString("description", ""));
|
||||
|
||||
IsEndBiome = element.GetAttributeBool("endbiome", false);
|
||||
EndBiomeLocationCount = Math.Max(1, element.GetAttributeInt("endbiomelocationcount", 1));
|
||||
|
||||
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);
|
||||
|
||||
@@ -65,28 +65,28 @@ namespace Barotrauma
|
||||
set { maxHeight = Math.Max(value, minHeight); }
|
||||
}
|
||||
|
||||
[Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
[Serialize(2, IsPropertySaveable.Yes, description: "Minimum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
public int MinBranchCount
|
||||
{
|
||||
get { return minBranchCount; }
|
||||
set { minBranchCount = Math.Max(value, 0); }
|
||||
}
|
||||
|
||||
[Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
[Serialize(4, IsPropertySaveable.Yes, description: "Maximum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
public int MaxBranchCount
|
||||
{
|
||||
get { return maxBranchCount; }
|
||||
set { maxBranchCount = Math.Max(value, minBranchCount); }
|
||||
}
|
||||
|
||||
[Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)]
|
||||
[Serialize(50, IsPropertySaveable.Yes, description: "Total amount of level objects in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10000)]
|
||||
public int LevelObjectAmount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )]
|
||||
[Serialize(0.1f, IsPropertySaveable.Yes, description: "What portion of the empty cells in the cave should be turned into destructible walls? For example, 0.1 = 10%."), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )]
|
||||
public float DestructibleWallRatio
|
||||
{
|
||||
get;
|
||||
|
||||
@@ -16,6 +16,11 @@ namespace Barotrauma
|
||||
{
|
||||
partial class Level : Entity, IServerSerializable
|
||||
{
|
||||
public enum PlacementType
|
||||
{
|
||||
Top, Bottom
|
||||
}
|
||||
|
||||
public enum EventType
|
||||
{
|
||||
SingleDestructibleWall,
|
||||
@@ -61,6 +66,7 @@ namespace Barotrauma
|
||||
[Flags]
|
||||
public enum PositionType
|
||||
{
|
||||
None = 0,
|
||||
MainPath = 0x1,
|
||||
SidePath = 0x2,
|
||||
Cave = 0x4,
|
||||
@@ -68,7 +74,8 @@ namespace Barotrauma
|
||||
Wreck = 0x10,
|
||||
BeaconStation = 0x20,
|
||||
Abyss = 0x40,
|
||||
AbyssCave = 0x80
|
||||
AbyssCave = 0x80,
|
||||
Outpost = 0x100,
|
||||
}
|
||||
|
||||
public struct InterestingPosition
|
||||
@@ -413,6 +420,9 @@ namespace Barotrauma
|
||||
get { return LevelData.Type; }
|
||||
}
|
||||
|
||||
|
||||
public bool IsEndBiome => LevelData.Biome != null && LevelData.Biome.IsEndBiome;
|
||||
|
||||
/// <summary>
|
||||
/// Is there a loaded level set and is it an outpost?
|
||||
/// </summary>
|
||||
@@ -451,6 +461,19 @@ namespace Barotrauma
|
||||
borders = new Rectangle(Point.Zero, levelData.Size);
|
||||
}
|
||||
|
||||
public bool ShouldSpawnCrewInsideOutpost()
|
||||
{
|
||||
if (StartOutpost != null &&
|
||||
Type == LevelData.LevelType.Outpost &&
|
||||
(StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) &&
|
||||
StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player))
|
||||
{
|
||||
var reputation = GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation;
|
||||
return reputation == null || reputation.NormalizedValue >= Reputation.HostileThreshold;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
|
||||
{
|
||||
Debug.Assert(levelData.Biome != null);
|
||||
@@ -482,11 +505,8 @@ namespace Barotrauma
|
||||
EntitiesBeforeGenerate = GetEntities().ToList();
|
||||
EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count();
|
||||
|
||||
if (LevelData.ForceOutpostGenerationParams == null)
|
||||
{
|
||||
StartLocation = startLocation;
|
||||
EndLocation = endLocation;
|
||||
}
|
||||
StartLocation = startLocation;
|
||||
EndLocation = endLocation;
|
||||
|
||||
GenerateEqualityCheckValue(LevelGenStage.GenStart);
|
||||
SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier));
|
||||
@@ -889,6 +909,12 @@ namespace Barotrauma
|
||||
// remove unnecessary cells and create some holes at the bottom of the level
|
||||
//----------------------------------------------------------------------------------
|
||||
|
||||
if (GenerationParams.NoLevelGeometry)
|
||||
{
|
||||
cells.ForEach(c => c.CellType = CellType.Removed);
|
||||
cells.Clear();
|
||||
}
|
||||
|
||||
cells = cells.Except(pathCells).ToList();
|
||||
//remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad
|
||||
cells.ForEachMod(c =>
|
||||
@@ -1687,14 +1713,22 @@ namespace Barotrauma
|
||||
foreach (VoronoiCell cell in closeCells)
|
||||
{
|
||||
bool tooClose = false;
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
|
||||
Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
|
||||
MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
|
||||
|
||||
if (cell.IsPointInsideAABB(position, margin: minDistance))
|
||||
{
|
||||
tooClose = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
tooClose = true;
|
||||
break;
|
||||
if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
|
||||
Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
|
||||
MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
|
||||
{
|
||||
tooClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tooClose) { tooCloseCells.Add(cell); }
|
||||
@@ -1779,6 +1813,7 @@ namespace Barotrauma
|
||||
|
||||
if (AbyssArea.Height < islandSize.Y) { return; }
|
||||
|
||||
int createdCaves = 0;
|
||||
int islandCount = GenerationParams.AbyssIslandCount;
|
||||
for (int i = 0; i < islandCount; i++)
|
||||
{
|
||||
@@ -1808,7 +1843,11 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
|
||||
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability)
|
||||
bool createCave =
|
||||
//force at least one abyss cave
|
||||
(i == islandCount - 1 && createdCaves == 0) ||
|
||||
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability;
|
||||
if (!createCave)
|
||||
{
|
||||
float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f;
|
||||
var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance);
|
||||
@@ -1867,6 +1906,7 @@ namespace Barotrauma
|
||||
new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2),
|
||||
new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland)));
|
||||
AbyssIslands.Add(new AbyssIsland(islandArea, islandCells));
|
||||
createdCaves++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2942,13 +2982,13 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <param name="rotation">Used by clients to set the rotation for the resources</param>
|
||||
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable<Cave> targetCaves = null)
|
||||
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable<Cave> targetCaves = null)
|
||||
{
|
||||
var allValidLocations = GetAllValidClusterLocations();
|
||||
var placedResources = new List<Item>();
|
||||
rotation = 0.0f;
|
||||
|
||||
if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?!
|
||||
// if there are no valid locations, don't place anything
|
||||
if (allValidLocations.None()) { return placedResources; }
|
||||
|
||||
// Make sure not to pick a spot that already has other level resources
|
||||
for (int i = allValidLocations.Count - 1; i >= 0; i--)
|
||||
@@ -2979,10 +3019,12 @@ namespace Barotrauma
|
||||
|
||||
if (PositionsOfInterest.None(p => p.PositionType == positionType))
|
||||
{
|
||||
DebugConsole.AddWarning($"Failed to find a position of the type \"{positionType}\" for mission resources.");
|
||||
foreach (var validType in MineralMission.ValidPositionTypes)
|
||||
{
|
||||
if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType))
|
||||
{
|
||||
DebugConsole.AddWarning($"Placing in \"{validType}\" instead.");
|
||||
positionType = validType;
|
||||
break;
|
||||
}
|
||||
@@ -3042,7 +3084,6 @@ namespace Barotrauma
|
||||
}
|
||||
PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources);
|
||||
Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell);
|
||||
rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
|
||||
return placedResources;
|
||||
|
||||
static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath;
|
||||
@@ -3151,9 +3192,7 @@ namespace Barotrauma
|
||||
if (item.GetComponent<Holdable>() is Holdable h)
|
||||
{
|
||||
h.AttachToWall();
|
||||
#if CLIENT
|
||||
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
|
||||
#endif
|
||||
}
|
||||
else if (item.body != null)
|
||||
{
|
||||
@@ -3234,7 +3273,8 @@ namespace Barotrauma
|
||||
{
|
||||
suitablePositions.RemoveAll(p => !filter(p));
|
||||
}
|
||||
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
|
||||
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) ||
|
||||
positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave))
|
||||
{
|
||||
suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2()));
|
||||
}
|
||||
@@ -3399,8 +3439,7 @@ namespace Barotrauma
|
||||
bool closeEnough = false;
|
||||
foreach (VoronoiCell cell in wall.Cells)
|
||||
{
|
||||
if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize &&
|
||||
Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize)
|
||||
if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2))
|
||||
{
|
||||
closeEnough = true;
|
||||
break;
|
||||
@@ -3553,6 +3592,13 @@ namespace Barotrauma
|
||||
|
||||
var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
|
||||
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
|
||||
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value)
|
||||
{
|
||||
Type = type
|
||||
};
|
||||
|
||||
//place downwards by default
|
||||
var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom;
|
||||
|
||||
// Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
|
||||
Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000);
|
||||
@@ -3573,7 +3619,7 @@ namespace Barotrauma
|
||||
attemptsLeft--;
|
||||
if (TryGetSpawnPoint(out spawnPoint))
|
||||
{
|
||||
success = TryPositionSub(subBorders, subName, ref spawnPoint);
|
||||
success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
|
||||
if (success)
|
||||
{
|
||||
break;
|
||||
@@ -3594,10 +3640,6 @@ namespace Barotrauma
|
||||
{
|
||||
Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
|
||||
tempSW.Restart();
|
||||
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value)
|
||||
{
|
||||
Type = type
|
||||
};
|
||||
Submarine sub = new Submarine(info);
|
||||
if (type == SubmarineType.Wreck)
|
||||
{
|
||||
@@ -3647,10 +3689,10 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
bool TryPositionSub(Rectangle subBorders, string subName, ref Vector2 spawnPoint)
|
||||
{
|
||||
bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint)
|
||||
{
|
||||
positions.Add(spawnPoint);
|
||||
bool bottomFound = TryRaycastToBottom(subBorders, ref spawnPoint);
|
||||
bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint);
|
||||
positions.Add(spawnPoint);
|
||||
|
||||
bool leftSideBlocked = IsSideBlocked(subBorders, false);
|
||||
@@ -3658,21 +3700,21 @@ namespace Barotrauma
|
||||
int step = 5;
|
||||
if (rightSideBlocked && !leftSideBlocked)
|
||||
{
|
||||
bottomFound = TryMove(subBorders, ref spawnPoint, -step);
|
||||
bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
|
||||
}
|
||||
else if (leftSideBlocked && !rightSideBlocked)
|
||||
{
|
||||
bottomFound = TryMove(subBorders, ref spawnPoint, step);
|
||||
bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
|
||||
}
|
||||
else if (!bottomFound)
|
||||
{
|
||||
if (!leftSideBlocked)
|
||||
{
|
||||
bottomFound = TryMove(subBorders, ref spawnPoint, -step);
|
||||
bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
|
||||
}
|
||||
else if (!rightSideBlocked)
|
||||
{
|
||||
bottomFound = TryMove(subBorders, ref spawnPoint, step);
|
||||
bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3702,14 +3744,14 @@ namespace Barotrauma
|
||||
}
|
||||
return !isBlocked && bottomFound;
|
||||
|
||||
bool TryMove(Rectangle subBorders, ref Vector2 spawnPoint, float amount)
|
||||
bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount)
|
||||
{
|
||||
float maxMovement = 5000;
|
||||
float totalAmount = 0;
|
||||
bool foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint);
|
||||
bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
|
||||
while (!IsSideBlocked(subBorders, amount > 0))
|
||||
{
|
||||
foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint);
|
||||
foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
|
||||
totalAmount += amount;
|
||||
spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y);
|
||||
if (Math.Abs(totalAmount) > maxMovement)
|
||||
@@ -3738,7 +3780,7 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryRaycastToBottom(Rectangle subBorders, ref Vector2 spawnPoint)
|
||||
bool TryRaycast(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint)
|
||||
{
|
||||
// Shoot five rays and pick the highest hit point.
|
||||
int rayCount = 5;
|
||||
@@ -3764,16 +3806,18 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
var simPos = ConvertUnits.ToSimUnits(rayStart);
|
||||
var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1),
|
||||
customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body),
|
||||
var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1),
|
||||
customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)),
|
||||
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
|
||||
if (body != null)
|
||||
{
|
||||
positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, subBorders.Height / 2);
|
||||
positions[i] =
|
||||
ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) +
|
||||
new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1));
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
float highestPoint = positions.Max(p => p.Y);
|
||||
float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y);
|
||||
spawnPoint = new Vector2(spawnPoint.X, highestPoint);
|
||||
return hit;
|
||||
}
|
||||
@@ -3953,8 +3997,18 @@ namespace Barotrauma
|
||||
if (LevelData.OutpostGenerationParamsExist)
|
||||
{
|
||||
Location location = i == 0 ? StartLocation : EndLocation;
|
||||
OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ??
|
||||
LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient);
|
||||
OutpostGenerationParams outpostGenerationParams = null;
|
||||
if (LevelData.ForceOutpostGenerationParams != null)
|
||||
{
|
||||
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
|
||||
}
|
||||
else
|
||||
{
|
||||
outpostGenerationParams =
|
||||
LevelData.ForceOutpostGenerationParams ??
|
||||
LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
LocationType locationType = location?.Type;
|
||||
if (locationType == null)
|
||||
{
|
||||
@@ -4030,52 +4084,70 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
DockingPort outpostPort = null;
|
||||
closestDistance = float.MaxValue;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
Vector2 spawnPos;
|
||||
if (GenerationParams.ForceOutpostPosition != Vector2.Zero)
|
||||
{
|
||||
if (port.IsHorizontal || port.Docked) { continue; }
|
||||
if (port.Item.Submarine != outpost) { continue; }
|
||||
//the outpost port has to be at the bottom of the outpost
|
||||
if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; }
|
||||
float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X);
|
||||
if (dist < closestDistance)
|
||||
spawnPos = new Vector2(Size.X * GenerationParams.ForceOutpostPosition.X, Size.Y * GenerationParams.ForceOutpostPosition.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
DockingPort outpostPort = null;
|
||||
closestDistance = float.MaxValue;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
outpostPort = port;
|
||||
closestDistance = dist;
|
||||
if (port.IsHorizontal || port.Docked) { continue; }
|
||||
if (port.Item.Submarine != outpost) { continue; }
|
||||
//the outpost port has to be at the bottom of the outpost
|
||||
if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; }
|
||||
float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X);
|
||||
if (dist < closestDistance)
|
||||
{
|
||||
outpostPort = port;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X;
|
||||
//don't try to compensate if the port is very far from the sub's center of mass
|
||||
if (Math.Abs(subDockingPortOffset) > 5000.0f)
|
||||
{
|
||||
subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f);
|
||||
string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible.";
|
||||
DebugConsole.NewMessage(warningMsg, Color.Orange);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
|
||||
}
|
||||
|
||||
float? outpostDockingPortOffset = null;
|
||||
if (outpostPort != null)
|
||||
{
|
||||
outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X;
|
||||
//don't try to compensate if the port is very far from the outpost's center of mass
|
||||
if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f)
|
||||
float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X;
|
||||
//don't try to compensate if the port is very far from the sub's center of mass
|
||||
if (Math.Abs(subDockingPortOffset) > 5000.0f)
|
||||
{
|
||||
outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f);
|
||||
string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible.";
|
||||
subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f);
|
||||
string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible.";
|
||||
DebugConsole.NewMessage(warningMsg, Color.Orange);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
|
||||
}
|
||||
|
||||
float? outpostDockingPortOffset = null;
|
||||
if (outpostPort != null)
|
||||
{
|
||||
outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X;
|
||||
//don't try to compensate if the port is very far from the outpost's center of mass
|
||||
if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f)
|
||||
{
|
||||
outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f);
|
||||
string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible.";
|
||||
DebugConsole.NewMessage(warningMsg, Color.Orange);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
|
||||
}
|
||||
}
|
||||
|
||||
spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
|
||||
if (Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
|
||||
if (Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
|
||||
}
|
||||
outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false);
|
||||
|
||||
foreach (WayPoint wp in WayPoint.WayPointList)
|
||||
{
|
||||
if (wp.Submarine == outpost && wp.SpawnType != SpawnType.Path)
|
||||
{
|
||||
PositionsOfInterest.Add(new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost));
|
||||
}
|
||||
}
|
||||
|
||||
if ((i == 0) == !Mirrored)
|
||||
{
|
||||
StartOutpost = outpost;
|
||||
@@ -4094,13 +4166,12 @@ namespace Barotrauma
|
||||
outpost.Info.Name = EndLocation.Name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateBeaconStation()
|
||||
{
|
||||
if (!LevelData.HasBeaconStation) { return; }
|
||||
if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; }
|
||||
var beaconStationFiles = ContentPackageManager.EnabledPackages.All
|
||||
.SelectMany(p => p.GetFiles<BeaconStationFile>())
|
||||
.OrderBy(f => f.UintIdentifier).ToList();
|
||||
@@ -4111,27 +4182,40 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon);
|
||||
for (int i = beaconStationFiles.Count - 1; i >= 0; i--)
|
||||
ContentFile contentFile = null;
|
||||
if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation))
|
||||
{
|
||||
var beaconStationFile = beaconStationFiles[i];
|
||||
var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value);
|
||||
Debug.Assert(matchingInfo != null);
|
||||
if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
|
||||
contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation);
|
||||
if (contentFile == null)
|
||||
{
|
||||
if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty)
|
||||
{
|
||||
beaconStationFiles.RemoveAt(i);
|
||||
}
|
||||
DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead...");
|
||||
}
|
||||
}
|
||||
if (beaconStationFiles.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
|
||||
return;
|
||||
}
|
||||
var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
|
||||
|
||||
if (contentFile == null)
|
||||
{
|
||||
for (int i = beaconStationFiles.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var beaconStationFile = beaconStationFiles[i];
|
||||
var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value);
|
||||
Debug.Assert(matchingInfo != null);
|
||||
if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
|
||||
{
|
||||
if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty)
|
||||
{
|
||||
beaconStationFiles.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (beaconStationFiles.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
|
||||
return;
|
||||
}
|
||||
contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
|
||||
BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation);
|
||||
if (BeaconStation == null)
|
||||
{
|
||||
@@ -4195,7 +4279,7 @@ namespace Barotrauma
|
||||
{
|
||||
bool allowDisconnectedWires = true;
|
||||
bool allowDamagedWalls = true;
|
||||
if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info)
|
||||
if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info)
|
||||
{
|
||||
allowDisconnectedWires = info.AllowDisconnectedWires;
|
||||
allowDamagedWalls = info.AllowDamagedWalls;
|
||||
@@ -4346,7 +4430,7 @@ namespace Barotrauma
|
||||
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
|
||||
corpse.TeamID = CharacterTeamType.None;
|
||||
corpse.EnableDespawn = false;
|
||||
selectedPrefab.GiveItems(corpse, wreck);
|
||||
selectedPrefab.GiveItems(corpse, wreck, sp);
|
||||
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
|
||||
corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200));
|
||||
bool applyBurns = Rand.Value() < 0.1f;
|
||||
@@ -4377,7 +4461,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
corpse.CharacterHealth.ForceUpdateVisuals();
|
||||
corpse.GiveIdCardTags(sp);
|
||||
|
||||
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
|
||||
if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
|
||||
public readonly Biome Biome;
|
||||
|
||||
public readonly LevelGenerationParams GenerationParams;
|
||||
public LevelGenerationParams GenerationParams { get; private set; }
|
||||
|
||||
public bool HasBeaconStation;
|
||||
public bool IsBeaconActive;
|
||||
@@ -60,12 +60,14 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of <see cref="EventManager.MaxEventHistory"/>.
|
||||
/// </summary>
|
||||
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
|
||||
public readonly List<Identifier> EventHistory = new List<Identifier>();
|
||||
|
||||
/// <summary>
|
||||
/// Events that have already triggered in this level and can never trigger again. <see cref="EventSet.OncePerLevel"/>.
|
||||
/// </summary>
|
||||
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
|
||||
public readonly List<Identifier> NonRepeatableEvents = new List<Identifier>();
|
||||
|
||||
public readonly Dictionary<EventSet, int> FinishedEvents = new Dictionary<EventSet, int>();
|
||||
|
||||
/// <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"/>.
|
||||
@@ -150,10 +152,51 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty<string>());
|
||||
EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)));
|
||||
EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier));
|
||||
|
||||
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty<string>());
|
||||
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
|
||||
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier));
|
||||
|
||||
string finishedEventsName = nameof(FinishedEvents);
|
||||
if (element.GetChildElement(finishedEventsName) is { } finishedEventsElement)
|
||||
{
|
||||
foreach (var childElement in finishedEventsElement.GetChildElements(finishedEventsName))
|
||||
{
|
||||
Identifier eventSetIdentifier = childElement.GetAttributeIdentifier("set", Identifier.Empty);
|
||||
if (eventSetIdentifier.IsEmpty) { continue; }
|
||||
if (!EventSet.Prefabs.TryGet(eventSetIdentifier, out EventSet eventSet))
|
||||
{
|
||||
foreach (var prefab in EventSet.Prefabs)
|
||||
{
|
||||
if (FindSetRecursive(prefab, eventSetIdentifier) is { } foundSet)
|
||||
{
|
||||
eventSet = foundSet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventSet is null) { continue; }
|
||||
int count = childElement.GetAttributeInt("count", 0);
|
||||
if (count < 1) { continue; }
|
||||
FinishedEvents.Add(eventSet, count);
|
||||
}
|
||||
|
||||
static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier)
|
||||
{
|
||||
foreach (var childSet in parentSet.ChildSets)
|
||||
{
|
||||
if (childSet.Identifier == setIdentifier)
|
||||
{
|
||||
return childSet;
|
||||
}
|
||||
if (FindSetRecursive(childSet, setIdentifier) is { } foundSet)
|
||||
{
|
||||
return foundSet;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
|
||||
}
|
||||
@@ -163,7 +206,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public LevelData(LocationConnection locationConnection)
|
||||
{
|
||||
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
|
||||
Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed;
|
||||
Biome = locationConnection.Biome;
|
||||
Type = LevelType.LocationConnection;
|
||||
Difficulty = locationConnection.Difficulty;
|
||||
@@ -196,9 +239,9 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Instantiates level data using the properties of the location
|
||||
/// </summary>
|
||||
public LevelData(Location location, float difficulty)
|
||||
public LevelData(Location location, Map map, float difficulty)
|
||||
{
|
||||
Seed = location.BaseName;
|
||||
Seed = location.BaseName + map.Locations.IndexOf(location);
|
||||
Biome = location.Biome;
|
||||
Type = LevelType.Outpost;
|
||||
Difficulty = difficulty;
|
||||
@@ -254,14 +297,22 @@ namespace Barotrauma
|
||||
return levelData;
|
||||
}
|
||||
|
||||
public void ReassignGenerationParams(string seed)
|
||||
{
|
||||
GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier);
|
||||
}
|
||||
public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any();
|
||||
|
||||
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location)
|
||||
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
|
||||
{
|
||||
var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
|
||||
var suitableParams = OutpostGenerationParams.OutpostParams
|
||||
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
|
||||
.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
|
||||
if (!suitableParams.Any())
|
||||
{
|
||||
suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any());
|
||||
suitableParams = OutpostGenerationParams.OutpostParams
|
||||
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
|
||||
.Where(p => location == null || !p.AllowedLocationTypes.Any());
|
||||
if (!suitableParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
|
||||
@@ -305,11 +356,23 @@ namespace Barotrauma
|
||||
{
|
||||
if (EventHistory.Any())
|
||||
{
|
||||
newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier))));
|
||||
newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory)));
|
||||
}
|
||||
if (NonRepeatableEvents.Any())
|
||||
{
|
||||
newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier))));
|
||||
newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents)));
|
||||
}
|
||||
if (FinishedEvents.Any())
|
||||
{
|
||||
var finishedEventsElement = new XElement(nameof(FinishedEvents));
|
||||
foreach (var (set, count) in FinishedEvents)
|
||||
{
|
||||
var element = new XElement(nameof(FinishedEvents),
|
||||
new XAttribute("set", set.Identifier),
|
||||
new XAttribute("count", count));
|
||||
finishedEventsElement.Add(element);
|
||||
}
|
||||
newElement.Add(finishedEventsElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1.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)]
|
||||
[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;
|
||||
@@ -95,7 +95,7 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("20,40,50", IsPropertySaveable.Yes), Editable()]
|
||||
[Serialize("20,40,50", IsPropertySaveable.Yes), Editable]
|
||||
public Color BackgroundTextureColor
|
||||
{
|
||||
get;
|
||||
@@ -116,6 +116,13 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("255,255,255", IsPropertySaveable.Yes), Editable]
|
||||
public Color WaterParticleColor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private Vector2 startPosition;
|
||||
[Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)]
|
||||
public Vector2 StartPosition
|
||||
@@ -142,6 +149,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 forceOutpostPosition;
|
||||
[Serialize("0,0", IsPropertySaveable.Yes, "Position of the outpost (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner). If set to 0,0, the outpost is placed in a suitable position automatically."), Editable(DecimalCount = 2)]
|
||||
public Vector2 ForceOutpostPosition
|
||||
{
|
||||
get { return forceOutpostPosition; }
|
||||
set
|
||||
{
|
||||
forceOutpostPosition = new Vector2(
|
||||
MathHelper.Clamp(value.X, 0.0f, 1.0f),
|
||||
MathHelper.Clamp(value.Y, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable]
|
||||
public bool CreateHoleNextToEnd
|
||||
{
|
||||
@@ -156,6 +176,13 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, no walls generate in the level. Can be useful for e.g. levels that are just supposed to consist of a pre-built outpost."), Editable]
|
||||
public bool NoLevelGeometry
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)]
|
||||
public int LevelObjectAmount
|
||||
{
|
||||
@@ -191,21 +218,21 @@ namespace Barotrauma
|
||||
set { height = MathHelper.Clamp(value, 2000, 1000000); }
|
||||
}
|
||||
|
||||
[Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
|
||||
[Serialize(80000, IsPropertySaveable.Yes, description: "Minimum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
|
||||
public int InitialDepthMin
|
||||
{
|
||||
get { return initialDepthMin; }
|
||||
set { initialDepthMin = Math.Max(value, 0); }
|
||||
}
|
||||
|
||||
[Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
|
||||
[Serialize(80000, IsPropertySaveable.Yes, description: "Maximum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
|
||||
public int InitialDepthMax
|
||||
{
|
||||
get { return initialDepthMax; }
|
||||
set { initialDepthMax = Math.Max(value, initialDepthMin); }
|
||||
}
|
||||
|
||||
[Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)]
|
||||
[Serialize(6500, IsPropertySaveable.Yes, description: "Minimum width of the main tunnel going through the level, in pixels. Can be automatically increased by the level editor if the submarine is larger than this."), Editable(MinValueInt = 5000, MaxValueInt = 1000000)]
|
||||
public int MinTunnelRadius
|
||||
{
|
||||
get;
|
||||
@@ -213,7 +240,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
[Serialize("0,1", IsPropertySaveable.Yes), Editable]
|
||||
[Serialize("0,1", IsPropertySaveable.Yes, description: "Amount of side tunnels in the level (min,max)."), Editable]
|
||||
public Point SideTunnelCount
|
||||
{
|
||||
get;
|
||||
@@ -221,14 +248,14 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float SideTunnelVariance
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("2000,6000", IsPropertySaveable.Yes), Editable]
|
||||
[Serialize("2000,6000", IsPropertySaveable.Yes, description: "Minimum width of the side tunnels, in pixels. Unlike the main tunnel, does not get adjusted based on the size of the submarine."), Editable]
|
||||
public Point MinSideTunnelRadius
|
||||
{
|
||||
get;
|
||||
@@ -309,7 +336,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float MainPathVariance
|
||||
{
|
||||
get;
|
||||
@@ -358,28 +385,28 @@ namespace Barotrauma
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
public float CaveResourceSpawnChance { get; set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "Number of floating, destructible ice chunks in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int FloatingIceChunkCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)]
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "Number of islands (static wall chunks along the main path) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100)]
|
||||
public int IslandCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "Number of ice spires in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int IceSpireCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
[Serialize(5, IsPropertySaveable.Yes, description: "Number of abyss islands in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int AbyssIslandCount
|
||||
{
|
||||
get;
|
||||
@@ -400,7 +427,7 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes), Editable()]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "The probability of an abyss island having a cave. There is always a cave in at least one of the islands regardless of this setting."), Editable()]
|
||||
public float AbyssIslandCaveProbability
|
||||
{
|
||||
get;
|
||||
@@ -478,14 +505,14 @@ namespace Barotrauma
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
public int RuinCount { get; set; }
|
||||
|
||||
// TODO: Move the wreck parameters under a separate class?
|
||||
#region Wreck parameters
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
public int MinWreckCount { get; set; }
|
||||
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)]
|
||||
public int MaxWreckCount { get; set; }
|
||||
|
||||
// TODO: Move the wreck parameters under a separate class?
|
||||
#region Wreck parameters
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int MinCorpseCount { get; set; }
|
||||
|
||||
@@ -503,7 +530,10 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
|
||||
public float WreckFloodingHullMaxWaterPercentage { get; set; }
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Should a beacon station always spawn in this type of level?")]
|
||||
public string ForceBeaconStation { get; set; }
|
||||
|
||||
[Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()]
|
||||
public float BottomHoleProbability
|
||||
@@ -519,6 +549,14 @@ namespace Barotrauma
|
||||
private set { waterParticleScale = Math.Max(value, 0.01f); }
|
||||
}
|
||||
|
||||
private Vector2 waterParticleVelocity;
|
||||
[Serialize("0,10", IsPropertySaveable.Yes, description: "How fast the water particle texture scrolls."), Editable]
|
||||
public Vector2 WaterParticleVelocity
|
||||
{
|
||||
get { return waterParticleVelocity; }
|
||||
private set { waterParticleVelocity = value; }
|
||||
}
|
||||
|
||||
[Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)]
|
||||
public float WallTextureSize
|
||||
{
|
||||
@@ -533,6 +571,34 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("0,0", IsPropertySaveable.Yes, description: "Interval of lightning-like flashes of light in the level."), Editable]
|
||||
public Vector2 FlashInterval
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Color of lightning-like flashes of light in the level."), Editable]
|
||||
public Color FlashColor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the \"ambient noise\" of the biome play in this level if it's an outpost level."), Editable]
|
||||
public bool PlayNoiseLoopInOutpostLevel
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float WaterAmbienceVolume
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)]
|
||||
public float WallEdgeExpandOutwardsAmount
|
||||
{
|
||||
@@ -556,8 +622,13 @@ namespace Barotrauma
|
||||
public Sprite WallSpriteDestroyed { get; private set; }
|
||||
public Sprite WaterParticles { get; private set; }
|
||||
|
||||
#if CLIENT
|
||||
public Sounds.Sound FlashSound { get; private set; }
|
||||
#endif
|
||||
|
||||
#warning TODO: this should be in the unit test project (#3164)
|
||||
public static void CheckValidity()
|
||||
|
||||
{
|
||||
foreach (Biome biome in Biome.Prefabs)
|
||||
{
|
||||
@@ -575,7 +646,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default)
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default)
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
@@ -590,14 +661,29 @@ namespace Barotrauma
|
||||
lp.Type == type &&
|
||||
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
|
||||
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
|
||||
matchingLevelParams = biome.IsEmpty
|
||||
? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome))
|
||||
: matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome));
|
||||
if (biomeId.IsEmpty)
|
||||
{
|
||||
//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));
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome;
|
||||
if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)))
|
||||
{
|
||||
//in the end biome, we must choose level parameters meant specifically for the end levels
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId));
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId));
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingLevelParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
|
||||
if (!biome.IsEmpty)
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
|
||||
if (!biomeId.IsEmpty)
|
||||
{
|
||||
//try to find params that at least have a suitable type
|
||||
matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type);
|
||||
@@ -611,7 +697,7 @@ namespace Barotrauma
|
||||
|
||||
if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty))
|
||||
{
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -661,6 +747,11 @@ namespace Barotrauma
|
||||
case "waterparticles":
|
||||
WaterParticles = new Sprite(subElement);
|
||||
break;
|
||||
#if CLIENT
|
||||
case "flashsound":
|
||||
FlashSound = GameMain.SoundManager.LoadSound(subElement);
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-19
@@ -235,9 +235,22 @@ namespace Barotrauma
|
||||
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient);
|
||||
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
|
||||
PlaceObject(prefab, spawnPosition, level, cave);
|
||||
if (prefab.MaxCount < amount)
|
||||
if (amount > prefab.MaxCount && objects.Count > prefab.MaxCount)
|
||||
{
|
||||
if (objects.Count(o => o.Prefab == prefab && o.ParentCave == cave) >= prefab.MaxCount)
|
||||
bool maxReached = false;
|
||||
int objectCount = 0;
|
||||
for (int j = 0; j < objects.Count; j++)
|
||||
{
|
||||
if (objects[j].Prefab == prefab && objects[j].ParentCave == cave)
|
||||
{
|
||||
objectCount++;
|
||||
if (objectCount >= prefab.MaxCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (objectCount >= prefab.MaxCount)
|
||||
{
|
||||
availablePrefabs.Remove(prefab);
|
||||
}
|
||||
@@ -350,8 +363,8 @@ namespace Barotrauma
|
||||
|
||||
private void AddObject(LevelObject newObject, Level level)
|
||||
{
|
||||
if (newObject.Triggers != null)
|
||||
{
|
||||
if (newObject.Triggers != null)
|
||||
{
|
||||
foreach (LevelTrigger trigger in newObject.Triggers)
|
||||
{
|
||||
trigger.OnTriggered += (levelTrigger, obj) =>
|
||||
@@ -360,7 +373,7 @@ namespace Barotrauma
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var spriteCorners = new List<Vector2>
|
||||
{
|
||||
Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero
|
||||
@@ -393,11 +406,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z;
|
||||
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z;
|
||||
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z / 10000.0f;
|
||||
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z / 10000.0f;
|
||||
|
||||
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos;
|
||||
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos;
|
||||
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z / 10000.0f - level.BottomPos;
|
||||
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z / 10000.0f - level.BottomPos;
|
||||
|
||||
if (newObject.Triggers != null)
|
||||
{
|
||||
@@ -436,11 +449,11 @@ namespace Barotrauma
|
||||
|
||||
int xStart = (int)Math.Floor(minX / GridSize);
|
||||
int xEnd = (int)Math.Floor(maxX / GridSize);
|
||||
if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) return;
|
||||
if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) { return; }
|
||||
|
||||
int yStart = (int)Math.Floor(minY / GridSize);
|
||||
int yEnd = (int)Math.Floor(maxY / GridSize);
|
||||
if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) return;
|
||||
if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) { return; }
|
||||
|
||||
xStart = Math.Max(xStart, 0);
|
||||
xEnd = Math.Min(xEnd, objectGrid.GetLength(0) - 1);
|
||||
@@ -451,13 +464,21 @@ namespace Barotrauma
|
||||
{
|
||||
for (int y = yStart; y <= yEnd; y++)
|
||||
{
|
||||
if (objectGrid[x, y] == null) objectGrid[x, y] = new List<LevelObject>();
|
||||
objectGrid[x, y].Add(newObject);
|
||||
var list = objectGrid[x, y];
|
||||
if (objectGrid[x, y] == null) { list = objectGrid[x, y] = new List<LevelObject>(); }
|
||||
|
||||
//insertion sort in ascending order (= prefer rendering objects in front)
|
||||
int drawOrderIndex = 0;
|
||||
while (drawOrderIndex < list.Count && list[drawOrderIndex].Position.Z < newObject.Position.Z)
|
||||
{
|
||||
drawOrderIndex++;
|
||||
}
|
||||
list.Insert(drawOrderIndex, newObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition)
|
||||
public static Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition)
|
||||
{
|
||||
return new Microsoft.Xna.Framework.Point(
|
||||
(int)Math.Floor(worldPosition.X / GridSize),
|
||||
@@ -500,7 +521,7 @@ namespace Barotrauma
|
||||
return objectsInRange;
|
||||
}
|
||||
|
||||
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true)
|
||||
private static List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true)
|
||||
{
|
||||
List<LevelObjectPrefab.SpawnPosType> spawnPosTypes = new List<LevelObjectPrefab.SpawnPosType>(4);
|
||||
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
|
||||
@@ -593,12 +614,12 @@ namespace Barotrauma
|
||||
if (obj == triggeredObject || obj.Triggers == null) { continue; }
|
||||
foreach (LevelTrigger otherTrigger in obj.Triggers)
|
||||
{
|
||||
otherTrigger.OtherTriggered(triggeredObject, trigger);
|
||||
otherTrigger.OtherTriggered(trigger, triggerer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LevelObjectPrefab GetRandomPrefab(Level level, IList<LevelObjectPrefab> availablePrefabs)
|
||||
private static LevelObjectPrefab GetRandomPrefab(Level level, IList<LevelObjectPrefab> availablePrefabs)
|
||||
{
|
||||
if (availablePrefabs.Sum(p => p.GetCommonness(level.LevelData)) <= 0.0f) { return null; }
|
||||
return ToolBox.SelectWeightedRandom(
|
||||
@@ -606,7 +627,7 @@ namespace Barotrauma
|
||||
availablePrefabs.Select(p => p.GetCommonness(level.LevelData)).ToList(), Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs, bool requireCaveSpecificOverride)
|
||||
private static LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs, bool requireCaveSpecificOverride)
|
||||
{
|
||||
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; }
|
||||
return ToolBox.SelectWeightedRandom(
|
||||
@@ -634,7 +655,7 @@ namespace Barotrauma
|
||||
|
||||
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
|
||||
{
|
||||
if (!(extraData is EventData eventData)) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); }
|
||||
if (extraData is not EventData eventData) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); }
|
||||
LevelObject obj = eventData.LevelObject;
|
||||
msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count);
|
||||
obj.ServerWrite(msg, c);
|
||||
|
||||
+3
-3
@@ -129,7 +129,7 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable]
|
||||
[Serialize("0.0,1.0", IsPropertySaveable.Yes, description: "The sprite depth of the object (min, max). Values of 0 or less make the object render in front of walls, values larger than 0 make it render behind walls with a parallax effect."), Editable]
|
||||
public Vector2 DepthRange
|
||||
{
|
||||
get;
|
||||
@@ -273,14 +273,14 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the object disappear if the object is destroyed? Only relevant if TakeLevelWallDamage is true."), Editable]
|
||||
public bool HideWhenBroken
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Amount of health the object has. Only relevant if TakeLevelWallDamage is true."), Editable]
|
||||
public float Health
|
||||
{
|
||||
get;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
@@ -215,7 +216,7 @@ namespace Barotrauma
|
||||
PhysicsBody.FarseerBody.SetIsSensor(element.GetAttributeBool("sensor", true));
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
|
||||
ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
|
||||
ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f));
|
||||
|
||||
PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
|
||||
}
|
||||
@@ -470,7 +471,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Another trigger was triggered, check if this one should react to it
|
||||
/// </summary>
|
||||
public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger)
|
||||
public void OtherTriggered(LevelTrigger otherTrigger, Entity triggerer)
|
||||
{
|
||||
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; }
|
||||
|
||||
@@ -486,7 +487,16 @@ namespace Barotrauma
|
||||
triggeredTimer = stayTriggeredDelay;
|
||||
if (!wasAlreadyTriggered)
|
||||
{
|
||||
OnTriggered?.Invoke(this, null);
|
||||
if (!IsTriggeredByEntity(triggerer, triggeredBy, mustBeOutside: true)) { return; }
|
||||
if (!triggerers.Contains(triggerer))
|
||||
{
|
||||
if (!IsTriggered)
|
||||
{
|
||||
OnTriggered?.Invoke(this, triggerer);
|
||||
}
|
||||
TriggererPosition[triggerer] = triggerer.WorldPosition;
|
||||
triggerers.Add(triggerer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -658,6 +668,10 @@ namespace Barotrauma
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position);
|
||||
}
|
||||
else if (triggerer is Submarine sub)
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, sub, Array.Empty<ISerializableEntity>(), position);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
@@ -747,7 +761,7 @@ namespace Barotrauma
|
||||
Vector2 baseVel = GetWaterFlowVelocity();
|
||||
if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero;
|
||||
|
||||
float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
|
||||
float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f));
|
||||
float dist = Vector2.Distance(viewPosition, WorldPosition);
|
||||
if (dist > triggerSize) return Vector2.Zero;
|
||||
|
||||
|
||||
@@ -476,7 +476,7 @@ namespace Barotrauma
|
||||
bool leaveBehind = false;
|
||||
if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine))
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit);
|
||||
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEitherExit);
|
||||
if (Submarine.MainSub.AtEndExit)
|
||||
{
|
||||
leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit;
|
||||
|
||||
@@ -3,7 +3,6 @@ using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -63,10 +62,17 @@ namespace Barotrauma
|
||||
|
||||
public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false;
|
||||
|
||||
public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
|
||||
|
||||
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
|
||||
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
|
||||
public int LocationTypeChangeCooldown;
|
||||
|
||||
/// <summary>
|
||||
/// Is some mission blocking this location from changing its type?
|
||||
/// </summary>
|
||||
public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges);
|
||||
|
||||
public string BaseName { get => baseName; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
@@ -83,7 +89,11 @@ namespace Barotrauma
|
||||
|
||||
public int PortraitId { get; private set; }
|
||||
|
||||
public Reputation Reputation { get; set; }
|
||||
public Faction Faction { get; set; }
|
||||
|
||||
public Faction SecondaryFaction { get; set; }
|
||||
|
||||
public Reputation Reputation => Faction?.Reputation;
|
||||
|
||||
public int TurnsInRadiation { get; set; }
|
||||
|
||||
@@ -92,6 +102,7 @@ namespace Barotrauma
|
||||
public class StoreInfo
|
||||
{
|
||||
public Identifier Identifier { get; }
|
||||
public Identifier MerchantFaction { get; private set; }
|
||||
public int Balance { get; set; }
|
||||
public List<PurchasedItem> Stock { get; } = new List<PurchasedItem>();
|
||||
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
|
||||
@@ -101,6 +112,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public int PriceModifier { get; set; }
|
||||
public Location Location { get; }
|
||||
private float MaxReputationModifier => Location.StoreMaxReputationModifier;
|
||||
|
||||
private StoreInfo(Location location)
|
||||
{
|
||||
@@ -125,6 +137,7 @@ namespace Barotrauma
|
||||
public StoreInfo(Location location, XElement storeElement) : this(location)
|
||||
{
|
||||
Identifier = storeElement.GetAttributeIdentifier("identifier", "");
|
||||
MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), "");
|
||||
Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance);
|
||||
PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
|
||||
// Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute
|
||||
@@ -281,15 +294,17 @@ namespace Barotrauma
|
||||
{
|
||||
price = Location.DailySpecialPriceModifier * price;
|
||||
}
|
||||
// Adjust by current location reputation
|
||||
price *= Location.GetStoreReputationModifier(true);
|
||||
// Adjust by current reputation
|
||||
price *= GetReputationModifier(true);
|
||||
|
||||
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
if (characters.Any())
|
||||
{
|
||||
if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
|
||||
var faction = GetMerchantOrLocationFactionIdentifier();
|
||||
if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
|
||||
{
|
||||
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated));
|
||||
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
|
||||
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
|
||||
}
|
||||
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
|
||||
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
|
||||
@@ -312,8 +327,8 @@ namespace Barotrauma
|
||||
{
|
||||
price = Location.RequestGoodPriceModifier * price;
|
||||
}
|
||||
// Adjust by current location reputation
|
||||
price *= Location.GetStoreReputationModifier(false);
|
||||
// Adjust by location reputation
|
||||
price *= GetReputationModifier(false);
|
||||
|
||||
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
if (characters.Any())
|
||||
@@ -326,6 +341,45 @@ namespace Barotrauma
|
||||
return Math.Max((int)price, 1);
|
||||
}
|
||||
|
||||
public void SetMerchantFaction(Identifier factionIdentifier)
|
||||
{
|
||||
MerchantFaction = factionIdentifier;
|
||||
}
|
||||
|
||||
public Identifier GetMerchantOrLocationFactionIdentifier()
|
||||
{
|
||||
return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty);
|
||||
}
|
||||
|
||||
public float GetReputationModifier(bool buying)
|
||||
{
|
||||
var factionIdentifier = GetMerchantOrLocationFactionIdentifier();
|
||||
var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation;
|
||||
if (reputation == null) { return 1.0f; }
|
||||
if (buying)
|
||||
{
|
||||
if (reputation.Value > 0.0f)
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (reputation.Value > 0.0f)
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Identifier.Value;
|
||||
@@ -387,6 +441,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void SelectMission(Mission mission)
|
||||
{
|
||||
if (!SelectedMissions.Contains(mission) && mission != null)
|
||||
@@ -449,17 +505,22 @@ namespace Barotrauma
|
||||
|
||||
public bool IsGateBetweenBiomes;
|
||||
|
||||
private struct LoadedMission
|
||||
private readonly struct LoadedMission
|
||||
{
|
||||
public MissionPrefab MissionPrefab { get; }
|
||||
public int DestinationIndex { get; }
|
||||
public bool SelectedMission { get; }
|
||||
public readonly MissionPrefab MissionPrefab;
|
||||
public readonly int TimesAttempted;
|
||||
public readonly int OriginLocationIndex;
|
||||
public readonly int DestinationIndex;
|
||||
public readonly bool SelectedMission;
|
||||
|
||||
public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission)
|
||||
public LoadedMission(XElement element)
|
||||
{
|
||||
MissionPrefab = prefab;
|
||||
DestinationIndex = destinationIndex;
|
||||
SelectedMission = selectedMission;
|
||||
var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty);
|
||||
MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null;
|
||||
TimesAttempted = element.GetAttributeInt("timesattempted", 0);
|
||||
OriginLocationIndex = element.GetAttributeInt("origin", -1);
|
||||
DestinationIndex = element.GetAttributeInt("destinationindex", -1);
|
||||
SelectedMission = element.GetAttributeBool("selected", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +545,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Create a location from save data
|
||||
/// </summary>
|
||||
public Location(XElement element)
|
||||
public Location(CampaignMode campaign, XElement element)
|
||||
{
|
||||
Identifier locationTypeId = element.GetAttributeIdentifier("type", "");
|
||||
bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type);
|
||||
@@ -497,12 +558,23 @@ namespace Barotrauma
|
||||
baseName = element.GetAttributeString("basename", "");
|
||||
Name = element.GetAttributeString("name", "");
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -640,13 +712,11 @@ namespace Barotrauma
|
||||
loadedMissions = new List<LoadedMission>();
|
||||
foreach (XElement childElement in missionsElement.GetChildElements("mission"))
|
||||
{
|
||||
var id = childElement.GetAttributeString("prefabid", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id);
|
||||
if (prefab == null) { continue; }
|
||||
var destination = childElement.GetAttributeInt("destinationindex", -1);
|
||||
var selected = childElement.GetAttributeBool("selected", false);
|
||||
loadedMissions.Add(new LoadedMission(prefab, destination, selected));
|
||||
var loadedMission = new LoadedMission(childElement);
|
||||
if (loadedMission.MissionPrefab != null)
|
||||
{
|
||||
loadedMissions.Add(loadedMission);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -656,7 +726,7 @@ namespace Barotrauma
|
||||
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
|
||||
}
|
||||
|
||||
public void ChangeType(LocationType newType, bool createStores = true)
|
||||
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true)
|
||||
{
|
||||
if (newType == Type) { return; }
|
||||
|
||||
@@ -671,56 +741,61 @@ namespace Barotrauma
|
||||
Type = newType;
|
||||
Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
|
||||
if (Type.MissionIdentifiers.Any())
|
||||
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced());
|
||||
if (Faction == null)
|
||||
{
|
||||
Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced);
|
||||
}
|
||||
if (SecondaryFaction == null)
|
||||
{
|
||||
SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced);
|
||||
}
|
||||
}
|
||||
if (Type.MissionTags.Any())
|
||||
else
|
||||
{
|
||||
UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced());
|
||||
Faction = null;
|
||||
SecondaryFaction = null;
|
||||
}
|
||||
|
||||
UnlockInitialMissions(Rand.RandSync.Unsynced);
|
||||
|
||||
if (createStores)
|
||||
{
|
||||
CreateStores(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnlockInitialMissions()
|
||||
public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
|
||||
{
|
||||
if (Type.MissionIdentifiers.Any())
|
||||
{
|
||||
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient));
|
||||
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync));
|
||||
}
|
||||
if (Type.MissionTags.Any())
|
||||
{
|
||||
UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient));
|
||||
UnlockMissionByTag(Type.MissionTags.GetRandom(randSync));
|
||||
}
|
||||
}
|
||||
|
||||
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
|
||||
var mission = InstantiateMission(missionPrefab, connection);
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
|
||||
AddMission(InstantiateMission(missionPrefab, connection));
|
||||
}
|
||||
|
||||
public void UnlockMission(MissionPrefab missionPrefab)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
|
||||
var mission = InstantiateMission(missionPrefab);
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
|
||||
AddMission(InstantiateMission(missionPrefab));
|
||||
}
|
||||
|
||||
public Mission UnlockMissionByIdentifier(Identifier identifier)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; }
|
||||
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
|
||||
|
||||
var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier);
|
||||
if (missionPrefab == null)
|
||||
@@ -735,43 +810,45 @@ namespace Barotrauma
|
||||
{
|
||||
return null;
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
AddMission(mission);
|
||||
DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true);
|
||||
return mission;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Mission UnlockMissionByTag(Identifier tag)
|
||||
public Mission UnlockMissionByTag(Identifier tag, Random random = null)
|
||||
{
|
||||
var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag));
|
||||
if (!matchingMissions.Any())
|
||||
if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
|
||||
var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag));
|
||||
if (matchingMissions.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m));
|
||||
var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m));
|
||||
if (unusedMissions.Any())
|
||||
{
|
||||
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this)));
|
||||
if (!suitableMissions.Any())
|
||||
if (suitableMissions.None())
|
||||
{
|
||||
suitableMissions = unusedMissions;
|
||||
}
|
||||
MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced);
|
||||
|
||||
MissionPrefab missionPrefab =
|
||||
random != null ?
|
||||
ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) :
|
||||
ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, Rand.RandSync.Unsynced);
|
||||
|
||||
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
|
||||
//don't allow duplicate missions in the same connection
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
AddMission(mission);
|
||||
DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\".", debugOnly: true);
|
||||
return mission;
|
||||
}
|
||||
else
|
||||
@@ -783,6 +860,20 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private void AddMission(Mission mission)
|
||||
{
|
||||
if (!mission.Prefab.AllowOtherMissionsInLevel)
|
||||
{
|
||||
availableMissions.Clear();
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#else
|
||||
(GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
|
||||
#endif
|
||||
}
|
||||
|
||||
private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
|
||||
{
|
||||
if (prefab.IsAllowed(this, this))
|
||||
@@ -792,7 +883,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
|
||||
if (!suitableConnections.Any())
|
||||
if (suitableConnections.None())
|
||||
{
|
||||
suitableConnections = Connections.ToList();
|
||||
}
|
||||
@@ -877,6 +968,11 @@ namespace Barotrauma
|
||||
destination = Connections.First().OtherLocation(this);
|
||||
}
|
||||
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
|
||||
if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count)
|
||||
{
|
||||
mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
|
||||
}
|
||||
mission.TimesAttempted = loadedMission.TimesAttempted;
|
||||
availableMissions.Add(mission);
|
||||
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
|
||||
}
|
||||
@@ -978,12 +1074,22 @@ namespace Barotrauma
|
||||
|
||||
private string RandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
|
||||
{
|
||||
if (!type.ForceLocationName.IsNullOrEmpty())
|
||||
{
|
||||
baseName = type.ForceLocationName.Value;
|
||||
return baseName;
|
||||
}
|
||||
baseName = type.GetRandomName(rand, existingLocations);
|
||||
if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; }
|
||||
nameFormatIndex = rand.Next() % type.NameFormats.Count;
|
||||
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
|
||||
}
|
||||
|
||||
public void ForceName(string name)
|
||||
{
|
||||
baseName = Name = name;
|
||||
}
|
||||
|
||||
public void LoadStores(XElement locationElement)
|
||||
{
|
||||
UpdateStoreIdentifiers();
|
||||
@@ -1068,13 +1174,21 @@ namespace Barotrauma
|
||||
|
||||
public int GetAdjustedMechanicalCost(int cost)
|
||||
{
|
||||
float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f);
|
||||
return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
|
||||
float discount = 0.0f;
|
||||
if (Reputation != null)
|
||||
{
|
||||
discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f);
|
||||
}
|
||||
return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
|
||||
}
|
||||
|
||||
public int GetAdjustedHealCost(int cost)
|
||||
{
|
||||
float discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f);
|
||||
float discount = 0.0f;
|
||||
if (Reputation != null)
|
||||
{
|
||||
discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f);
|
||||
}
|
||||
return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier);
|
||||
}
|
||||
|
||||
@@ -1258,32 +1372,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float GetStoreReputationModifier(bool buying)
|
||||
{
|
||||
if (buying)
|
||||
{
|
||||
if (Reputation.Value > 0.0f)
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Reputation.Value > 0.0f)
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetExtraSpecialSalesCount()
|
||||
{
|
||||
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
@@ -1314,14 +1402,14 @@ namespace Barotrauma
|
||||
{
|
||||
return LevelData != null &&
|
||||
LevelData.OutpostGenerationParamsExist &&
|
||||
LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType));
|
||||
LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType));
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
public void Reset(CampaignMode campaign)
|
||||
{
|
||||
if (Type != OriginalType)
|
||||
{
|
||||
ChangeType(OriginalType);
|
||||
ChangeType(campaign, OriginalType);
|
||||
PendingLocationTypeChange = null;
|
||||
}
|
||||
CreateStores(force: true);
|
||||
@@ -1345,6 +1433,16 @@ namespace Barotrauma
|
||||
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
|
||||
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
|
||||
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
|
||||
|
||||
if (Faction != null)
|
||||
{
|
||||
locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
|
||||
}
|
||||
if (SecondaryFaction != null)
|
||||
{
|
||||
locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier));
|
||||
}
|
||||
|
||||
LevelData.Save(locationElement);
|
||||
|
||||
for (int i = 0; i < Type.CanChangeTo.Count; i++)
|
||||
@@ -1403,6 +1501,7 @@ namespace Barotrauma
|
||||
{
|
||||
var storeElement = new XElement("store",
|
||||
new XAttribute("identifier", store.Identifier.Value),
|
||||
new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction),
|
||||
new XAttribute("balance", store.Balance),
|
||||
new XAttribute("pricemodifier", store.PriceModifier));
|
||||
foreach (PurchasedItem item in store.Stock)
|
||||
@@ -1442,10 +1541,13 @@ namespace Barotrauma
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
|
||||
var i = map.Locations.IndexOf(location);
|
||||
var destinationIndex = map.Locations.IndexOf(location);
|
||||
var originIndex = map.Locations.IndexOf(mission.OriginLocation);
|
||||
missionsElement.Add(new XElement("mission",
|
||||
new XAttribute("prefabid", mission.Prefab.Identifier),
|
||||
new XAttribute("destinationindex", i),
|
||||
new XAttribute("destinationindex", destinationIndex),
|
||||
new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted),
|
||||
new XAttribute("origin", originIndex),
|
||||
new XAttribute("selected", selectedMissions.Contains(mission))));
|
||||
}
|
||||
locationElement.Add(missionsElement);
|
||||
|
||||
@@ -13,8 +13,8 @@ namespace Barotrauma
|
||||
{
|
||||
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
|
||||
|
||||
private readonly List<string> names;
|
||||
private readonly List<Sprite> portraits = new List<Sprite>();
|
||||
private readonly ImmutableArray<string> names;
|
||||
private readonly ImmutableArray<Sprite> portraits;
|
||||
|
||||
//<name, commonness>
|
||||
private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs;
|
||||
@@ -26,6 +26,8 @@ namespace Barotrauma
|
||||
public readonly LocalizedString Name;
|
||||
public readonly LocalizedString Description;
|
||||
|
||||
public readonly LocalizedString ForceLocationName;
|
||||
|
||||
public readonly float BeaconStationChance;
|
||||
|
||||
public readonly CharacterTeamType OutpostTeam;
|
||||
@@ -39,7 +41,12 @@ namespace Barotrauma
|
||||
|
||||
public bool IsEnterable { get; private set; }
|
||||
|
||||
public bool UseInMainMenu
|
||||
/// <summary>
|
||||
/// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone
|
||||
/// </summary>
|
||||
public bool AllowInRandomLevels { get; private set; }
|
||||
|
||||
public bool UsePortraitInRandomLoadingScreens
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
@@ -96,6 +103,8 @@ namespace Barotrauma
|
||||
public int DailySpecialsCount { get; } = 1;
|
||||
public int RequestedGoodsCount { get; } = 1;
|
||||
|
||||
public readonly bool ShowSonarMarker = true;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"LocationType (" + Identifier + ")";
|
||||
@@ -108,9 +117,12 @@ namespace Barotrauma
|
||||
|
||||
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
|
||||
|
||||
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
|
||||
UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true);
|
||||
HasOutpost = element.GetAttributeBool("hasoutpost", true);
|
||||
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
|
||||
AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true);
|
||||
|
||||
ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true);
|
||||
|
||||
MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty<Identifier>()).ToImmutableArray();
|
||||
MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty<Identifier>()).ToImmutableArray();
|
||||
@@ -126,23 +138,31 @@ namespace Barotrauma
|
||||
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
|
||||
Enum.TryParse(teamStr, out OutpostTeam);
|
||||
|
||||
string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" });
|
||||
names = new List<string>();
|
||||
foreach (string rawPath in rawNamePaths)
|
||||
if (element.GetAttribute("name") != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
|
||||
names.AddRange(File.ReadAllLines(path.Value).ToList());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
|
||||
}
|
||||
ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty));
|
||||
}
|
||||
if (!names.Any())
|
||||
else
|
||||
{
|
||||
names.Add("ERROR: No names found");
|
||||
string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" });
|
||||
var names = new List<string>();
|
||||
foreach (string rawPath in rawNamePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
|
||||
names.AddRange(File.ReadAllLines(path.Value).ToList());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
|
||||
}
|
||||
}
|
||||
if (!names.Any())
|
||||
{
|
||||
names.Add("ERROR: No names found");
|
||||
}
|
||||
this.names = names.ToImmutableArray();
|
||||
}
|
||||
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
|
||||
@@ -172,7 +192,7 @@ namespace Barotrauma
|
||||
}
|
||||
MinCountPerZone[zoneIndex] = minCount;
|
||||
}
|
||||
|
||||
var portraits = new List<Sprite>();
|
||||
var hireableJobs = new List<(Identifier, float)>();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
@@ -213,6 +233,7 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.portraits = portraits.ToImmutableArray();
|
||||
this.hireableJobs = hireableJobs.ToImmutableArray();
|
||||
}
|
||||
|
||||
@@ -229,10 +250,10 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
public Sprite GetPortrait(int portraitId)
|
||||
public Sprite GetPortrait(int randomSeed)
|
||||
{
|
||||
if (portraits.Count == 0) { return null; }
|
||||
return portraits[Math.Abs(portraitId) % portraits.Count];
|
||||
if (portraits.Length == 0) { return null; }
|
||||
return portraits[Math.Abs(randomSeed) % portraits.Length];
|
||||
}
|
||||
|
||||
public string GetRandomName(Random rand, IEnumerable<Location> existingLocations)
|
||||
@@ -245,17 +266,34 @@ namespace Barotrauma
|
||||
return unusedNames[rand.Next() % unusedNames.Count];
|
||||
}
|
||||
}
|
||||
return names[rand.Next() % names.Count];
|
||||
return names[rand.Next() % names.Length];
|
||||
}
|
||||
|
||||
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false)
|
||||
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
|
||||
{
|
||||
Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
|
||||
|
||||
LocationType[] allowedLocationTypes =
|
||||
Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost))
|
||||
Prefabs.Where(lt =>
|
||||
(predicate == null || predicate(lt)) && IsValid(lt))
|
||||
.OrderBy(p => p.UintIdentifier).ToArray();
|
||||
|
||||
bool IsValid(LocationType lt)
|
||||
{
|
||||
if (requireOutpost && !lt.HasOutpost) { return false; }
|
||||
if (zone.HasValue)
|
||||
{
|
||||
if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; }
|
||||
}
|
||||
//if zone is not defined, this is a "random" (non-campaign) level
|
||||
//-> don't choose location types that aren't allowed in those
|
||||
else if (!lt.AllowInRandomLevels)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowedLocationTypes.Length == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using static Barotrauma.LocationTypeChange;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -58,7 +58,7 @@ namespace Barotrauma
|
||||
public Requirement(XElement element, LocationTypeChange change)
|
||||
{
|
||||
RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty<Identifier>())).ToImmutableArray();
|
||||
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1);
|
||||
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0);
|
||||
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
|
||||
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
|
||||
RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false);
|
||||
@@ -91,37 +91,30 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool AnyWithinDistance(Location startLocation, int distance)
|
||||
{
|
||||
return Map.LocationOrConnectionWithinDistance(
|
||||
startLocation,
|
||||
maxDistance: distance,
|
||||
criteria: MatchesLocation,
|
||||
connectionCriteria: MatchesConnection);
|
||||
}
|
||||
|
||||
public bool MatchesLocation(Location location)
|
||||
{
|
||||
return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated();
|
||||
}
|
||||
|
||||
public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet<Location> checkedLocations = null)
|
||||
public bool MatchesConnection(LocationConnection connection)
|
||||
{
|
||||
if (currentDistance > maxDistance) { return false; }
|
||||
if (currentDistance > 0 && MatchesLocation(location)) { return true; }
|
||||
|
||||
checkedLocations ??= new HashSet<Location>();
|
||||
checkedLocations.Add(location);
|
||||
|
||||
foreach (var connection in location.Connections)
|
||||
if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive)
|
||||
{
|
||||
if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var otherLocation = connection.OtherLocation(location);
|
||||
if (!checkedLocations.Contains(otherLocation))
|
||||
{
|
||||
if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -141,24 +134,25 @@ namespace Barotrauma
|
||||
|
||||
private readonly bool requireChangeMessages;
|
||||
private readonly string messageTag;
|
||||
private ImmutableArray<string>? messages = null;
|
||||
public IReadOnlyList<string> Messages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!messages.HasValue)
|
||||
{
|
||||
messages = TextManager.GetAll(messageTag).ToImmutableArray();
|
||||
if (messages.Value.None())
|
||||
{
|
||||
if (requireChangeMessages)
|
||||
{
|
||||
DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages.Value;
|
||||
public IReadOnlyList<string> GetMessages(Faction faction)
|
||||
{
|
||||
if (faction != null && TextManager.ContainsTag(messageTag + "." + faction.Prefab.Identifier))
|
||||
{
|
||||
return TextManager.GetAll(messageTag + "." + faction.Prefab.Identifier).ToImmutableArray();
|
||||
}
|
||||
|
||||
if (TextManager.ContainsTag(messageTag))
|
||||
{
|
||||
return TextManager.GetAll(messageTag).ToImmutableArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requireChangeMessages)
|
||||
{
|
||||
DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}");
|
||||
}
|
||||
return Enumerable.Empty<string>().ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,8 +220,9 @@ namespace Barotrauma
|
||||
if (location.LocationTypeChangeCooldown > 0) { return 0.0f; }
|
||||
if (location.IsGateBetweenBiomes) { return 0.0f; }
|
||||
|
||||
if (DisallowedAdjacentLocations.Any() &&
|
||||
AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
|
||||
if (DisallowedAdjacentLocations.Any() &&
|
||||
Map.LocationOrConnectionWithinDistance(location, DisallowedProximity,
|
||||
(otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
@@ -246,7 +241,6 @@ namespace Barotrauma
|
||||
probability *= requirement.Probability;
|
||||
}
|
||||
}
|
||||
|
||||
if (location.ProximityTimer.ContainsKey(requirement))
|
||||
{
|
||||
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
|
||||
@@ -265,25 +259,5 @@ namespace Barotrauma
|
||||
|
||||
return probability;
|
||||
}
|
||||
|
||||
private bool AnyWithinDistance(Location location, int maxDistance, Func<Location, bool> predicate, int currentDistance = 0, HashSet<Location> checkedLocations = null)
|
||||
{
|
||||
if (currentDistance > maxDistance) { return false; }
|
||||
if (currentDistance > 0 && predicate(location)) { return true; }
|
||||
|
||||
checkedLocations ??= new HashSet<Location>();
|
||||
checkedLocations.Add(location);
|
||||
|
||||
foreach (var connection in location.Connections)
|
||||
{
|
||||
var otherLocation = connection.OtherLocation(location);
|
||||
if (!checkedLocations.Contains(otherLocation))
|
||||
{
|
||||
if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly NamedEvent<LocationChangeInfo> OnLocationChanged = new NamedEvent<LocationChangeInfo>();
|
||||
|
||||
public Location EndLocation { get; private set; }
|
||||
private List<Location> endLocations = new List<Location>();
|
||||
public IReadOnlyList<Location> EndLocations { get { return endLocations; } }
|
||||
|
||||
public Location StartLocation { get; private set; }
|
||||
|
||||
@@ -69,13 +70,13 @@ namespace Barotrauma
|
||||
public List<Location> Locations { get; private set; }
|
||||
|
||||
private readonly List<Location> locationsDiscovered = new List<Location>();
|
||||
private readonly List<Location> outpostsVisited = new List<Location>();
|
||||
private readonly List<Location> locationsVisited = new List<Location>();
|
||||
|
||||
public List<LocationConnection> Connections { get; private set; }
|
||||
|
||||
public Radiation Radiation;
|
||||
|
||||
private bool wasLocationDiscoveryOrderTracked = true;
|
||||
private bool trackedLocationDiscoveryAndVisitOrder = true;
|
||||
|
||||
public Map(CampaignSettings settings)
|
||||
{
|
||||
@@ -117,7 +118,7 @@ namespace Barotrauma
|
||||
Locations.Add(null);
|
||||
}
|
||||
lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase);
|
||||
Locations[i] = new Location(subElement);
|
||||
Locations[i] = new Location(campaign, subElement);
|
||||
break;
|
||||
case "radiation":
|
||||
Radiation = new Radiation(this, generationParams.RadiationParams, subElement)
|
||||
@@ -127,11 +128,6 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
|
||||
for (int i = 0; i < Locations.Count; i++)
|
||||
{
|
||||
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient));
|
||||
}
|
||||
|
||||
List<XElement> connectionElements = new List<XElement>();
|
||||
foreach (var subElement in element.Elements())
|
||||
@@ -187,23 +183,72 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
int endLocationindex = element.GetAttributeInt("endlocation", -1);
|
||||
if (endLocationindex >= 0 && endLocationindex < Locations.Count)
|
||||
|
||||
if (element.GetAttribute("endlocation") != null)
|
||||
{
|
||||
EndLocation = Locations[endLocationindex];
|
||||
//backwards compatibility
|
||||
int endLocationIndex = element.GetAttributeInt("endlocation", -1);
|
||||
if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
|
||||
{
|
||||
endLocations.Add(Locations[endLocationIndex]);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count}).");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationindex}, location count: {Locations.Count}).");
|
||||
foreach (Location location in Locations)
|
||||
int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty<int>());
|
||||
foreach (int endLocationIndex in endLocationindices)
|
||||
{
|
||||
if (EndLocation == null || location.MapPosition.X > EndLocation.MapPosition.X)
|
||||
if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
|
||||
{
|
||||
EndLocation = location;
|
||||
endLocations.Add(Locations[endLocationIndex]);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!endLocations.Any())
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading the map. No end location(s) found. Choosing the rightmost location as the end location...");
|
||||
Location endLocation = null;
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (endLocation == null || location.MapPosition.X > endLocation.MapPosition.X)
|
||||
{
|
||||
endLocation = location;
|
||||
}
|
||||
}
|
||||
endLocations.Add(endLocation);
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(endLocations.First().Biome != null, "End location biome was null.");
|
||||
System.Diagnostics.Debug.Assert(endLocations.First().Biome.IsEndBiome, "The biome of the end location isn't the end biome.");
|
||||
|
||||
//backwards compatibility (or support for loading maps created with mods that modify the end biome setup):
|
||||
//if there's too few end locations, create more
|
||||
int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count;
|
||||
|
||||
Location firstEndLocation = EndLocations[0];
|
||||
for (int i = 0; i < missingOutpostCount; i++)
|
||||
{
|
||||
Vector2 mapPos = new Vector2(
|
||||
MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
|
||||
Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
|
||||
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations)
|
||||
{
|
||||
Biome = endLocations.First().Biome
|
||||
};
|
||||
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
|
||||
Locations.Add(newEndLocation);
|
||||
endLocations.Add(newEndLocation);
|
||||
}
|
||||
|
||||
//backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds
|
||||
if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds))
|
||||
{
|
||||
@@ -214,6 +259,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var endLocation in EndLocations)
|
||||
{
|
||||
if (endLocation.Type?.ForceLocationName != null &&
|
||||
!endLocation.Type.ForceLocationName.IsNullOrEmpty())
|
||||
{
|
||||
endLocation.ForceName(endLocation.Type.ForceLocationName.Value);
|
||||
}
|
||||
}
|
||||
|
||||
AssignEndLocationLevelData();
|
||||
|
||||
//backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml)
|
||||
float maxX = Locations.Select(l => l.MapPosition.X).Max();
|
||||
if (maxX > Width) { Width = (int)(maxX + 10); }
|
||||
@@ -231,18 +287,13 @@ namespace Barotrauma
|
||||
Seed = seed;
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
|
||||
|
||||
Generate(campaign.Settings);
|
||||
Generate(campaign);
|
||||
|
||||
if (Locations.Count == 0)
|
||||
{
|
||||
throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < Locations.Count; i++)
|
||||
{
|
||||
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient));
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.Type.Identifier != "outpost") { continue; }
|
||||
@@ -263,6 +314,20 @@ namespace Barotrauma
|
||||
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
|
||||
{
|
||||
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
|
||||
StartLocation.SecondaryFaction = null;
|
||||
var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost);
|
||||
if (startOutpostFaction != null)
|
||||
{
|
||||
StartLocation.Faction = startOutpostFaction;
|
||||
foreach (var connection in StartLocation.Connections)
|
||||
{
|
||||
var otherLocation = connection.OtherLocation(StartLocation);
|
||||
if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
otherLocation.Faction = startOutpostFaction;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +338,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (StartLocation != null)
|
||||
{
|
||||
StartLocation.LevelData = new LevelData(StartLocation, 0);
|
||||
StartLocation.LevelData = new LevelData(StartLocation, this, 0);
|
||||
}
|
||||
|
||||
//ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
|
||||
@@ -289,7 +354,7 @@ namespace Barotrauma
|
||||
|
||||
if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost))
|
||||
{
|
||||
CurrentLocation.ChangeType(tutorialOutpost);
|
||||
CurrentLocation.ChangeType(campaign, tutorialOutpost);
|
||||
}
|
||||
Discover(CurrentLocation);
|
||||
Visit(CurrentLocation);
|
||||
@@ -307,7 +372,7 @@ namespace Barotrauma
|
||||
|
||||
#region Generation
|
||||
|
||||
private void Generate(CampaignSettings settings)
|
||||
private void Generate(CampaignMode campaign)
|
||||
{
|
||||
Connections.Clear();
|
||||
Locations.Clear();
|
||||
@@ -525,12 +590,14 @@ namespace Barotrauma
|
||||
connectionsBetweenZones[zone1].Add(connection);
|
||||
}
|
||||
}
|
||||
else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1])
|
||||
else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
|
||||
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
|
||||
{
|
||||
connectionsBetweenZones[zone1].Add(connection);
|
||||
}
|
||||
}
|
||||
|
||||
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
|
||||
for (int i = Connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X);
|
||||
@@ -538,9 +605,9 @@ namespace Barotrauma
|
||||
if (zone1 == zone2) { continue; }
|
||||
if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; }
|
||||
|
||||
if (generationParams.GateCount[Math.Min(zone1, zone2)] == 0) { continue; }
|
||||
|
||||
if (!connectionsBetweenZones[Math.Min(zone1, zone2)].Contains(Connections[i]))
|
||||
int leftZone = Math.Min(zone1, zone2);
|
||||
if (generationParams.GateCount[leftZone] == 0) { continue; }
|
||||
if (!connectionsBetweenZones[leftZone].Contains(Connections[i]))
|
||||
{
|
||||
Connections.RemoveAt(i);
|
||||
}
|
||||
@@ -552,11 +619,18 @@ namespace Barotrauma
|
||||
Connections[i].Locations[1];
|
||||
if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned")
|
||||
{
|
||||
leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"),
|
||||
leftMostLocation.ChangeType(
|
||||
campaign,
|
||||
LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"),
|
||||
createStores: false);
|
||||
}
|
||||
leftMostLocation.IsGateBetweenBiomes = true;
|
||||
Connections[i].Locked = true;
|
||||
|
||||
if (leftMostLocation.Type.HasOutpost && campaign != null && gateFactions.Any())
|
||||
{
|
||||
leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(Connections[i]) % gateFactions.Count];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,21 +698,27 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
CreateEndLocation();
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
|
||||
location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
location.CreateStores(force: true);
|
||||
}
|
||||
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
connection.LevelData = new LevelData(connection);
|
||||
}
|
||||
|
||||
CreateEndLocation(campaign);
|
||||
|
||||
float CalculateDifficulty(float mapPosition, Biome biome)
|
||||
{
|
||||
float settingsFactor = settings.LevelDifficultyMultiplier;
|
||||
float settingsFactor = campaign.Settings.LevelDifficultyMultiplier;
|
||||
float minDifficulty = 0;
|
||||
float maxDifficulty = 100;
|
||||
float difficulty = mapPosition / Width * 100;
|
||||
@@ -707,18 +787,18 @@ namespace Barotrauma
|
||||
System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
|
||||
}
|
||||
|
||||
private void CreateEndLocation()
|
||||
private void CreateEndLocation(CampaignMode campaign)
|
||||
{
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
Vector2 endPos = new Vector2(Width - zoneWidth / 2, Height / 2);
|
||||
Vector2 endPos = new Vector2(Width - zoneWidth * 0.7f, Height / 2);
|
||||
float closestDist = float.MaxValue;
|
||||
EndLocation = Locations.First();
|
||||
var endLocation = Locations.First();
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(endPos, location.MapPosition);
|
||||
if (location.Biome.IsEndBiome && dist < closestDist)
|
||||
{
|
||||
EndLocation = location;
|
||||
endLocation = location;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
@@ -732,17 +812,39 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (EndLocation == null || previousToEndLocation == null) { return; }
|
||||
if (endLocation == null || previousToEndLocation == null) { return; }
|
||||
|
||||
endLocations = new List<Location>() { endLocation };
|
||||
if (endLocation.Biome.EndBiomeLocationCount > 1)
|
||||
{
|
||||
FindConnectedEndLocations(endLocation);
|
||||
|
||||
void FindConnectedEndLocations(Location currLocation)
|
||||
{
|
||||
if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; }
|
||||
foreach (var connection in currLocation.Connections)
|
||||
{
|
||||
if (connection.Biome != endLocation.Biome) { continue; }
|
||||
var otherLocation = connection.OtherLocation(currLocation);
|
||||
if (otherLocation != null && !endLocations.Contains(otherLocation))
|
||||
{
|
||||
if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; }
|
||||
endLocations.Add(otherLocation);
|
||||
FindConnectedEndLocations(otherLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
|
||||
{
|
||||
previousToEndLocation.ChangeType(locationType, createStores: false);
|
||||
previousToEndLocation.ChangeType(campaign, locationType, createStores: false);
|
||||
}
|
||||
|
||||
//remove all locations from the end biome except the end location
|
||||
for (int i = Locations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Locations[i].Biome.IsEndBiome && Locations[i] != EndLocation)
|
||||
if (Locations[i].Biome.IsEndBiome)
|
||||
{
|
||||
for (int j = Locations[i].Connections.Count - 1; j >= 0; j--)
|
||||
{
|
||||
@@ -753,7 +855,10 @@ namespace Barotrauma
|
||||
otherLocation?.Connections.Remove(connection);
|
||||
Connections.Remove(connection);
|
||||
}
|
||||
Locations.RemoveAt(i);
|
||||
if (!endLocations.Contains(Locations[i]))
|
||||
{
|
||||
Locations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,22 +875,39 @@ namespace Barotrauma
|
||||
}
|
||||
var newConnection = new LocationConnection(previousToEndLocation, connectTo)
|
||||
{
|
||||
Biome = EndLocation.Biome,
|
||||
Biome = endLocation.Biome,
|
||||
Difficulty = 100.0f
|
||||
};
|
||||
newConnection.LevelData = new LevelData(newConnection);
|
||||
Connections.Add(newConnection);
|
||||
previousToEndLocation.Connections.Add(newConnection);
|
||||
connectTo.Connections.Add(newConnection);
|
||||
}
|
||||
|
||||
var endConnection = new LocationConnection(previousToEndLocation, EndLocation)
|
||||
var endConnection = new LocationConnection(previousToEndLocation, endLocation)
|
||||
{
|
||||
Biome = EndLocation.Biome,
|
||||
Biome = endLocation.Biome,
|
||||
Difficulty = 100.0f
|
||||
};
|
||||
endConnection.LevelData = new LevelData(endConnection);
|
||||
Connections.Add(endConnection);
|
||||
previousToEndLocation.Connections.Add(endConnection);
|
||||
EndLocation.Connections.Add(endConnection);
|
||||
endLocation.Connections.Add(endConnection);
|
||||
|
||||
AssignEndLocationLevelData();
|
||||
}
|
||||
|
||||
private void AssignEndLocationLevelData()
|
||||
{
|
||||
for (int i = 0; i < endLocations.Count; i++)
|
||||
{
|
||||
endLocations[i].LevelData.ReassignGenerationParams(Seed);
|
||||
var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
|
||||
if (outpostParams != null)
|
||||
{
|
||||
endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpandBiomes(List<LocationConnection> seeds)
|
||||
@@ -817,19 +939,48 @@ namespace Barotrauma
|
||||
|
||||
public void MoveToNextLocation()
|
||||
{
|
||||
if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
|
||||
{
|
||||
//force the location at the end of the level to be selected, even if it's been deselect on the map
|
||||
//(e.g. due to returning to an empty location the beginning of the level during the round)
|
||||
SelectLocation(Level.Loaded.EndLocation);
|
||||
}
|
||||
if (SelectedConnection == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
if (!endLocations.Contains(CurrentLocation))
|
||||
{
|
||||
DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (SelectedLocation == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
if (endLocations.Contains(CurrentLocation))
|
||||
{
|
||||
int currentEndLocationIndex = endLocations.IndexOf(CurrentLocation);
|
||||
if (currentEndLocationIndex < endLocations.Count - 1)
|
||||
{
|
||||
//more end locations to go, progress to the next one
|
||||
SelectedLocation = endLocations[currentEndLocationIndex + 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
//at the last end location, end of campaign
|
||||
SelectedLocation = StartLocation;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Location prevLocation = CurrentLocation;
|
||||
SelectedConnection.Passed = true;
|
||||
if (SelectedConnection != null)
|
||||
{
|
||||
SelectedConnection.Passed = true;
|
||||
}
|
||||
|
||||
CurrentLocation = SelectedLocation;
|
||||
Discover(CurrentLocation);
|
||||
@@ -898,12 +1049,24 @@ namespace Barotrauma
|
||||
Location prevSelected = SelectedLocation;
|
||||
SelectedLocation = Locations[index];
|
||||
var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
|
||||
SelectedConnection =
|
||||
Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
|
||||
Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
if (currentDisplayLocation == SelectedLocation)
|
||||
{
|
||||
SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedConnection =
|
||||
Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
|
||||
Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
}
|
||||
if (SelectedConnection?.Locked ?? false)
|
||||
{
|
||||
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
string errorMsg =
|
||||
$"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." +
|
||||
$" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
|
||||
+ Environment.StackTrace.CleanupStackTrace();
|
||||
GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
}
|
||||
if (prevSelected != SelectedLocation)
|
||||
{
|
||||
@@ -979,7 +1142,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ProgressWorld(CampaignMode.TransitionType transitionType, float roundDuration)
|
||||
public void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration)
|
||||
{
|
||||
//one step per 10 minutes of play time
|
||||
int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f));
|
||||
@@ -992,7 +1155,7 @@ namespace Barotrauma
|
||||
steps = Math.Min(steps, 5);
|
||||
for (int i = 0; i < steps; i++)
|
||||
{
|
||||
ProgressWorld();
|
||||
ProgressWorld(campaign);
|
||||
}
|
||||
|
||||
// always update specials every step
|
||||
@@ -1008,7 +1171,7 @@ namespace Barotrauma
|
||||
Radiation?.OnStep(steps);
|
||||
}
|
||||
|
||||
private void ProgressWorld()
|
||||
private void ProgressWorld(CampaignMode campaign)
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
@@ -1036,14 +1199,14 @@ namespace Barotrauma
|
||||
|
||||
if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; }
|
||||
|
||||
if (!ProgressLocationTypeChanges(location) && location.Discovered)
|
||||
if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
|
||||
{
|
||||
location.UpdateStores();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ProgressLocationTypeChanges(Location location)
|
||||
private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location)
|
||||
{
|
||||
location.TimeSinceLastTypeChange++;
|
||||
location.LocationTypeChangeCooldown--;
|
||||
@@ -1063,7 +1226,7 @@ namespace Barotrauma
|
||||
location.PendingLocationTypeChange.Value.parentMission);
|
||||
if (location.PendingLocationTypeChange.Value.delay <= 0)
|
||||
{
|
||||
return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
|
||||
return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1096,7 +1259,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return ChangeLocationType(location, selectedTypeChange);
|
||||
return ChangeLocationType(campaign, location, selectedTypeChange);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1121,52 +1284,7 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
|
||||
{
|
||||
if (startingLocation.Type.HasOutpost)
|
||||
{
|
||||
endingLocation = startingLocation;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int iterations = 0;
|
||||
int distance = 0;
|
||||
endingLocation = null;
|
||||
|
||||
List<Location> testedLocations = new List<Location>();
|
||||
List<Location> locationsToTest = new List<Location> { startingLocation };
|
||||
|
||||
while (endingLocation == null && iterations < 100)
|
||||
{
|
||||
List<Location> nextTestingBatch = new List<Location>();
|
||||
for (int i = 0; i < locationsToTest.Count; i++)
|
||||
{
|
||||
Location testLocation = locationsToTest[i];
|
||||
for (int j = 0; j < testLocation.Connections.Count; j++)
|
||||
{
|
||||
Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation);
|
||||
if (potentialOutpost.Type.HasOutpost)
|
||||
{
|
||||
distance = iterations + 1;
|
||||
endingLocation = potentialOutpost;
|
||||
}
|
||||
else if (!testedLocations.Contains(potentialOutpost))
|
||||
{
|
||||
nextTestingBatch.Add(potentialOutpost);
|
||||
}
|
||||
}
|
||||
|
||||
testedLocations.Add(testLocation);
|
||||
}
|
||||
|
||||
locationsToTest = nextTestingBatch;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
private bool ChangeLocationType(Location location, LocationTypeChange change)
|
||||
private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
|
||||
{
|
||||
string prevName = location.Name;
|
||||
|
||||
@@ -1176,12 +1294,14 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
if (location.LocationTypeChangesBlocked) { return false; }
|
||||
|
||||
if (newType.OutpostTeam != location.Type.OutpostTeam ||
|
||||
newType.HasOutpost != location.Type.HasOutpost)
|
||||
{
|
||||
location.ClearMissions();
|
||||
}
|
||||
location.ChangeType(newType);
|
||||
location.ChangeType(campaign, newType);
|
||||
ChangeLocationTypeProjSpecific(location, prevName, change);
|
||||
foreach (var requirement in change.Requirements)
|
||||
{
|
||||
@@ -1193,6 +1313,50 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
|
||||
{
|
||||
return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the shortest distance from the start location to another location that satisfies the specified criteria.
|
||||
/// </summary>
|
||||
/// <returns>The distance to a matching location, or int.MaxValue if none are found.</returns>
|
||||
public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func<Location, bool> criteria, Func<LocationConnection, bool> connectionCriteria = null)
|
||||
{
|
||||
int distance = 0;
|
||||
var locationsToTest = new List<Location>() { startLocation };
|
||||
var nextBatchToTest = new HashSet<Location>();
|
||||
var checkedLocations = new HashSet<Location>();
|
||||
while (locationsToTest.Any())
|
||||
{
|
||||
foreach (var location in locationsToTest)
|
||||
{
|
||||
checkedLocations.Add(location);
|
||||
if (criteria(location)) { return distance; }
|
||||
foreach (var connection in location.Connections)
|
||||
{
|
||||
if (connectionCriteria != null && connectionCriteria(connection))
|
||||
{
|
||||
return distance;
|
||||
}
|
||||
var otherLocation = connection.OtherLocation(location);
|
||||
if (!checkedLocations.Contains(otherLocation))
|
||||
{
|
||||
nextBatchToTest.Add(otherLocation);
|
||||
}
|
||||
}
|
||||
if (distance > maxDistance) { return int.MaxValue; }
|
||||
}
|
||||
distance++;
|
||||
locationsToTest.Clear();
|
||||
locationsToTest.AddRange(nextBatchToTest);
|
||||
nextBatchToTest.Clear();
|
||||
}
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
|
||||
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
|
||||
|
||||
partial void ClearAnimQueue();
|
||||
@@ -1211,29 +1375,38 @@ namespace Barotrauma
|
||||
public void Visit(Location location)
|
||||
{
|
||||
if (location is null) { return; }
|
||||
if (!location.HasOutpost()) { return; }
|
||||
if (outpostsVisited.Contains(location)) { return; }
|
||||
outpostsVisited.Add(location);
|
||||
if (locationsVisited.Contains(location)) { return; }
|
||||
locationsVisited.Add(location);
|
||||
RemoveFogOfWarProjSpecific(location);
|
||||
}
|
||||
|
||||
public void ClearLocationHistory()
|
||||
{
|
||||
locationsDiscovered.Clear();
|
||||
outpostsVisited.Clear();
|
||||
locationsVisited.Clear();
|
||||
}
|
||||
|
||||
public int? GetDiscoveryIndex(Location location)
|
||||
{
|
||||
if (!wasLocationDiscoveryOrderTracked) { return null; }
|
||||
if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
|
||||
if (location is null) { return -1; }
|
||||
return locationsDiscovered.IndexOf(location);
|
||||
}
|
||||
|
||||
public int? GetVisitIndex(Location location)
|
||||
public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false)
|
||||
{
|
||||
if (!wasLocationDiscoveryOrderTracked) { return null; }
|
||||
if (!trackedLocationDiscoveryAndVisitOrder) { return null; }
|
||||
if (location is null) { return -1; }
|
||||
return outpostsVisited.IndexOf(location);
|
||||
int index = locationsVisited.IndexOf(location);
|
||||
if (includeLocationsWithoutOutpost) { return index; }
|
||||
int noOutpostLocations = 0;
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
if (locationsVisited[i] is not Location l) { continue; }
|
||||
if (l.HasOutpost()) { continue; }
|
||||
noOutpostLocations++;
|
||||
}
|
||||
return index - noOutpostLocations;
|
||||
}
|
||||
|
||||
public bool IsDiscovered(Location location)
|
||||
@@ -1242,13 +1415,21 @@ namespace Barotrauma
|
||||
return locationsDiscovered.Contains(location);
|
||||
}
|
||||
|
||||
public bool IsVisited(Location location)
|
||||
{
|
||||
if (location is null) { return false; }
|
||||
return locationsVisited.Contains(location);
|
||||
}
|
||||
|
||||
partial void RemoveFogOfWarProjSpecific(Location location);
|
||||
|
||||
/// <summary>
|
||||
/// Load a previously saved map from an xml element
|
||||
/// </summary>
|
||||
public static Map Load(CampaignMode campaign, XElement element)
|
||||
{
|
||||
Map map = new Map(campaign, element);
|
||||
map.LoadState(element, false);
|
||||
map.LoadState(campaign, element, false);
|
||||
#if CLIENT
|
||||
map.DrawOffset = -map.CurrentLocation.MapPosition;
|
||||
#endif
|
||||
@@ -1258,12 +1439,12 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Load the state of an existing map from xml (current state of locations, where the crew is now, etc).
|
||||
/// </summary>
|
||||
public void LoadState(XElement element, bool showNotifications)
|
||||
public void LoadState(CampaignMode campaign, XElement element, bool showNotifications)
|
||||
{
|
||||
ClearAnimQueue();
|
||||
SetLocation(element.GetAttributeInt("currentlocation", 0));
|
||||
|
||||
if (!Version.TryParse(element.GetAttributeString("version", ""), out _))
|
||||
if (!Version.TryParse(element.GetAttributeString("version", ""), out Version version))
|
||||
{
|
||||
DebugConsole.ThrowError("Incompatible map save file, loading the game failed.");
|
||||
return;
|
||||
@@ -1275,7 +1456,13 @@ namespace Barotrauma
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "location":
|
||||
Location location = Locations[subElement.GetAttributeInt("i", 0)];
|
||||
int locationIndex = subElement.GetAttributeInt("i", -1);
|
||||
if (locationIndex < 0 || locationIndex >= Locations.Count)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading the campaign map: location index out of bounds ({locationIndex})");
|
||||
continue;
|
||||
}
|
||||
Location location = Locations[locationIndex];
|
||||
location.ProximityTimer.Clear();
|
||||
for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
|
||||
{
|
||||
@@ -1286,18 +1473,20 @@ namespace Barotrauma
|
||||
}
|
||||
location.LoadLocationTypeChange(subElement);
|
||||
|
||||
// Backwards compatibility
|
||||
// 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);
|
||||
wasLocationDiscoveryOrderTracked = false;
|
||||
Visit(location);
|
||||
trackedLocationDiscoveryAndVisitOrder = false;
|
||||
}
|
||||
|
||||
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
|
||||
string prevLocationName = location.Name;
|
||||
LocationType prevLocationType = location.Type;
|
||||
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
|
||||
location.ChangeType(newLocationType);
|
||||
location.ChangeType(campaign, newLocationType);
|
||||
if (showNotifications && prevLocationType != location.Type)
|
||||
{
|
||||
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier);
|
||||
@@ -1308,45 +1497,72 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
|
||||
|
||||
var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
|
||||
location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
|
||||
|
||||
location.LoadStores(subElement);
|
||||
location.LoadMissions(subElement);
|
||||
|
||||
break;
|
||||
case "connection":
|
||||
int connectionIndex = subElement.GetAttributeInt("i", 0);
|
||||
//the index wasn't saved previously, skip if that's the case
|
||||
if (subElement.Attribute("i") == null) { continue; }
|
||||
|
||||
int connectionIndex = subElement.GetAttributeInt("i", -1);
|
||||
if (connectionIndex < 0 || connectionIndex >= Connections.Count)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading the campaign map: connection index out of bounds ({connectionIndex})");
|
||||
continue;
|
||||
}
|
||||
Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
|
||||
Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false);
|
||||
Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false);
|
||||
break;
|
||||
case "radiation":
|
||||
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
|
||||
break;
|
||||
case "discovered":
|
||||
bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false);
|
||||
foreach (var childElement in subElement.GetChildElements("location"))
|
||||
{
|
||||
int index = childElement.GetAttributeInt("i", -1);
|
||||
if (index < 0) { continue; }
|
||||
if (Locations[index] is not Location l) { continue; }
|
||||
Discover(l);
|
||||
if (GetLocation(childElement) is Location l)
|
||||
{
|
||||
Discover(l);
|
||||
if (!trackedVisitedEmptyLocations)
|
||||
{
|
||||
if (!l.HasOutpost())
|
||||
{
|
||||
Visit(l);
|
||||
}
|
||||
trackedLocationDiscoveryAndVisitOrder = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "visited":
|
||||
foreach (var childElement in subElement.GetChildElements("location"))
|
||||
{
|
||||
int index = childElement.GetAttributeInt("i", -1);
|
||||
if (index < 0) { continue; }
|
||||
if (Locations[index] is not Location l) { continue; }
|
||||
Visit(l);
|
||||
if (GetLocation(childElement) is Location l)
|
||||
{
|
||||
Visit(l);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Location GetLocation(XElement element)
|
||||
{
|
||||
int index = element.GetAttributeInt("i", -1);
|
||||
if (index < 0) { return null; }
|
||||
return Locations[index];
|
||||
}
|
||||
}
|
||||
|
||||
void Discover(Location location)
|
||||
{
|
||||
this.Discover(location, checkTalents: false);
|
||||
#if CLIENT
|
||||
RemoveFogOfWar(location);
|
||||
#endif
|
||||
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
|
||||
{
|
||||
furthestDiscoveredLocation = location;
|
||||
@@ -1358,6 +1574,24 @@ namespace Barotrauma
|
||||
location?.InstantiateLoadedMissions(this);
|
||||
}
|
||||
|
||||
//backwards compatibility:
|
||||
//if the save is from a version prior to the addition of faction-specific outposts, assign factions
|
||||
if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null))
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
location.Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
|
||||
if (location != StartLocation)
|
||||
{
|
||||
location.SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1);
|
||||
if (currentLocationConnection >= 0)
|
||||
{
|
||||
@@ -1396,7 +1630,7 @@ namespace Barotrauma
|
||||
mapElement.Add(new XAttribute("height", Height));
|
||||
mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex));
|
||||
mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation)));
|
||||
mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation)));
|
||||
mapElement.Add(new XAttribute("endlocations", string.Join(',', EndLocations.Select(e => Locations.IndexOf(e)))));
|
||||
mapElement.Add(new XAttribute("seed", Seed));
|
||||
|
||||
for (int i = 0; i < Locations.Count; i++)
|
||||
@@ -1415,6 +1649,7 @@ namespace Barotrauma
|
||||
new XAttribute("locked", connection.Locked),
|
||||
new XAttribute("difficulty", connection.Difficulty),
|
||||
new XAttribute("biome", connection.Biome.Identifier),
|
||||
new XAttribute("i", i),
|
||||
new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1])));
|
||||
connection.LevelData.Save(connectionElement);
|
||||
mapElement.Add(connectionElement);
|
||||
@@ -1427,7 +1662,8 @@ namespace Barotrauma
|
||||
|
||||
if (locationsDiscovered.Any())
|
||||
{
|
||||
var discoveryElement = new XElement("discovered");
|
||||
var discoveryElement = new XElement("discovered",
|
||||
new XAttribute("trackedvisitedemptylocations", true));
|
||||
foreach (Location location in locationsDiscovered)
|
||||
{
|
||||
int index = Locations.IndexOf(location);
|
||||
@@ -1437,10 +1673,10 @@ namespace Barotrauma
|
||||
mapElement.Add(discoveryElement);
|
||||
}
|
||||
|
||||
if (outpostsVisited.Any())
|
||||
if (locationsVisited.Any())
|
||||
{
|
||||
var visitElement = new XElement("visited");
|
||||
foreach (Location location in outpostsVisited)
|
||||
foreach (Location location in locationsVisited)
|
||||
{
|
||||
int index = Locations.IndexOf(location);
|
||||
var locationElement = new XElement("location", new XAttribute("i", index));
|
||||
|
||||
@@ -19,9 +19,6 @@ namespace Barotrauma
|
||||
protected List<ushort> linkedToID;
|
||||
public List<ushort> unresolvedLinkedToID;
|
||||
|
||||
private const int GapUpdateInterval = 4;
|
||||
private static int gapUpdateTimer;
|
||||
|
||||
/// <summary>
|
||||
/// List of upgrades this item has
|
||||
/// </summary>
|
||||
@@ -59,7 +56,24 @@ namespace Barotrauma
|
||||
//the position and dimensions of the entity
|
||||
protected Rectangle rect;
|
||||
|
||||
public bool ExternalHighlight = false;
|
||||
protected static readonly HashSet<MapEntity> highlightedEntities = new HashSet<MapEntity>();
|
||||
|
||||
public static IEnumerable<MapEntity> HighlightedEntities => highlightedEntities;
|
||||
|
||||
|
||||
private bool externalHighlight = false;
|
||||
public bool ExternalHighlight
|
||||
{
|
||||
get { return externalHighlight; }
|
||||
set
|
||||
{
|
||||
if (value != externalHighlight)
|
||||
{
|
||||
externalHighlight = value;
|
||||
CheckIsHighlighted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//is the mouse inside the rect
|
||||
private bool isHighlighted;
|
||||
@@ -67,7 +81,14 @@ namespace Barotrauma
|
||||
public bool IsHighlighted
|
||||
{
|
||||
get { return isHighlighted || ExternalHighlight; }
|
||||
set { isHighlighted = value; }
|
||||
set
|
||||
{
|
||||
if (value != IsHighlighted)
|
||||
{
|
||||
isHighlighted = value;
|
||||
CheckIsHighlighted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Rectangle Rect
|
||||
@@ -158,7 +179,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!float.IsNaN(value))
|
||||
{
|
||||
_spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f);
|
||||
_spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999999f);
|
||||
if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); }
|
||||
SpriteDepthOverrideIsSet = true;
|
||||
}
|
||||
@@ -362,6 +383,31 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void CheckIsHighlighted()
|
||||
{
|
||||
if (IsHighlighted || ExternalHighlight)
|
||||
{
|
||||
highlightedEntities.Add(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
highlightedEntities.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly List<MapEntity> tempHighlightedEntities = new List<MapEntity>();
|
||||
public static void ClearHighlightedEntities()
|
||||
{
|
||||
highlightedEntities.RemoveWhere(e => e.Removed);
|
||||
tempHighlightedEntities.Clear();
|
||||
tempHighlightedEntities.AddRange(highlightedEntities);
|
||||
foreach (var entity in tempHighlightedEntities)
|
||||
{
|
||||
entity.IsHighlighted = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public abstract MapEntity Clone();
|
||||
|
||||
public static List<MapEntity> Clone(List<MapEntity> entitiesToClone)
|
||||
@@ -458,7 +504,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
(clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire);
|
||||
cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false);
|
||||
cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], n, addNode: false);
|
||||
}
|
||||
|
||||
if ((cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent<DockingPort>() == null)
|
||||
@@ -579,14 +625,9 @@ namespace Barotrauma
|
||||
//the water/air will always tend to flow through the first gap in the list,
|
||||
//which may lead to weird behavior like water draining down only through
|
||||
//one gap in a room even if there are several
|
||||
gapUpdateTimer++;
|
||||
if (gapUpdateTimer >= GapUpdateInterval)
|
||||
foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue)))
|
||||
{
|
||||
foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue)))
|
||||
{
|
||||
gap.Update(deltaTime * GapUpdateInterval, cam);
|
||||
}
|
||||
gapUpdateTimer = 0;
|
||||
gap.Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
@@ -649,6 +690,9 @@ namespace Barotrauma
|
||||
List<MapEntity> entities = new List<MapEntity>();
|
||||
foreach (var element in parentElement.Elements())
|
||||
{
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested();
|
||||
#endif
|
||||
string typeName = element.Name.ToString();
|
||||
|
||||
Type t;
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace Barotrauma
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float MaxLevelDifficulty { get; set; }
|
||||
|
||||
[Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable]
|
||||
public Level.PlacementType Placement { get; set; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -12,23 +8,23 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly static PrefabCollection<NPCSet> Sets = new PrefabCollection<NPCSet>();
|
||||
|
||||
|
||||
private readonly ImmutableArray<HumanPrefab> Humans;
|
||||
|
||||
private bool Disposed { get; set; }
|
||||
|
||||
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)
|
||||
public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier, bool logError = true)
|
||||
{
|
||||
HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault();
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\".");
|
||||
if (logError)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\".");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return prefab;
|
||||
|
||||
@@ -23,77 +23,106 @@ namespace Barotrauma
|
||||
get { return allowedLocationTypes; }
|
||||
}
|
||||
|
||||
[Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)]
|
||||
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)]
|
||||
public int ForceToEndLocationIndex
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
[Serialize(10, IsPropertySaveable.Yes, description: "Total number of modules in the outpost."), Editable(MinValueInt = 1, MaxValueInt = 50)]
|
||||
public int TotalModuleCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the generator append generic (module flag \"none\") modules to the outpost to reach the total module count."), Editable]
|
||||
public bool AppendToReachTotalModuleCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(200.0f, IsPropertySaveable.Yes, description: "Minimum length of the hallways between modules. If 0, the generator will place the modules directly against each other assuming it can be done without making any modules overlap."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float MinHallwayLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be destructible, regardless if damaging outposts is allowed by the server?"), Editable]
|
||||
public bool AlwaysDestructible
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be rewireable, regardless if rewiring is allowed by the server?"), Editable]
|
||||
public bool AlwaysRewireable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should stealing from this outpost be always allowed?"), Editable]
|
||||
public bool AllowStealing
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the crew spawn inside the outpost (if not, they'll spawn in the submarine)."), Editable]
|
||||
public bool SpawnCrewInsideOutpost
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should doors at the edges of an outpost module that didn't get connected to another module be locked?"), Editable]
|
||||
public bool LockUnusedDoors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should gaps at the edges of an outpost module that didn't get connected to another module be removed?"), Editable]
|
||||
public bool RemoveUnusedGaps
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the whole outpost render behind submarines? Only set this to true if the submarine is intended to go inside the outpost."), Editable]
|
||||
public bool DrawBehindSubs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
|
||||
public float MinWaterPercentage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Maximum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
|
||||
public float MaxWaterPercentage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes), Editable]
|
||||
public LevelData.LevelType? LevelType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable]
|
||||
public string ReplaceInRadiation { get; set; }
|
||||
|
||||
public ContentPath OutpostFilePath { get; set; }
|
||||
@@ -104,17 +133,21 @@ namespace Barotrauma
|
||||
public int Count;
|
||||
public int Order;
|
||||
|
||||
public Identifier RequiredFaction;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public ModuleCount(Identifier id, int count)
|
||||
{
|
||||
Identifier = id;
|
||||
Count = count;
|
||||
RequiredFaction = Identifier.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,16 +165,20 @@ namespace Barotrauma
|
||||
private readonly HumanPrefab humanPrefab = null;
|
||||
private readonly Identifier setIdentifier = Identifier.Empty;
|
||||
private readonly Identifier npcIdentifier = Identifier.Empty;
|
||||
|
||||
public readonly Identifier FactionIdentifier = Identifier.Empty;
|
||||
|
||||
public Entry(HumanPrefab humanPrefab)
|
||||
public Entry(HumanPrefab humanPrefab, Identifier factionIdentifier)
|
||||
{
|
||||
this.humanPrefab = humanPrefab;
|
||||
this.FactionIdentifier = factionIdentifier;
|
||||
}
|
||||
|
||||
public Entry(Identifier setIdentifier, Identifier npcIdentifier)
|
||||
public Entry(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier)
|
||||
{
|
||||
this.setIdentifier = setIdentifier;
|
||||
this.npcIdentifier = npcIdentifier;
|
||||
this.FactionIdentifier = factionIdentifier;
|
||||
}
|
||||
|
||||
public HumanPrefab HumanPrefab
|
||||
@@ -150,29 +187,41 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<Entry> entries = new List<Entry>();
|
||||
|
||||
public void Add(HumanPrefab humanPrefab)
|
||||
=> entries.Add(new Entry(humanPrefab));
|
||||
public void Add(HumanPrefab humanPrefab, Identifier factionIdentifier)
|
||||
=> entries.Add(new Entry(humanPrefab, factionIdentifier));
|
||||
|
||||
|
||||
public void Add(Identifier setIdentifier, Identifier npcIdentifier)
|
||||
=> entries.Add(new Entry(setIdentifier, npcIdentifier));
|
||||
public void Add(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier)
|
||||
=> entries.Add(new Entry(setIdentifier, npcIdentifier, factionIdentifier));
|
||||
|
||||
public IEnumerator<HumanPrefab> GetEnumerator()
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry == null) { continue; }
|
||||
yield return entry.HumanPrefab;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IEnumerable<HumanPrefab> GetByFaction(IEnumerable<FactionPrefab> factions)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.FactionIdentifier == Identifier.Empty || factions.Any(f => f.Identifier == entry.FactionIdentifier))
|
||||
{
|
||||
yield return entry.HumanPrefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => entries.Count;
|
||||
|
||||
public HumanPrefab this[int index] => entries[index].HumanPrefab;
|
||||
}
|
||||
|
||||
private readonly ImmutableArray<IReadOnlyList<HumanPrefab>> humanPrefabCollections;
|
||||
private readonly ImmutableArray<NpcCollection> humanPrefabCollections;
|
||||
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
|
||||
|
||||
@@ -184,9 +233,23 @@ namespace Barotrauma
|
||||
Name = element.GetAttributeString("name", Identifier.Value);
|
||||
allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()).ToHashSet();
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
if (element.GetAttribute("leveltype") != null)
|
||||
{
|
||||
string levelTypeStr = element.GetAttributeString("leveltype", "");
|
||||
if (Enum.TryParse(levelTypeStr, out LevelData.LevelType parsedLevelType))
|
||||
{
|
||||
LevelType = parsedLevelType;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.");
|
||||
}
|
||||
}
|
||||
|
||||
OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath));
|
||||
|
||||
var humanPrefabCollections = new List<IReadOnlyList<HumanPrefab>>();
|
||||
var humanPrefabCollections = new List<NpcCollection>();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -199,14 +262,14 @@ namespace Barotrauma
|
||||
foreach (var npcElement in subElement.Elements())
|
||||
{
|
||||
Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty);
|
||||
|
||||
Identifier faction = npcElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
if (from != Identifier.Empty)
|
||||
{
|
||||
newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty));
|
||||
newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty), faction);
|
||||
}
|
||||
else
|
||||
{
|
||||
newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from));
|
||||
newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from), faction);
|
||||
}
|
||||
}
|
||||
humanPrefabCollections.Add(newCollection);
|
||||
@@ -254,10 +317,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(Rand.RandSync randSync)
|
||||
public IReadOnlyList<HumanPrefab> GetHumanPrefabs(IEnumerable<FactionPrefab> factions, Rand.RandSync randSync)
|
||||
{
|
||||
if (!humanPrefabCollections.Any()) { return Array.Empty<HumanPrefab>(); }
|
||||
return humanPrefabCollections.GetRandom(randSync);
|
||||
|
||||
var collection = humanPrefabCollections.GetRandom(randSync);
|
||||
return collection.GetByFaction(factions).ToImmutableList();
|
||||
}
|
||||
|
||||
public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
|
||||
@@ -266,7 +331,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var prefab in collection)
|
||||
{
|
||||
if (prefab.CampaignInteractionType == interactionType)
|
||||
if (prefab != null && prefab.CampaignInteractionType == interactionType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -150,11 +150,14 @@ namespace Barotrauma
|
||||
|
||||
selectedModules.Clear();
|
||||
//select which module types the outpost should consist of
|
||||
List<Identifier> pendingModuleFlags =
|
||||
onlyEntrance ?
|
||||
(generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty<Identifier>()).ToList() :
|
||||
SelectModules(outpostModules, generationParams);
|
||||
|
||||
List<Identifier> pendingModuleFlags = new List<Identifier>();
|
||||
if (generationParams.ModuleCounts.Any())
|
||||
{
|
||||
pendingModuleFlags = onlyEntrance ?
|
||||
generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() :
|
||||
SelectModules(outpostModules, location, generationParams);
|
||||
}
|
||||
|
||||
foreach (Identifier flag in pendingModuleFlags)
|
||||
{
|
||||
if (flag == "none") { continue; }
|
||||
@@ -246,12 +249,17 @@ namespace Barotrauma
|
||||
wp.FindHull();
|
||||
}
|
||||
}
|
||||
EnableFactionSpecificEntities(sub, location);
|
||||
return sub;
|
||||
}
|
||||
remainingTries--;
|
||||
}
|
||||
|
||||
#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>())
|
||||
@@ -268,6 +276,7 @@ namespace Barotrauma
|
||||
sub = new Submarine(prebuiltOutpostInfo);
|
||||
sub.Info.OutpostGenerationParams = generationParams;
|
||||
location?.RemoveTakenItems();
|
||||
EnableFactionSpecificEntities(sub, location);
|
||||
return sub;
|
||||
|
||||
List<MapEntity> loadEntities(Submarine sub)
|
||||
@@ -297,7 +306,7 @@ namespace Barotrauma
|
||||
}
|
||||
idOffset = moduleEntities.Max(e => e.ID) + 1;
|
||||
|
||||
var wallEntities = moduleEntities.Where(e => e is Structure).Cast<Structure>();
|
||||
var wallEntities = moduleEntities.Where(e => e is Structure s && s.HasBody).Cast<Structure>();
|
||||
var hullEntities = moduleEntities.Where(e => e is Hull).Cast<Hull>();
|
||||
|
||||
// Tell the hulls what tags the module has, used to spawn NPCs on specific rooms
|
||||
@@ -306,18 +315,27 @@ namespace Barotrauma
|
||||
hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags);
|
||||
}
|
||||
|
||||
selectedModule.HullBounds = new Rectangle(
|
||||
hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height),
|
||||
hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y));
|
||||
selectedModule.HullBounds = new Rectangle(
|
||||
selectedModule.HullBounds.X, selectedModule.HullBounds.Y,
|
||||
selectedModule.HullBounds.Width - selectedModule.HullBounds.X, selectedModule.HullBounds.Height - selectedModule.HullBounds.Y);
|
||||
selectedModule.Bounds = new Rectangle(
|
||||
wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height),
|
||||
wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y));
|
||||
selectedModule.Bounds = new Rectangle(
|
||||
selectedModule.Bounds.X, selectedModule.Bounds.Y,
|
||||
selectedModule.Bounds.Width - selectedModule.Bounds.X, selectedModule.Bounds.Height - selectedModule.Bounds.Y);
|
||||
if (!hullEntities.Any())
|
||||
{
|
||||
selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint());
|
||||
}
|
||||
else
|
||||
{
|
||||
Point min = new Point(hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height));
|
||||
Point max = new Point(hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y));
|
||||
selectedModule.HullBounds = new Rectangle(min, max - min);
|
||||
}
|
||||
|
||||
if (!wallEntities.Any())
|
||||
{
|
||||
selectedModule.Bounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint());
|
||||
}
|
||||
else
|
||||
{
|
||||
Point min = new Point(wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height));
|
||||
Point max = new Point(wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y));
|
||||
selectedModule.Bounds = new Rectangle(min, max - min);
|
||||
}
|
||||
|
||||
if (selectedModule.PreviousModule != null)
|
||||
{
|
||||
@@ -406,6 +424,23 @@ namespace Barotrauma
|
||||
{
|
||||
LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps);
|
||||
}
|
||||
if (generationParams.DrawBehindSubs)
|
||||
{
|
||||
foreach (var entity in allEntities)
|
||||
{
|
||||
if (entity is Structure structure)
|
||||
{
|
||||
//eww
|
||||
structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth);
|
||||
#if CLIENT
|
||||
foreach (var light in structure.Lights)
|
||||
{
|
||||
light.IsBackground = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
AlignLadders(selectedModules, entities);
|
||||
PowerUpOutpost(entities.SelectMany(e => e.Value));
|
||||
if (generationParams.MaxWaterPercentage > 0.0f)
|
||||
@@ -436,7 +471,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Select the number and types of the modules to use in the outpost
|
||||
/// </summary>
|
||||
private static List<Identifier> SelectModules(IEnumerable<SubmarineInfo> modules, OutpostGenerationParams generationParams)
|
||||
private static List<Identifier> SelectModules(IEnumerable<SubmarineInfo> modules, Location location, OutpostGenerationParams generationParams)
|
||||
{
|
||||
int totalModuleCount = generationParams.TotalModuleCount;
|
||||
var pendingModuleFlags = new List<Identifier>();
|
||||
@@ -447,23 +482,29 @@ namespace Barotrauma
|
||||
while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound)
|
||||
{
|
||||
availableModulesFound = false;
|
||||
foreach (var moduleFlag in generationParams.ModuleCounts)
|
||||
foreach (var moduleCount in generationParams.ModuleCounts)
|
||||
{
|
||||
if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier))
|
||||
if (!moduleCount.RequiredFaction.IsEmpty &&
|
||||
location.Faction?.Prefab.Identifier != moduleCount.RequiredFaction &&
|
||||
location.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier)))
|
||||
if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found).");
|
||||
continue;
|
||||
}
|
||||
if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier)))
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleCount.Identifier}\" found).");
|
||||
continue;
|
||||
}
|
||||
availableModulesFound = true;
|
||||
pendingModuleFlags.Add(moduleFlag.Identifier);
|
||||
pendingModuleFlags.Add(moduleCount.Identifier);
|
||||
}
|
||||
}
|
||||
pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient));
|
||||
while (pendingModuleFlags.Count < totalModuleCount)
|
||||
pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f).Order).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient));
|
||||
while (pendingModuleFlags.Count < totalModuleCount && generationParams.AppendToReachTotalModuleCount)
|
||||
{
|
||||
//don't place "none" modules at the end because
|
||||
// a. "filler rooms" at the end of a hallway are pointless
|
||||
@@ -514,12 +555,8 @@ namespace Barotrauma
|
||||
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
|
||||
{
|
||||
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
|
||||
if (!allowExtendBelowInitialModule)
|
||||
{
|
||||
//don't continue downwards if it'd extend below the airlock
|
||||
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
|
||||
}
|
||||
|
||||
if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; }
|
||||
|
||||
PlacedModule newModule = null;
|
||||
//try appending to the current module if possible
|
||||
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
|
||||
@@ -540,6 +577,7 @@ namespace Barotrauma
|
||||
foreach (OutpostModuleInfo.GapPosition otherGapPosition in
|
||||
GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
|
||||
{
|
||||
if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; }
|
||||
newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
|
||||
if (newModule != null)
|
||||
{
|
||||
@@ -588,6 +626,16 @@ namespace Barotrauma
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
|
||||
}
|
||||
|
||||
static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule)
|
||||
{
|
||||
if (!allowExtendBelowInitialModule)
|
||||
{
|
||||
//don't continue downwards if it'd extend below the airlock
|
||||
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1057,8 +1105,8 @@ namespace Barotrauma
|
||||
DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.PreviousModule.Info.Name}\")");
|
||||
continue;
|
||||
}
|
||||
wire.Connect(thisJunctionBox.Connections[i], addNode: false);
|
||||
wire.Connect(previousJunctionBox.Connections[i], addNode: false);
|
||||
wire.TryConnect(thisJunctionBox.Connections[i], addNode: false);
|
||||
wire.TryConnect(previousJunctionBox.Connections[i], addNode: false);
|
||||
wire.SetNodes(new List<Vector2>());
|
||||
}
|
||||
}
|
||||
@@ -1390,6 +1438,31 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnableFactionSpecificEntities(Submarine sub, Location location)
|
||||
{
|
||||
foreach (MapEntity me in MapEntity.mapEntityList)
|
||||
{
|
||||
if (string.IsNullOrEmpty(me.Layer) || me.Submarine != sub) { continue; }
|
||||
|
||||
var layerAsIdentifier = me.Layer.ToIdentifier();
|
||||
if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier))
|
||||
{
|
||||
me.HiddenInGame =
|
||||
location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier];
|
||||
#if CLIENT
|
||||
//normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
|
||||
if (me.HiddenInGame && me is Item item)
|
||||
{
|
||||
foreach (var lightComponent in item.GetComponents<LightComponent>())
|
||||
{
|
||||
lightComponent.Light.Enabled = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void LockUnusedDoors(IEnumerable<PlacedModule> placedModules, Dictionary<PlacedModule, List<MapEntity>> entities, bool removeUnusedGaps)
|
||||
{
|
||||
foreach (PlacedModule module in placedModules)
|
||||
@@ -1592,7 +1665,12 @@ namespace Barotrauma
|
||||
List<HumanPrefab> killedCharacters = new List<HumanPrefab>();
|
||||
List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters
|
||||
= new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>();
|
||||
var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient);
|
||||
|
||||
List<FactionPrefab> factions = new List<FactionPrefab>();
|
||||
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);
|
||||
foreach (HumanPrefab humanPrefab in humanPrefabs)
|
||||
{
|
||||
if (humanPrefab is null) { continue; }
|
||||
@@ -1611,7 +1689,7 @@ namespace Barotrauma
|
||||
for (int tries = 0; tries < 100; tries++)
|
||||
{
|
||||
var characterInfo = killedCharacter.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
|
||||
if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier()))
|
||||
if (location != null && !location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier()))
|
||||
{
|
||||
selectedCharacters.Add((killedCharacter, characterInfo));
|
||||
break;
|
||||
@@ -1633,22 +1711,21 @@ namespace Barotrauma
|
||||
npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true);
|
||||
npc.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
npc.HumanPrefab = humanPrefab;
|
||||
if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier))
|
||||
outpost.Info.AddOutpostNPCIdentifierOrTag(npc, humanPrefab.Identifier);
|
||||
foreach (Identifier tag in humanPrefab.GetTags())
|
||||
{
|
||||
outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List<Character>());
|
||||
outpost.Info.AddOutpostNPCIdentifierOrTag(npc, tag);
|
||||
}
|
||||
outpost.Info.OutpostNPCs[humanPrefab.Identifier].Add(npc);
|
||||
if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.KillableNPCs)
|
||||
{
|
||||
npc.CharacterHealth.Unkillable = true;
|
||||
}
|
||||
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient);
|
||||
humanPrefab.GiveItems(npc, outpost, gotoTarget as WayPoint, Rand.RandSync.ServerAndClient);
|
||||
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
|
||||
{
|
||||
item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing;
|
||||
item.SpawnedInCurrentOutpost = true;
|
||||
}
|
||||
npc.GiveIdCardTags(gotoTarget as WayPoint);
|
||||
humanPrefab.InitializeCharacter(npc, gotoTarget);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,13 +93,11 @@ namespace Barotrauma
|
||||
{
|
||||
moduleFlags.Add("hallwayhorizontal".ToIdentifier());
|
||||
if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); }
|
||||
return;
|
||||
}
|
||||
if (newFlags.Contains("hallwayvertical".ToIdentifier()))
|
||||
{
|
||||
moduleFlags.Add("hallwayvertical".ToIdentifier());
|
||||
if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); }
|
||||
return;
|
||||
}
|
||||
if (!newFlags.Any())
|
||||
{
|
||||
|
||||
@@ -34,6 +34,13 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public const int DefaultAmount = 5;
|
||||
|
||||
private readonly Dictionary<Identifier, float> minReputation = new Dictionary<Identifier, float>();
|
||||
|
||||
/// <summary>
|
||||
/// Minimum reputation needed to buy the item (Key = faction ID, Value = min rep)
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<Identifier, float> MinReputation => minReputation;
|
||||
|
||||
/// <summary>
|
||||
/// Support for the old style of determining item prices
|
||||
/// when there were individual Price elements for each location type
|
||||
@@ -70,6 +77,19 @@ namespace Barotrauma
|
||||
RequiresUnlock = requiresUnlock;
|
||||
}
|
||||
|
||||
private void LoadReputationRestrictions(XElement priceInfoElement)
|
||||
{
|
||||
foreach (XElement childElement in priceInfoElement.GetChildElements("reputation"))
|
||||
{
|
||||
Identifier factionId = childElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
float rep = childElement.GetAttributeFloat("min", 0.0f);
|
||||
if (!factionId.IsEmpty && rep > 0)
|
||||
{
|
||||
minReputation.Add(factionId, rep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<PriceInfo> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
|
||||
{
|
||||
var priceInfos = new List<PriceInfo>();
|
||||
@@ -106,6 +126,7 @@ namespace Barotrauma
|
||||
displayNonEmpty: displayNonEmpty,
|
||||
requiresUnlock: requiresUnlock,
|
||||
storeIdentifier: storeIdentifier);
|
||||
priceInfo.LoadReputationRestrictions(childElement);
|
||||
priceInfos.Add(priceInfo);
|
||||
}
|
||||
bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false));
|
||||
@@ -117,7 +138,8 @@ namespace Barotrauma
|
||||
minLevelDifficulty: minLevelDifficulty,
|
||||
buyingPriceMultiplier: buyingPriceMultiplier,
|
||||
displayNonEmpty: displayNonEmpty,
|
||||
requiresUnlock: requiresUnlock);
|
||||
requiresUnlock: requiresUnlock);
|
||||
defaultPrice.LoadReputationRestrictions(element);
|
||||
return priceInfos;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,8 @@ namespace Barotrauma
|
||||
get { return base.Prefab.Sprite; }
|
||||
}
|
||||
|
||||
public bool IsExteriorWall { get; private set; } = true;
|
||||
|
||||
public bool IsPlatform
|
||||
{
|
||||
get { return Prefab.Platform; }
|
||||
@@ -402,6 +404,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
CheckIsExteriorWall();
|
||||
|
||||
#if CLIENT
|
||||
convexHulls?.ForEach(x => x.Move(amount));
|
||||
|
||||
@@ -697,15 +701,41 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2[] CalculateExtremes(Rectangle sectionRect)
|
||||
public void CheckIsExteriorWall()
|
||||
{
|
||||
Vector2[] corners = new Vector2[4];
|
||||
corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height);
|
||||
corners[1] = new Vector2(sectionRect.X, sectionRect.Y);
|
||||
corners[2] = new Vector2(sectionRect.Right, sectionRect.Y);
|
||||
corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height);
|
||||
if (!HasBody)
|
||||
{
|
||||
IsExteriorWall = false;
|
||||
return;
|
||||
}
|
||||
|
||||
return corners;
|
||||
Vector2 point1 = WorldPosition + BodyOffset * Scale;
|
||||
//point1 = MathUtils.RotatePointAroundTarget(WorldPosition, point1, BodyRotation);
|
||||
Vector2 point2 = point1;
|
||||
|
||||
Vector2 normal = new Vector2(
|
||||
(float)-Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
|
||||
(float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation));
|
||||
|
||||
float thickness = IsHorizontal ?
|
||||
(BodyHeight > 0 ? BodyHeight : rect.Height) :
|
||||
(BodyWidth > 0 ? BodyWidth : rect.Width);
|
||||
|
||||
point1 += normal * (thickness / 2 + 16);
|
||||
point2 -= normal * (thickness / 2 + 16);
|
||||
|
||||
IsExteriorWall =
|
||||
Hull.FindHullUnoptimized(point1, null, useWorldCoordinates: true) == null ||
|
||||
Hull.FindHullUnoptimized(point2, null, useWorldCoordinates: true) == null;
|
||||
#if CLIENT
|
||||
if (convexHulls != null)
|
||||
{
|
||||
foreach (ConvexHull ch in convexHulls)
|
||||
{
|
||||
ch.IsExteriorWall = IsExteriorWall;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -715,7 +745,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (MapEntity mapEntity in mapEntityList)
|
||||
{
|
||||
if (!(mapEntity is Structure structure)) { continue; }
|
||||
if (mapEntity is not Structure structure) { continue; }
|
||||
if (!structure.Prefab.AllowAttachItems) { continue; }
|
||||
if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; }
|
||||
Rectangle worldRect = mapEntity.WorldRect;
|
||||
@@ -939,7 +969,7 @@ namespace Barotrauma
|
||||
Rand.Range(worldRect.X, worldRect.Right + 1),
|
||||
Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1));
|
||||
|
||||
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f);
|
||||
var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f);
|
||||
if (particle == null) break;
|
||||
}
|
||||
}
|
||||
@@ -1085,9 +1115,9 @@ namespace Barotrauma
|
||||
return new AttackResult(damageAmount, null);
|
||||
}
|
||||
|
||||
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true)
|
||||
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true)
|
||||
{
|
||||
if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
|
||||
if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; }
|
||||
if (!Prefab.Body) { return; }
|
||||
if (!MathUtils.IsValid(damage)) { return; }
|
||||
|
||||
@@ -1635,6 +1665,7 @@ namespace Barotrauma
|
||||
{
|
||||
SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false);
|
||||
}
|
||||
CheckIsExteriorWall();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.IO;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
#endif
|
||||
@@ -44,30 +45,26 @@ namespace Barotrauma
|
||||
|
||||
public override ImmutableHashSet<string> Aliases { get; }
|
||||
|
||||
//does the structure have a physics body
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Does the structure have a physics body?")]
|
||||
public bool Body { get; private set; }
|
||||
|
||||
//rotation of the physics body in degrees
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the physics body in degrees.")]
|
||||
public float BodyRotation { get; private set; }
|
||||
|
||||
//in display units
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "Width of the physics body in pixels.")]
|
||||
public float BodyWidth { get; private set; }
|
||||
|
||||
//in display units
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "Height of the physics body in pixels.")]
|
||||
public float BodyHeight { get; private set; }
|
||||
|
||||
//in display units
|
||||
[Serialize("0.0,0.0", IsPropertySaveable.No)]
|
||||
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "Offset of the physics body from the center of the structure in pixels.")]
|
||||
public Vector2 BodyOffset { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Is the structure a platform (i.e. a \"floor\" the players can pass through)? Only relevant if the structure has a physics body.")]
|
||||
public bool Platform { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can items like signal components be attached on this structure? Should be enabled on structures like decorative background walls.")]
|
||||
public bool AllowAttachItems { get; private set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
@@ -81,27 +78,30 @@ namespace Barotrauma
|
||||
private set { health = Math.Max(value, MinHealth); }
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the structure be indestructible when used in an outpost?")]
|
||||
public bool IndestructibleInOutposts { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the structure cast shadows and obstruct visibility when LOS is enabled?")]
|
||||
public bool CastShadow { get; private set; }
|
||||
|
||||
[Serialize(Direction.None, IsPropertySaveable.No)]
|
||||
[Serialize(Direction.None, IsPropertySaveable.No, description: "Makes the structure function as a staircase.")]
|
||||
public Direction StairDirection { get; private set; }
|
||||
|
||||
[Serialize(45.0f, IsPropertySaveable.No)]
|
||||
[Serialize(45.0f, IsPropertySaveable.No, description: "Angle of the stairs in degrees. Only relevant if StairDirection is something else than None.")]
|
||||
public float StairAngle { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If enabled, monsters will not be able to target this structure.")]
|
||||
public bool NoAITarget { get; private set; }
|
||||
|
||||
[Serialize("0,0", IsPropertySaveable.Yes)]
|
||||
[Serialize("0,0", IsPropertySaveable.Yes, description: "Size of the structure in pixels. If not set, the size is determined, based on the attributes width and height, and if those aren't defined either, based on the size of the structure's sprite.")]
|
||||
public Vector2 Size { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the sound that plays when something damages the wall.")]
|
||||
public string DamageSound { get; private set; }
|
||||
|
||||
[Serialize("shrapnel", IsPropertySaveable.Yes, description: "Identifier of the particles emitted when something damages the wall.")]
|
||||
public string DamageParticle { get; private set; }
|
||||
|
||||
protected Vector2 textureScale = Vector2.One;
|
||||
[Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)]
|
||||
public Vector2 TextureScale
|
||||
@@ -175,7 +175,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString())
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "sprite":
|
||||
Sprite = new Sprite(subElement, lazyLoad: true);
|
||||
|
||||
@@ -213,9 +213,21 @@ namespace Barotrauma
|
||||
get
|
||||
{
|
||||
if (Level.Loaded == null) { return false; }
|
||||
if (Level.Loaded.EndOutpost != null && DockedTo.Contains(Level.Loaded.EndOutpost))
|
||||
if (Level.Loaded.EndOutpost != null)
|
||||
{
|
||||
return true;
|
||||
if (DockedTo.Contains(Level.Loaded.EndOutpost))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (Level.Loaded.EndOutpost.exitPoints.Any())
|
||||
{
|
||||
return IsAtOutpostExit(Level.Loaded.EndOutpost);
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded.Type == LevelData.LevelType.Outpost && Level.Loaded.StartOutpost != null)
|
||||
{
|
||||
//in outpost levels, the outpost is always the start outpost: check it if has an exit
|
||||
return IsAtOutpostExit(Level.Loaded.StartOutpost);
|
||||
}
|
||||
return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance);
|
||||
}
|
||||
@@ -226,14 +238,44 @@ namespace Barotrauma
|
||||
get
|
||||
{
|
||||
if (Level.Loaded == null) { return false; }
|
||||
if (Level.Loaded.StartOutpost != null && DockedTo.Contains(Level.Loaded.StartOutpost))
|
||||
if (Level.Loaded.StartOutpost != null)
|
||||
{
|
||||
return true;
|
||||
if (DockedTo.Contains(Level.Loaded.StartOutpost))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (Level.Loaded.StartOutpost.exitPoints.Any())
|
||||
{
|
||||
return IsAtOutpostExit(Level.Loaded.StartOutpost);
|
||||
}
|
||||
}
|
||||
return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartExitPosition) < Level.ExitDistance * Level.ExitDistance);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AtEitherExit => AtStartExit || AtEndExit;
|
||||
|
||||
private bool IsAtOutpostExit(Submarine outpost)
|
||||
{
|
||||
if (outpost.exitPoints.Any())
|
||||
{
|
||||
Rectangle worldBorders = Borders;
|
||||
worldBorders.Location += WorldPosition.ToPoint();
|
||||
foreach (var exitPoint in outpost.exitPoints)
|
||||
{
|
||||
if (exitPoint.ExitPointSize != Point.Zero)
|
||||
{
|
||||
if (RectsOverlap(worldBorders, exitPoint.ExitPointWorldRect)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (RectContains(worldBorders, exitPoint.WorldPosition)) { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public new Vector2 DrawPosition
|
||||
{
|
||||
@@ -284,6 +326,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<WayPoint> exitPoints = new List<WayPoint>();
|
||||
public IReadOnlyList<WayPoint> ExitPoints { get { return exitPoints; } }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")";
|
||||
@@ -350,12 +395,23 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public WreckAI WreckAI { get; private set; }
|
||||
public SubmarineTurretAI TurretAI { get; private set; }
|
||||
|
||||
public bool CreateWreckAI()
|
||||
{
|
||||
WreckAI = WreckAI.Create(this);
|
||||
return WreckAI != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an AI that operates all the turrets on a sub, same as Thalamus but only operates the turrets.
|
||||
/// </summary>
|
||||
public bool CreateTurretAI()
|
||||
{
|
||||
TurretAI = new SubmarineTurretAI(this);
|
||||
return TurretAI != null;
|
||||
}
|
||||
|
||||
public void DisableWreckAI()
|
||||
{
|
||||
if (WreckAI == null)
|
||||
@@ -991,6 +1047,7 @@ namespace Barotrauma
|
||||
{
|
||||
WreckAI?.Update(deltaTime);
|
||||
}
|
||||
TurretAI?.Update(deltaTime);
|
||||
|
||||
if (subBody?.Body == null) { return; }
|
||||
|
||||
@@ -1038,7 +1095,7 @@ namespace Barotrauma
|
||||
|
||||
public void ApplyForce(Vector2 force)
|
||||
{
|
||||
if (subBody != null) subBody.ApplyForce(force);
|
||||
if (subBody != null) { subBody.ApplyForce(force); }
|
||||
}
|
||||
|
||||
public void EnableMaintainPosition()
|
||||
@@ -1271,22 +1328,38 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the sub whose borders contain the position
|
||||
/// Finds the sub whose borders contain the position. Note that this method uses the "actual" position of the sub outside the level:
|
||||
/// only use this if the position is in a submarine's local coordinate space!
|
||||
/// </summary>
|
||||
public static Submarine FindContaining(Vector2 position)
|
||||
public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f)
|
||||
{
|
||||
foreach (Submarine sub in Loaded)
|
||||
{
|
||||
Rectangle subBorders = sub.Borders;
|
||||
subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height);
|
||||
|
||||
subBorders.Inflate(500.0f, 500.0f);
|
||||
|
||||
if (subBorders.Contains(position)) return sub;
|
||||
subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height);
|
||||
subBorders.Inflate(inflate, inflate);
|
||||
if (subBorders.Contains(position)) { return sub; }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the sub whose world borders contain the position.
|
||||
/// </summary>
|
||||
public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f)
|
||||
{
|
||||
foreach (Submarine sub in Loaded)
|
||||
{
|
||||
Rectangle worldBorders = sub.Borders;
|
||||
worldBorders.Location += sub.WorldPosition.ToPoint();
|
||||
worldBorders.Inflate(inflate, inflate);
|
||||
if (RectContains(worldBorders, worldPosition)) { return sub; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static Rectangle GetBorders(XElement submarineElement)
|
||||
{
|
||||
Vector4 bounds = Vector4.Zero;
|
||||
@@ -1325,14 +1398,19 @@ namespace Barotrauma
|
||||
|
||||
//place the sub above the top of the level
|
||||
HiddenSubPosition = HiddenSubStartPosition;
|
||||
if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null)
|
||||
if (GameMain.GameSession?.LevelData != null)
|
||||
{
|
||||
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
|
||||
}
|
||||
|
||||
foreach (Submarine sub in loaded)
|
||||
for (int i = 0; i < loaded.Count; i++)
|
||||
{
|
||||
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
|
||||
Submarine sub = loaded[i];
|
||||
HiddenSubPosition =
|
||||
new Vector2(
|
||||
//1st sub on the left side, 2nd on the right, etc
|
||||
HiddenSubPosition.X * (i % 2 == 0 ? 1 : -1),
|
||||
HiddenSubPosition.Y + sub.Borders.Height + 5000.0f);
|
||||
}
|
||||
|
||||
IdOffset = IdRemap.DetermineNewOffset();
|
||||
@@ -1460,10 +1538,15 @@ namespace Barotrauma
|
||||
MapEntity.MapLoaded(newEntities, true);
|
||||
foreach (MapEntity me in MapEntity.mapEntityList)
|
||||
{
|
||||
if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this)
|
||||
if (me.Submarine != this) { continue; }
|
||||
if (me is LinkedSubmarine linkedSub)
|
||||
{
|
||||
linkedSub.LinkDummyToMainSubmarine();
|
||||
}
|
||||
else if (me is WayPoint wayPoint && wayPoint.SpawnType.HasFlag(SpawnType.ExitPoint))
|
||||
{
|
||||
exitPoints.Add(wayPoint);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Hull hull in matchingHulls)
|
||||
@@ -1478,6 +1561,7 @@ namespace Barotrauma
|
||||
|
||||
#if CLIENT
|
||||
GameMain.LightManager.OnMapLoaded();
|
||||
Lights.ConvexHull.RecalculateAll(this);
|
||||
#endif
|
||||
//if the sub was made using an older version,
|
||||
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
|
||||
@@ -1683,57 +1767,66 @@ namespace Barotrauma
|
||||
|
||||
public static void Unload()
|
||||
{
|
||||
if (Unloading)
|
||||
{
|
||||
DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading.");
|
||||
return;
|
||||
}
|
||||
|
||||
Unloading = true;
|
||||
try
|
||||
{
|
||||
|
||||
#if CLIENT
|
||||
RoundSound.RemoveAllRoundSounds();
|
||||
GameMain.LightManager?.ClearLights();
|
||||
RoundSound.RemoveAllRoundSounds();
|
||||
GameMain.LightManager?.ClearLights();
|
||||
#endif
|
||||
|
||||
var _loaded = new List<Submarine>(loaded);
|
||||
foreach (Submarine sub in _loaded)
|
||||
{
|
||||
sub.Remove();
|
||||
}
|
||||
|
||||
loaded.Clear();
|
||||
|
||||
visibleEntities = null;
|
||||
|
||||
if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; }
|
||||
|
||||
RemoveAll();
|
||||
|
||||
if (Item.ItemList.Count > 0)
|
||||
{
|
||||
List<Item> items = new List<Item>(Item.ItemList);
|
||||
foreach (Item item in items)
|
||||
var _loaded = new List<Submarine>(loaded);
|
||||
foreach (Submarine sub in _loaded)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed");
|
||||
try
|
||||
{
|
||||
item.Remove();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e);
|
||||
}
|
||||
sub.Remove();
|
||||
}
|
||||
Item.ItemList.Clear();
|
||||
|
||||
loaded.Clear();
|
||||
|
||||
visibleEntities = null;
|
||||
|
||||
if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; }
|
||||
|
||||
RemoveAll();
|
||||
|
||||
if (Item.ItemList.Count > 0)
|
||||
{
|
||||
List<Item> items = new List<Item>(Item.ItemList);
|
||||
foreach (Item item in items)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed");
|
||||
try
|
||||
{
|
||||
item.Remove();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e);
|
||||
}
|
||||
}
|
||||
Item.ItemList.Clear();
|
||||
}
|
||||
|
||||
Ragdoll.RemoveAll();
|
||||
PhysicsBody.RemoveAll();
|
||||
GameMain.World = null;
|
||||
|
||||
Powered.Grids.Clear();
|
||||
|
||||
GC.Collect();
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
Unloading = false;
|
||||
}
|
||||
|
||||
Ragdoll.RemoveAll();
|
||||
|
||||
PhysicsBody.RemoveAll();
|
||||
|
||||
GameMain.World?.Clear();
|
||||
GameMain.World = null;
|
||||
|
||||
Powered.Grids.Clear();
|
||||
|
||||
GC.Collect();
|
||||
|
||||
Unloading = false;
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
@@ -1800,18 +1893,18 @@ namespace Barotrauma
|
||||
{
|
||||
if (node == null || node.Waypoint == null) { continue; }
|
||||
var wp = node.Waypoint;
|
||||
if (wp.isObstructed) { continue; }
|
||||
if (wp.IsObstructed) { continue; }
|
||||
foreach (var connection in node.connections)
|
||||
{
|
||||
var connectedWp = connection.Waypoint;
|
||||
if (connectedWp.isObstructed) { continue; }
|
||||
if (connectedWp.IsObstructed) { continue; }
|
||||
Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition);
|
||||
var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false);
|
||||
if (body != null)
|
||||
{
|
||||
connectedWp.isObstructed = true;
|
||||
wp.isObstructed = true;
|
||||
connectedWp.IsObstructed = true;
|
||||
wp.IsObstructed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1830,11 +1923,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (node == null || node.Waypoint == null) { continue; }
|
||||
var wp = node.Waypoint;
|
||||
if (wp.isObstructed) { continue; }
|
||||
if (wp.IsObstructed) { continue; }
|
||||
foreach (var connection in node.connections)
|
||||
{
|
||||
var connectedWp = connection.Waypoint;
|
||||
if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; }
|
||||
if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; }
|
||||
Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition;
|
||||
Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition;
|
||||
var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true);
|
||||
@@ -1842,8 +1935,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
|
||||
{
|
||||
connectedWp.isObstructed = true;
|
||||
wp.isObstructed = true;
|
||||
connectedWp.IsObstructed = true;
|
||||
wp.IsObstructed = true;
|
||||
if (!obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
|
||||
{
|
||||
nodes = new HashSet<PathNode>();
|
||||
@@ -1865,7 +1958,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
|
||||
{
|
||||
nodes.ForEach(n => n.Waypoint.isObstructed = false);
|
||||
nodes.ForEach(n => n.Waypoint.IsObstructed = false);
|
||||
nodes.Clear();
|
||||
obstructedNodes.Remove(otherSub);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ namespace Barotrauma
|
||||
{
|
||||
public const float NeutralBallastPercentage = 0.07f;
|
||||
|
||||
public const Category CollidesWith =
|
||||
Physics.CollisionItem |
|
||||
Physics.CollisionLevel |
|
||||
Physics.CollisionCharacter |
|
||||
Physics.CollisionProjectile |
|
||||
Physics.CollisionWall;
|
||||
|
||||
const float HorizontalDrag = 0.01f;
|
||||
const float VerticalDrag = 0.05f;
|
||||
const float MaxDrag = 0.1f;
|
||||
@@ -32,6 +39,8 @@ namespace Barotrauma
|
||||
private const float MaxCollisionImpact = 5.0f;
|
||||
private const float Friction = 0.2f, Restitution = 0.0f;
|
||||
|
||||
private readonly List<Contact> levelContacts = new List<Contact>();
|
||||
|
||||
public List<Vector2> HullVertices
|
||||
{
|
||||
get;
|
||||
@@ -39,6 +48,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private float depthDamageTimer = 10.0f;
|
||||
private float damageSoundTimer = 10.0f;
|
||||
|
||||
private readonly Submarine submarine;
|
||||
|
||||
@@ -48,6 +58,9 @@ namespace Barotrauma
|
||||
|
||||
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
||||
|
||||
private float forceUpwardsTimer;
|
||||
private const float ForceUpwardsDelay = 30.0f;
|
||||
|
||||
struct Impact
|
||||
{
|
||||
public Fixture Target;
|
||||
@@ -146,9 +159,13 @@ namespace Barotrauma
|
||||
farseerBody.CollidesWith = collidesWith;
|
||||
farseerBody.Enabled = false;
|
||||
farseerBody.UserData = this;
|
||||
if (sub.Info.IsOutpost)
|
||||
{
|
||||
farseerBody.BodyType = BodyType.Static;
|
||||
}
|
||||
foreach (var mapEntity in MapEntity.mapEntityList)
|
||||
{
|
||||
if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; }
|
||||
if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; }
|
||||
|
||||
bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None;
|
||||
Rectangle rect = wall.Rect;
|
||||
@@ -185,13 +202,20 @@ namespace Barotrauma
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != submarine) { continue; }
|
||||
if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; }
|
||||
|
||||
Vector2 simPos = ConvertUnits.ToSimUnits(item.Position);
|
||||
if (item.GetComponent<Door>() is Door door)
|
||||
{
|
||||
door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith);
|
||||
door.OutsideSubmarineFixture.UserData = item;
|
||||
}
|
||||
|
||||
if (item.StaticBodyConfig == null) { continue; }
|
||||
|
||||
float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale;
|
||||
float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale;
|
||||
float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale;
|
||||
|
||||
Vector2 simPos = ConvertUnits.ToSimUnits(item.Position);
|
||||
float simRadius = ConvertUnits.ToSimUnits(radius);
|
||||
float simWidth = ConvertUnits.ToSimUnits(width);
|
||||
float simHeight = ConvertUnits.ToSimUnits(height);
|
||||
@@ -374,6 +398,33 @@ namespace Barotrauma
|
||||
|
||||
//-------------------------
|
||||
|
||||
//if heading up and there's another sub on top of us, gradually force it upwards
|
||||
//(i.e. apply "artificial buoyancy" to it) to prevent us from getting pinned under it
|
||||
//only applies to enemy subs with no enemies inside them (like destroyed pirate subs)
|
||||
if (totalForce.Y > 0)
|
||||
{
|
||||
ContactEdge contactEdge = Body?.FarseerBody?.ContactList;
|
||||
while (contactEdge?.Next != null)
|
||||
{
|
||||
if (contactEdge.Contact.Enabled &&
|
||||
contactEdge.Other.UserData is Submarine otherSubmarine &&
|
||||
otherSubmarine.TeamID != Submarine.TeamID &&
|
||||
contactEdge.Contact.IsTouching)
|
||||
{
|
||||
contactEdge.Contact.GetWorldManifold(out Vector2 _, out FixedArray2<Vector2> points);
|
||||
if (points[0].Y > Body.SimPosition.Y &&
|
||||
!Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID))
|
||||
{
|
||||
otherSubmarine.SubBody.forceUpwardsTimer += deltaTime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------
|
||||
|
||||
if (Body.LinearVelocity.LengthSquared() > 0.0001f)
|
||||
{
|
||||
//TODO: sync current drag with clients?
|
||||
@@ -397,7 +448,34 @@ namespace Barotrauma
|
||||
|
||||
ApplyForce(totalForce);
|
||||
|
||||
if (Velocity.LengthSquared() < 0.01f)
|
||||
{
|
||||
levelContacts.Clear();
|
||||
levelContacts.AddRange(GetLevelContacts(Body));
|
||||
for (int i = 0; i < levelContacts.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < levelContacts.Count; j++)
|
||||
{
|
||||
levelContacts[i].GetWorldManifold(out Vector2 normal1, out _);
|
||||
levelContacts[j].GetWorldManifold(out Vector2 normal2, out _);
|
||||
|
||||
//normals pointing in different directions = sub lodged between two walls
|
||||
if (Vector2.Dot(normal1, normal2) < 0)
|
||||
{
|
||||
//apply an extra force to hopefully dislodge the sub
|
||||
ApplyForce(totalForce * 100.0f);
|
||||
i = levelContacts.Count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalForcePerFrame = Vector2.Zero;
|
||||
|
||||
UpdateDepthDamage(deltaTime);
|
||||
|
||||
forceUpwardsTimer = MathHelper.Clamp(forceUpwardsTimer - deltaTime * 0.1f, 0.0f, ForceUpwardsDelay);
|
||||
}
|
||||
|
||||
partial void ClientUpdatePosition(float deltaTime);
|
||||
@@ -464,16 +542,28 @@ namespace Barotrauma
|
||||
float buoyancy = NeutralBallastPercentage - waterPercentage;
|
||||
|
||||
if (buoyancy > 0.0f)
|
||||
{
|
||||
buoyancy *= 2.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
buoyancy = Math.Max(buoyancy, -0.5f);
|
||||
}
|
||||
|
||||
if (forceUpwardsTimer > 0.0f)
|
||||
{
|
||||
buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay);
|
||||
}
|
||||
|
||||
return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f);
|
||||
}
|
||||
|
||||
|
||||
private Vector2 totalForcePerFrame;
|
||||
public void ApplyForce(Vector2 force)
|
||||
{
|
||||
Body.ApplyForce(force);
|
||||
totalForcePerFrame += force;
|
||||
}
|
||||
|
||||
public void SetPosition(Vector2 position)
|
||||
@@ -489,36 +579,58 @@ namespace Barotrauma
|
||||
if (Level.Loaded == null) { return; }
|
||||
|
||||
//camera shake and sounds start playing 500 meters before crush depth
|
||||
float depthEffectThreshold = 500.0f;
|
||||
if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold)
|
||||
const float CosmeticEffectThreshold = -500.0f;
|
||||
//breaches won't get any more severe 500 meters below crush depth
|
||||
const float MaxEffectThreshold = 500.0f;
|
||||
const float MinWallDamageProbability = 0.1f;
|
||||
const float MaxWallDamageProbability = 1.0f;
|
||||
const float MinWallDamage = 50f;
|
||||
const float MaxWallDamage = 500.0f;
|
||||
const float MinCameraShake = 5f;
|
||||
const float MaxCameraShake = 50.0f;
|
||||
|
||||
if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
depthDamageTimer -= deltaTime;
|
||||
if (depthDamageTimer > 0.0f) { return; }
|
||||
|
||||
#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);
|
||||
#endif
|
||||
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
damageSoundTimer -= deltaTime;
|
||||
if (damageSoundTimer <= 0.0f)
|
||||
{
|
||||
if (wall.Submarine != submarine) { continue; }
|
||||
|
||||
float wallCrushDepth = wall.CrushDepth;
|
||||
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
|
||||
if (pastCrushDepth > 0)
|
||||
{
|
||||
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f);
|
||||
}
|
||||
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f));
|
||||
}
|
||||
#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);
|
||||
#endif
|
||||
damageSoundTimer = Rand.Range(5.0f, 10.0f);
|
||||
}
|
||||
|
||||
depthDamageTimer = 10.0f;
|
||||
depthDamageTimer -= deltaTime;
|
||||
if (depthDamageTimer <= 0.0f)
|
||||
{
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
if (wall.Submarine != submarine) { continue; }
|
||||
|
||||
float wallCrushDepth = wall.CrushDepth;
|
||||
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
|
||||
float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f);
|
||||
|
||||
if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; }
|
||||
|
||||
float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio);
|
||||
if (pastCrushDepth > 0)
|
||||
{
|
||||
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f);
|
||||
#if CLIENT
|
||||
SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f);
|
||||
#endif
|
||||
}
|
||||
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio));
|
||||
}
|
||||
}
|
||||
depthDamageTimer = Rand.Range(5.0f, 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void FlipX()
|
||||
@@ -623,7 +735,7 @@ namespace Barotrauma
|
||||
attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier;
|
||||
}
|
||||
|
||||
if (impactMass * attackMultiplier > MinImpactLimbMass)
|
||||
if (impactMass * attackMultiplier > MinImpactLimbMass && Body.BodyType != BodyType.Static)
|
||||
{
|
||||
Vector2 normal =
|
||||
Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
|
||||
@@ -641,20 +753,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//find all contacts between the limb and level walls
|
||||
List<Contact> levelContacts = new List<Contact>();
|
||||
ContactEdge contactEdge = limb.body.FarseerBody.ContactList;
|
||||
while (contactEdge?.Contact != null)
|
||||
{
|
||||
if (contactEdge.Contact.Enabled &&
|
||||
contactEdge.Contact.IsTouching &&
|
||||
contactEdge.Other?.UserData is VoronoiCell)
|
||||
{
|
||||
levelContacts.Add(contactEdge.Contact);
|
||||
}
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
IEnumerable<Contact> levelContacts = GetLevelContacts(limb.body);
|
||||
int levelContactCount = levelContacts.Count();
|
||||
|
||||
if (levelContacts.Count == 0) { return; }
|
||||
if (levelContactCount == 0) { return; }
|
||||
|
||||
//if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it
|
||||
//not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete),
|
||||
@@ -677,9 +779,9 @@ namespace Barotrauma
|
||||
avgContactNormal += contactNormal;
|
||||
|
||||
//apply impacts at the positions where this sub is touching the limb
|
||||
ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContacts.Count, contactNormal, collision.ImpactPos, applyDamage: false);
|
||||
ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContactCount, contactNormal, collision.ImpactPos, applyDamage: false);
|
||||
}
|
||||
avgContactNormal /= levelContacts.Count;
|
||||
avgContactNormal /= levelContactCount;
|
||||
|
||||
float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal);
|
||||
if (contactDot > 0.001f)
|
||||
@@ -718,6 +820,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Contact> GetLevelContacts(PhysicsBody body)
|
||||
{
|
||||
ContactEdge contactEdge = body.FarseerBody.ContactList;
|
||||
while (contactEdge?.Contact != null)
|
||||
{
|
||||
if (contactEdge.Contact.Enabled &&
|
||||
contactEdge.Contact.IsTouching &&
|
||||
contactEdge.Other?.UserData is VoronoiCell)
|
||||
{
|
||||
yield return contactEdge.Contact;
|
||||
}
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
|
||||
{
|
||||
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10)
|
||||
@@ -786,21 +903,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//find all contacts between this sub and level walls
|
||||
List<Contact> levelContacts = new List<Contact>();
|
||||
ContactEdge contactEdge = Body?.FarseerBody?.ContactList;
|
||||
while (contactEdge?.Next != null)
|
||||
{
|
||||
if (contactEdge.Contact.Enabled &&
|
||||
contactEdge.Other.UserData is VoronoiCell &&
|
||||
contactEdge.Contact.IsTouching)
|
||||
{
|
||||
levelContacts.Add(contactEdge.Contact);
|
||||
}
|
||||
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
|
||||
if (levelContacts.Count == 0) { return; }
|
||||
IEnumerable<Contact> levelContacts = GetLevelContacts(Body);
|
||||
int levelContactCount = levelContacts.Count();
|
||||
if (levelContactCount == 0) { return; }
|
||||
|
||||
//if this sub is in contact with the level, apply artifical impacts
|
||||
//to both subs to prevent the other sub from bouncing on top of this one
|
||||
@@ -811,8 +916,7 @@ namespace Barotrauma
|
||||
levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
|
||||
|
||||
//if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal
|
||||
VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
|
||||
((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
|
||||
VoronoiCell cell = levelContact.FixtureB.UserData as VoronoiCell ?? levelContact.FixtureA.UserData as VoronoiCell;
|
||||
|
||||
var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center;
|
||||
if (Vector2.Dot(contactNormal, cellDiff) < 0)
|
||||
@@ -823,9 +927,9 @@ namespace Barotrauma
|
||||
avgContactNormal += contactNormal;
|
||||
|
||||
//apply impacts at the positions where this sub is touching the level
|
||||
ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContacts.Count, contactNormal, impact.ImpactPos);
|
||||
ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContactCount, contactNormal, impact.ImpactPos);
|
||||
}
|
||||
avgContactNormal /= levelContacts.Count;
|
||||
avgContactNormal /= levelContactCount;
|
||||
|
||||
//apply an impact to the other sub
|
||||
float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
#if DEBUG
|
||||
using System.IO;
|
||||
@@ -478,7 +479,6 @@ namespace Barotrauma
|
||||
hashTask = new Task(() =>
|
||||
{
|
||||
hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace);
|
||||
Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow);
|
||||
});
|
||||
hashTask.Start();
|
||||
}
|
||||
@@ -559,6 +559,14 @@ namespace Barotrauma
|
||||
}
|
||||
return structureCrushDepthsDefined;
|
||||
}
|
||||
public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag)
|
||||
{
|
||||
if (!OutpostNPCs.ContainsKey(idOrTag))
|
||||
{
|
||||
OutpostNPCs.Add(idOrTag, new List<Character>());
|
||||
}
|
||||
OutpostNPCs[idOrTag].Add(npc);
|
||||
}
|
||||
|
||||
//saving/loading ----------------------------------------------------
|
||||
public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null)
|
||||
@@ -590,7 +598,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
SaveUtil.CompressStringToFile(filePath, doc.ToString());
|
||||
Md5Hash.Cache.Remove(filePath);
|
||||
}
|
||||
|
||||
public static void AddToSavedSubs(SubmarineInfo subInfo)
|
||||
@@ -752,6 +759,36 @@ namespace Barotrauma
|
||||
return doc;
|
||||
}
|
||||
|
||||
public int GetPrice(Location location = null, ImmutableHashSet<Character> characterList = null)
|
||||
{
|
||||
if (location is null)
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation is { } currentLocation)
|
||||
{
|
||||
location = currentLocation;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
return Price;
|
||||
}
|
||||
}
|
||||
|
||||
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
float price = Price;
|
||||
if (characterList.Any())
|
||||
{
|
||||
if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
|
||||
{
|
||||
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated));
|
||||
}
|
||||
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier));
|
||||
}
|
||||
|
||||
return (int)price;
|
||||
}
|
||||
|
||||
public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1;
|
||||
|
||||
public const int HighestTier = 3;
|
||||
|
||||
@@ -11,7 +11,7 @@ using Barotrauma.Extensions;
|
||||
namespace Barotrauma
|
||||
{
|
||||
[Flags]
|
||||
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 };
|
||||
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32 };
|
||||
|
||||
partial class WayPoint : MapEntity
|
||||
{
|
||||
@@ -29,7 +29,32 @@ namespace Barotrauma
|
||||
|
||||
private HashSet<Identifier> tags;
|
||||
|
||||
public bool isObstructed;
|
||||
public bool IsObstructed;
|
||||
|
||||
public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y;
|
||||
|
||||
// Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open.
|
||||
// The only way to add the open gaps should be by calling OnGapStateSchanged.
|
||||
public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater);
|
||||
|
||||
private HashSet<Gap> openGaps;
|
||||
/// <summary>
|
||||
/// Only called by a Gap when the state changes.
|
||||
/// So in practice used like an event callback, although technically just a method
|
||||
/// (It would be cleaner to use an actual event in Gap.cs, but event registering and unregistering might cause an extra hassle)
|
||||
/// </summary>
|
||||
public void OnGapStateChanged(bool open, Gap gap)
|
||||
{
|
||||
openGaps ??= new HashSet<Gap>();
|
||||
if (open)
|
||||
{
|
||||
openGaps.Add(gap);
|
||||
}
|
||||
else
|
||||
{
|
||||
openGaps.Remove(gap);
|
||||
}
|
||||
}
|
||||
|
||||
private ushort gapId;
|
||||
public Gap ConnectedGap
|
||||
@@ -54,6 +79,12 @@ namespace Barotrauma
|
||||
set { spawnType = value; }
|
||||
}
|
||||
|
||||
public Point ExitPointSize { get; private set; }
|
||||
|
||||
public Rectangle ExitPointWorldRect => new Rectangle(
|
||||
(int)WorldPosition.X - ExitPointSize.X / 2, (int)WorldPosition.Y + ExitPointSize.Y / 2,
|
||||
ExitPointSize.X, ExitPointSize.Y);
|
||||
|
||||
public Action<WayPoint> OnLinksChanged { get; set; }
|
||||
|
||||
public override string Name
|
||||
@@ -140,7 +171,9 @@ namespace Barotrauma
|
||||
{ "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) },
|
||||
{ "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) },
|
||||
{ "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) },
|
||||
{ "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) }
|
||||
{ "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) },
|
||||
{ "Submarine", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) },
|
||||
{ "ExitPoint", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) }
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -981,6 +1014,12 @@ namespace Barotrauma
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
if (Submarine == null)
|
||||
{
|
||||
// Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc.
|
||||
// Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost.
|
||||
return;
|
||||
}
|
||||
InitializeLinks();
|
||||
FindHull();
|
||||
FindStairs();
|
||||
@@ -1018,7 +1057,6 @@ namespace Barotrauma
|
||||
int.Parse(element.GetAttribute("y").Value),
|
||||
(int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
|
||||
|
||||
|
||||
Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
|
||||
WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element))
|
||||
{
|
||||
@@ -1036,6 +1074,8 @@ namespace Barotrauma
|
||||
w.IdCardTags = idCardTagString.Split(',');
|
||||
}
|
||||
|
||||
w.ExitPointSize = element.GetAttributePoint("exitpointsize", Point.Zero);
|
||||
|
||||
w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
|
||||
|
||||
Identifier jobIdentifier = element.GetAttributeIdentifier("job", Identifier.Empty);
|
||||
@@ -1076,6 +1116,10 @@ namespace Barotrauma
|
||||
new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)),
|
||||
new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)),
|
||||
new XAttribute("spawn", spawnType));
|
||||
if (SpawnType == SpawnType.ExitPoint)
|
||||
{
|
||||
element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(IdCardDesc)) element.Add(new XAttribute("idcarddesc", IdCardDesc));
|
||||
if (idCardTags.Length > 0)
|
||||
|
||||
Reference in New Issue
Block a user