v0.11.0.9

This commit is contained in:
Joonas Rikkonen
2020-12-09 16:34:16 +02:00
parent bbf06f0984
commit f433a7ba10
325 changed files with 13947 additions and 3652 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
using System;
using System.Collections;
using System.Xml.Linq;
namespace Barotrauma
{
class BallastFloraPrefab : IPrefab, IDisposable
{
public string OriginalName { get; }
public string Identifier { get; }
public string FilePath { get; }
public XElement Element { get; }
public ContentPackage ContentPackage { get; private set; }
public bool Disposed;
public static readonly PrefabCollection<BallastFloraPrefab> Prefabs = new PrefabCollection<BallastFloraPrefab>();
private BallastFloraPrefab(XElement element, string filePath, bool isOverride)
{
Identifier = element.GetAttributeString("identifier", "");
OriginalName = element.GetAttributeString("name", "");
Element = element;
FilePath = filePath;
Prefabs.Add(this, isOverride);
}
public static BallastFloraPrefab Find(string idenfitier)
{
return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null;
}
public static void LoadAll(IEnumerable files)
{
DebugConsole.Log("Loading map creature prefabs: ");
foreach (ContentFile file in files) { LoadFromFile(file); }
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc?.Root;
if (rootElement == null) { return; }
switch (rootElement.Name.ToString().ToLowerInvariant())
{
case "ballastflorabehavior":
{
new BallastFloraPrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage };
break;
}
case "ballastflorabehaviors":
{
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
XElement upgradeElement = element.GetChildElement("mapcreature");
if (upgradeElement != null)
{
new BallastFloraPrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage };
}
else
{
DebugConsole.ThrowError($"Cannot find a map creature element from the children of the override element defined in {file.Path}");
}
}
else
{
if (element.Name.ToString().Equals("mapcreature", StringComparison.OrdinalIgnoreCase))
{
new BallastFloraPrefab(element, file.Path, false) { ContentPackage = file.ContentPackage };
}
}
}
break;
}
case "override":
{
XElement mapCreatures = rootElement.GetChildElement("ballastflorabehaviors");
if (mapCreatures != null)
{
foreach (XElement element in mapCreatures.Elements())
{
new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage };
}
}
foreach (XElement element in rootElement.GetChildElements("ballastflorabehavior"))
{
new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage };
}
break;
}
default:
{
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " +
"Valid elements are: \"MapCreature\", \"MapCreatures\" and \"Override\".");
break;
}
}
}
private void Dispose(bool disposing)
{
if (!Disposed)
{
if (disposing)
{
Prefabs.Remove(this);
}
}
Disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
@@ -0,0 +1,49 @@
#nullable enable
namespace Barotrauma.MapCreatures.Behavior
{
class BallastFloraStateMachine
{
private readonly BallastFloraBehavior parent;
public BallastFloraStateMachine(BallastFloraBehavior parent)
{
this.parent = parent;
}
private IBallastFloraState? lastState;
public IBallastFloraState? State;
public void EnterState(IBallastFloraState newState)
{
lastState = State;
State?.Exit();
newState.Enter();
State = newState;
}
public void Update(float deltaTime)
{
if (State == null)
{
EnterState(new GrowIdleState(parent));
return;
}
State.Update(deltaTime);
switch (State.GetState())
{
case ExitState.Running:
break;
case ExitState.ReturnLast when lastState != null && lastState.GetState() == ExitState.Running:
EnterState(lastState);
break;
default:
EnterState(new GrowIdleState(parent));
break;
}
}
}
}
@@ -0,0 +1,126 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.MapCreatures.Behavior
{
class DefendWithPumpState : IBallastFloraState
{
private readonly BallastFloraBranch targetBranch;
private readonly List<Pump> allAvailablePumps = new List<Pump>();
private readonly List<Door> allAvailableDoors = new List<Door>();
private readonly List<Pump> targetPumps = new List<Pump>();
private readonly List<Door> jammedDoors = new List<Door>();
private bool isFinished;
private float timer = 10f;
private bool filled;
private bool tryDrown;
private readonly Character attacker;
public DefendWithPumpState(BallastFloraBranch branch, List<Item> items, Character attacker)
{
targetBranch = branch;
this.attacker = attacker;
foreach (Item item in items)
{
if (item.GetComponent<Pump>() is { } pump)
{
allAvailablePumps.Add(pump);
}
if (item.GetComponent<Door>() is { } door)
{
allAvailableDoors.Add(door);
}
}
}
public ExitState GetState()
{
if (isFinished) { return ExitState.ReturnLast; }
if (targetBranch.CurrentHull == null) { return ExitState.ReturnLast; }
return timer < 0 ? ExitState.ReturnLast : ExitState.Running;
}
public void Enter()
{
foreach (Pump pump in allAvailablePumps)
{
if (pump.Item.CurrentHull == targetBranch.CurrentHull)
{
targetPumps.Add(pump);
SetPump(pump);
pump.Hijacked = true;
}
}
if (!targetPumps.Any() || targetPumps.All(p => !p.HasPower))
{
isFinished = true;
}
// lock the doors if the attacker is in the same hull as the ballast flora to try to drown them
if (targetBranch.CurrentHull != null && attacker != null && attacker.CurrentHull == targetBranch.CurrentHull)
{
foreach (Door door in allAvailableDoors)
{
if (door.LinkedGap != null && door.LinkedGap.linkedTo.Contains(targetBranch.CurrentHull))
{
door.TrySetState(false, false, true);
door.IsJammed = true;
jammedDoors.Add(door);
}
}
tryDrown = true;
}
}
public void Exit()
{
foreach (Pump pump in targetPumps)
{
pump.Hijacked = false;
}
foreach (Door door in jammedDoors)
{
door.IsJammed = false;
}
}
private void SetPump(Pump pump)
{
if (pump.TargetLevel != null)
{
pump.TargetLevel = 100f;
}
else
{
pump.FlowPercentage = 100f;
}
}
public void Update(float deltaTime)
{
foreach (Pump pump in targetPumps)
{
SetPump(pump);
}
if (tryDrown && !filled)
{
// keep the ballast filled for extra 10 seconds
if (targetBranch.CurrentHull == null || targetBranch.CurrentHull.WaterPercentage >= 95f)
{
filled = true;
timer += 10f;
}
}
timer -= deltaTime;
}
}
}
@@ -0,0 +1,187 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
namespace Barotrauma.MapCreatures.Behavior
{
internal class GrowIdleState: IBallastFloraState
{
public readonly BallastFloraBehavior Behavior;
private float growthTimer;
public GrowIdleState(BallastFloraBehavior behavior)
{
Behavior = behavior;
}
public virtual ExitState GetState() => ExitState.Running;
public virtual void Enter()
{
foreach (BallastFloraBranch branch in Behavior.Branches.Where(b => b.CanGrowMore()))
{
if (TryScanTargets(branch)) { return; }
}
}
public void Exit() { }
private bool TryScanTargets(BallastFloraBranch branch)
{
if (ScanForTargets(branch) is { } newTarget)
{
Behavior.StateMachine.EnterState(new GrowToTargetState(Behavior, branch, newTarget));
return true;
}
return false;
}
public void Update(float deltaTime)
{
if (growthTimer > 0)
{
growthTimer -= Behavior.GetGrowthSpeed(deltaTime);
}
else
{
Grow();
UpdateIgnoredTargets();
growthTimer = Behavior.GrowthWarps > 0 ? 0f : 5f;
}
}
protected virtual void Grow()
{
List<BallastFloraBranch> newTiles = GrowRandomly();
#if DEBUG || UNSTABLE
Behavior.debugSearchLines.Clear();
#endif
if (newTiles.Any(TryScanTargets)) { return; }
}
public void UpdateIgnoredTargets()
{
Behavior.IgnoredTargets.ForEachMod(pair =>
{
var (item, delay) = pair;
if (delay <= 0)
{
Behavior.IgnoredTargets.Remove(item);
}
else
{
Behavior.IgnoredTargets[item] = --delay;
}
});
}
private List<BallastFloraBranch> GrowRandomly()
{
List<BallastFloraBranch> newBranches = new List<BallastFloraBranch>();
List<BallastFloraBranch> newList = new List<BallastFloraBranch>(Behavior.Branches);
foreach (BallastFloraBranch branch in newList)
{
if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; }
if (Rand.Range(0, Behavior.Branches.Count(tile => tile.CanGrowMore())) != 0) { continue; }
TileSide side = branch.GetRandomFreeSide();
if (side == TileSide.None) { continue; }
Behavior.TryGrowBranch(branch, side, out List<BallastFloraBranch> result);
newBranches.AddRange(result);
}
return newBranches;
}
private Item? ScanForTargets(VineTile branch)
{
Hull parent = Behavior.Parent;
Vector2 worldPos = Behavior.GetWorldPosition() + branch.Position;
Vector2 pos = parent.Position + Behavior.Offset + branch.Position;
Vector2 diameter = ConvertUnits.ToSimUnits(new Vector2(branch.Rect.Width / 2f, branch.Rect.Height / 2f));
Vector2 topLeft = ConvertUnits.ToSimUnits(pos) - diameter;
Vector2 bottomRight = ConvertUnits.ToSimUnits(pos) + diameter;
int highestPriority = 0;
Item? currentItem = null;
foreach (Item item in Item.ItemList.Where(it => !Behavior.ClaimedTargets.Contains(it)))
{
if (Behavior.IgnoredTargets.ContainsKey(item)) { continue; }
int priority = 0;
foreach (BallastFloraBehavior.AITarget target in Behavior.Targets)
{
if (!target.Matches(item) || target.Priority <= highestPriority) { continue; }
priority = target.Priority;
break;
}
if (priority == 0) { continue; }
if (item.Submarine != parent.Submarine || Vector2.Distance(worldPos, item.WorldPosition) > Behavior.Sight) { continue; }
Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position);
#if DEBUG || UNSTABLE
Tuple<Vector2, Vector2> debugLine1 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(topLeft), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos - diameter));
Tuple<Vector2, Vector2> debugLine2 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(bottomRight), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos + diameter));
Behavior.debugSearchLines.Add(debugLine2);
Behavior.debugSearchLines.Add(debugLine1);
#endif
Body? body1 = Submarine.CheckVisibility(itemSimPos - diameter, topLeft);
if (Blocks(body1, item)) { continue; }
Body? body2 = Submarine.CheckVisibility(itemSimPos + diameter, bottomRight);
if (Blocks(body2, item)) { continue; }
highestPriority = priority;
currentItem = item;
}
if (currentItem != null)
{
foreach (BallastFloraBranch existingBranch in Behavior.Branches)
{
if (Behavior.BranchContainsTarget(existingBranch, currentItem))
{
Behavior.ClaimTarget(currentItem, existingBranch);
return null;
}
}
return currentItem;
}
return null;
static bool Blocks(Body body, Item target)
{
if (body == null) { return false; }
switch (body.UserData)
{
case Submarine _:
case Structure _:
case Item it when it != target:
return true;
default:
return false;
}
}
}
}
}
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma.MapCreatures.Behavior
{
class GrowToTargetState: GrowIdleState
{
public readonly List<BallastFloraBranch> TargetBranches = new List<BallastFloraBranch>();
public readonly Item Target;
private bool isFinished;
public GrowToTargetState(BallastFloraBehavior behavior, BallastFloraBranch starter, Item target) : base(behavior)
{
Target = target;
TargetBranches.Add(starter);
}
// do nothing
public override void Enter() { }
public override ExitState GetState() => isFinished ? ExitState.Terminate : ExitState.Running;
protected override void Grow()
{
if (Target == null || Target.Removed)
{
isFinished = true;
return;
}
GrowTowardsTarget();
}
private void GrowTowardsTarget()
{
bool succeeded = false;
List<BallastFloraBranch> newList = new List<BallastFloraBranch>(TargetBranches);
foreach (BallastFloraBranch branch in newList)
{
if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; }
// Get what side gets us closest to the target
TileSide side = GetClosestSide(branch, Target.WorldPosition);
if (branch.IsSideBlocked(side)) { continue; }
succeeded |= Behavior.TryGrowBranch(branch, side, out List<BallastFloraBranch> newBranches);
TargetBranches.AddRange(newBranches);
foreach (BallastFloraBranch newBranch in newBranches)
{
Rectangle worldRect = newBranch.Rect;
worldRect.Location = Behavior.GetWorldPosition().ToPoint() + worldRect.Location;
if (Behavior.BranchContainsTarget(newBranch, Target))
{
Behavior.ClaimTarget(Target, newBranch);
isFinished = true;
return;
}
}
}
if (!succeeded)
{
if (!Behavior.IgnoredTargets.ContainsKey(Target))
{
Behavior.IgnoredTargets.Add(Target, 1);
}
isFinished = true;
}
}
private TileSide GetClosestSide(VineTile tile, Vector2 targetPos)
{
var (distX, distY) = tile.Position + Behavior.GetWorldPosition() - targetPos;
int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
return absDistX > absDistY ? distX > 0 ? TileSide.Left : TileSide.Right : distY > 0 ? TileSide.Bottom : TileSide.Top;
}
}
}
@@ -0,0 +1,21 @@
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
namespace Barotrauma.MapCreatures.Behavior
{
enum ExitState
{
Running, // State is running
Terminate, // State has exited
ReturnLast // Return to the last running state if any
}
interface IBallastFloraState
{
public void Enter();
public void Exit();
public void Update(float deltaTime);
public ExitState GetState();
}
}
@@ -11,6 +11,9 @@ namespace Barotrauma
{
public const ushort NullEntityID = 0;
public const ushort EntitySpawnerID = ushort.MaxValue;
public const ushort RespawnManagerID = ushort.MaxValue - 1;
public const ushort ReservedIDStart = ushort.MaxValue - 2;
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static IEnumerable<Entity> GetEntities()
@@ -22,8 +25,6 @@ namespace Barotrauma
public static EntitySpawner Spawner;
private ushort id;
protected AITarget aiTarget;
private bool idFreed;
@@ -39,53 +40,7 @@ namespace Barotrauma
get { return idFreed; }
}
public ushort ID
{
get
{
return id;
}
set
{
if (this is EntitySpawner) { return; }
if (value == NullEntityID)
{
DebugConsole.ThrowError("Cannot set the ID of an entity to " + NullEntityID +
"! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (value == EntitySpawnerID)
{
DebugConsole.ThrowError("Cannot set the ID of an entity to " + EntitySpawnerID +
"! The value is reserved for EntitySpawner.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (dictionary.TryGetValue(id, out Entity thisEntity) && thisEntity == this)
{
dictionary.Remove(id);
}
//if there's already an entity with the same ID, give it the old ID of this one
if (dictionary.TryGetValue(value, out Entity existingEntity))
{
DebugConsole.Log(existingEntity + " had the same ID as " + this + " (" + value + ")");
dictionary.Remove(value);
dictionary.Add(id, existingEntity);
existingEntity.id = id;
DebugConsole.Log("The id of " + existingEntity + " is now " + id);
DebugConsole.Log("The id of " + this + " is now " + value);
}
id = value;
idFreed = false;
dictionary.Add(id, this);
}
}
/// <summary>
/// The ID the entity had after instantiation/loading. May have been taken up by another entity, causing a new ID to be assigned to this entity.
/// </summary>
public ushort OriginalID;
public readonly ushort ID;
public virtual Vector2 SimPosition
{
@@ -125,17 +80,27 @@ namespace Barotrauma
private readonly double spawnTime;
public Entity(Submarine submarine)
public Entity(Submarine submarine, ushort id)
{
this.Submarine = submarine;
spawnTime = Timing.TotalTime;
if (id != NullEntityID && dictionary.ContainsKey(id))
{
throw new Exception($"ID {id} is taken by {dictionary[id].ToString()}");
}
//give a unique ID
id = OriginalID = this is EntitySpawner ?
EntitySpawnerID :
FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset);
ID = DetermineID(id, submarine);
dictionary.Add(id, this);
dictionary.Add(ID, this);
}
protected virtual ushort DetermineID(ushort id, Submarine submarine)
{
return id != NullEntityID ?
id :
FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset);
}
public static ushort FindFreeID(ushort idOffset = 0)
@@ -153,7 +118,7 @@ namespace Barotrauma
{
id += 1;
IDfound = dictionary.ContainsKey(id);
} while (IDfound || id == NullEntityID || id == EntitySpawnerID);
} while (IDfound || id == NullEntityID || id > ReservedIDStart);
return id;
}
@@ -192,7 +157,7 @@ namespace Barotrauma
errorMsg.AppendLine("Some entities were not removed in Entity.RemoveAll:");
foreach (Entity e in dictionary.Values)
{
errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.id + ")");
errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.ID + ")");
}
}
if (Item.ItemList.Count > 0)
@@ -200,7 +165,7 @@ namespace Barotrauma
errorMsg.AppendLine("Some items were not removed in Entity.RemoveAll:");
foreach (Item item in Item.ItemList)
{
errorMsg.AppendLine(" - " + item.Name + "(ID " + item.id + ")");
errorMsg.AppendLine(" - " + item.Name + "(ID " + item.ID + ")");
}
var items = new List<Item>(Item.ItemList);
@@ -222,7 +187,7 @@ namespace Barotrauma
errorMsg.AppendLine("Some characters were not removed in Entity.RemoveAll:");
foreach (Character character in Character.CharacterList)
{
errorMsg.AppendLine(" - " + character.Name + "(ID " + character.id + ")");
errorMsg.AppendLine(" - " + character.Name + "(ID " + character.ID + ")");
}
var characters = new List<Character>(Character.CharacterList);
@@ -293,15 +258,15 @@ namespace Barotrauma
public static void DumpIds(int count, string filename)
{
List<Entity> entities = dictionary.Values.OrderByDescending(e => e.id).ToList();
List<Entity> entities = dictionary.Values.OrderByDescending(e => e.ID).ToList();
count = Math.Min(entities.Count, count);
List<string> lines = new List<string>();
for (int i = 0; i < count; i++)
{
lines.Add(entities[i].id + ": " + entities[i].ToString());
DebugConsole.ThrowError(entities[i].id + ": " + entities[i].ToString());
lines.Add(entities[i].ID + ": " + entities[i].ToString());
DebugConsole.ThrowError(entities[i].ID + ": " + entities[i].ToString());
}
if (!string.IsNullOrWhiteSpace(filename))
@@ -6,6 +6,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
namespace Barotrauma
{
@@ -30,8 +32,10 @@ namespace Barotrauma
private readonly float decalSize;
public float EmpStrength { get; set; }
public float BallastFloraDamage { get; set; }
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f)
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f)
{
attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range)
{
@@ -39,6 +43,7 @@ namespace Barotrauma
};
this.force = force;
this.EmpStrength = empStrength;
BallastFloraDamage = ballastFloraStrength;
sparks = true;
shockwave = true;
smoke = true;
@@ -65,6 +70,7 @@ namespace Barotrauma
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f);
decal = element.GetAttributeString("decal", "");
decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize");
@@ -125,7 +131,12 @@ namespace Barotrauma
if (attack.GetStructureDamage(1.0f) > 0.0f)
{
RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attacker);
RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attack.GetLevelWallDamage(1.0f), attacker);
}
if (BallastFloraDamage > 0.0f)
{
RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker);
}
if (EmpStrength > 0.0f)
@@ -167,8 +178,12 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.Condition <= 0.0f) { continue; }
if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; }
if (applyFireEffects && !item.FireProof)
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius));
if (dist > attack.Range) { continue; }
if (dist < attack.Range * 0.5f && applyFireEffects && !item.FireProof)
{
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
@@ -177,9 +192,9 @@ namespace Barotrauma
while (container != null)
{
if (container.FireProof)
{
fireProof = true;
break;
{
fireProof = true;
break;
}
container = container.Container;
}
@@ -190,20 +205,18 @@ namespace Barotrauma
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
}
}
}
}
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
{
float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range)
{
continue;
}
float distFactor = 1.0f - dist / attack.Range;
float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
Vector2 explosionPos = worldPosition;
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition);
item.Condition -= damageAmount * distFactor;
}
}
@@ -241,7 +254,7 @@ namespace Barotrauma
List<Affliction> modifiedAfflictions = new List<Affliction>();
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered || limb.ignoreCollisions) { continue; }
if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; }
float dist = Vector2.Distance(limb.WorldPosition, worldPosition);
@@ -255,37 +268,7 @@ namespace Barotrauma
float distFactor = 1.0f - dist / attack.Range;
//solid obstacles between the explosion and the limb reduce the effect of the explosion
var obstacles = Submarine.PickBodies(limb.SimPosition, explosionPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall);
foreach (var body in obstacles)
{
if (body.UserData is Item item)
{
var door = item.GetComponent<Door>();
if (door != null && !door.IsBroken) { distFactor *= 0.01f; }
}
else if (body.UserData is Structure structure)
{
int sectionIndex = structure.FindSectionIndex(worldPosition, world: true, clamp: true);
if (structure.SectionBodyDisabled(sectionIndex))
{
continue;
}
else if (structure.SectionIsLeaking(sectionIndex))
{
distFactor *= 0.1f;
}
else
{
distFactor *= 0.01f;
}
}
else
{
distFactor *= 0.1f;
}
}
if (distFactor <= 0.05f) { continue; }
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition);
distFactors.Add(limb, distFactor);
modifiedAfflictions.Clear();
@@ -372,7 +355,7 @@ namespace Barotrauma
/// <summary>
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
/// </summary>
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null)
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null)
{
List<Structure> structureList = new List<Structure>();
float dist = 600.0f;
@@ -395,23 +378,123 @@ namespace Barotrauma
{
float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange);
if (distFactor <= 0.0f) continue;
structure.AddDamage(i, damage * distFactor, attacker);
if (damagedStructures.ContainsKey(structure))
{
{
damagedStructures[structure] += damage * distFactor;
}
else
{
damagedStructures.Add(structure, damage * distFactor);
}
}
}
}
if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f))
{
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
{
if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; }
foreach (var cell in destructibleWall.Cells)
{
if (cell.IsPointInside(worldPosition))
{
destructibleWall.AddDamage(levelWallDamage, worldPosition);
continue;
}
foreach (var edge in cell.Edges)
{
if (!MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection))
{
continue;
}
float wallDist = Vector2.DistanceSquared(worldPosition, intersection);
if (wallDist < worldRange * worldRange)
{
destructibleWall.AddDamage(damage, worldPosition);
break;
}
}
}
}
}
return damagedStructures;
}
public void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null)
{
List<BallastFloraBehavior> ballastFlorae = new List<BallastFloraBehavior>();
foreach (Hull hull in Hull.hullList)
{
if (hull.BallastFlora != null) { ballastFlorae.Add(hull.BallastFlora); }
}
foreach (BallastFloraBehavior ballastFlora in ballastFlorae)
{
float resistanceMuliplier = ballastFlora.HasBrokenThrough ? 1f : 1f - ballastFlora.ExplosionResistance;
ballastFlora.Branches.ForEachMod(branch =>
{
Vector2 branchWorldPos = ballastFlora.GetWorldPosition() + branch.Position;
float branchDist = Vector2.Distance(branchWorldPos, worldPosition);
if (branchDist < worldRange)
{
float distFactor = 1.0f - (branchDist / worldRange);
if (distFactor <= 0.0f) { return; }
Vector2 explosionPos = worldPosition;
Vector2 branchPos = branchWorldPos;
if (ballastFlora.Parent?.Submarine != null)
{
explosionPos -= ballastFlora.Parent.Submarine.Position;
branchPos -= ballastFlora.Parent.Submarine.Position;
}
distFactor *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, ConvertUnits.ToSimUnits(branchPos));
ballastFlora.DamageBranch(branch, damage * distFactor * resistanceMuliplier, BallastFloraBehavior.AttackType.Explosives, attacker);
}
});
}
}
private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos)
{
float damageMultiplier = 1.0f;
var obstacles = Submarine.PickBodies(targetSimPos, explosionSimPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall);
foreach (var body in obstacles)
{
if (body.UserData is Item item)
{
var door = item.GetComponent<Door>();
if (door != null && !door.IsBroken) { damageMultiplier *= 0.01f; }
}
else if (body.UserData is Structure structure)
{
int sectionIndex = structure.FindSectionIndex(explosionWorldPos, world: true, clamp: true);
if (structure.SectionBodyDisabled(sectionIndex))
{
continue;
}
else if (structure.SectionIsLeaking(sectionIndex))
{
damageMultiplier *= 0.1f;
}
else
{
damageMultiplier *= 0.01f;
}
}
else
{
damageMultiplier *= 0.1f;
}
}
return damageMultiplier;
}
static partial void PlayTinnitusProjSpecific(float volume);
}
}
@@ -302,7 +302,7 @@ namespace Barotrauma
return;
}
float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered);
float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered && !l.Hidden);
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
@@ -129,8 +129,8 @@ namespace Barotrauma
: this(rect, rect.Width < rect.Height, submarine)
{ }
public Gap(Rectangle rect, bool isHorizontal, Submarine submarine)
: base(MapEntityPrefab.Find(null, "gap"), submarine)
public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID)
: base(MapEntityPrefab.Find(null, "gap"), submarine, id)
{
this.rect = rect;
flowForce = Vector2.Zero;
@@ -141,7 +141,8 @@ namespace Barotrauma
GapList.Add(this);
InsertToList();
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f);
float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2;
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize);
outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})";
outsideCollisionBlocker.BodyType = BodyType.Static;
outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall;
@@ -711,7 +712,7 @@ namespace Barotrauma
if (!DisableHullRechecks) FindHulls();
}
public static Gap Load(XElement element, Submarine submarine)
public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = Rectangle.Empty;
@@ -737,12 +738,10 @@ namespace Barotrauma
isHorizontal = horizontalAttribute.Value.ToString() == "true";
}
Gap g = new Gap(rect, isHorizontal, submarine)
Gap g = new Gap(rect, isHorizontal, submarine, idRemap.GetOffsetId(element))
{
ID = (ushort)int.Parse(element.Attribute("ID").Value),
linkedToID = new List<ushort>(),
};
g.OriginalID = g.ID;
return g;
}
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.MapCreatures.Behavior;
namespace Barotrauma
{
@@ -391,14 +392,16 @@ namespace Barotrauma
public List<DummyFireSource> FakeFireSources { get; private set; }
public BallastFloraBehavior BallastFlora { get; set; }
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
: this (prefab, rectangle, Submarine.MainSub)
{
}
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine)
: base (prefab, submarine)
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID)
: base (prefab, submarine, id)
{
rect = rectangle;
@@ -524,6 +527,8 @@ namespace Barotrauma
}
}
Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
BallastFlora?.OnMapLoaded();
}
public void AddToGrid(Submarine submarine)
@@ -607,6 +612,7 @@ namespace Barotrauma
{
base.Remove();
hullList.Remove(this);
BallastFlora?.Remove();
if (Submarine != null && !Submarine.Loading && !Submarine.Unloading)
{
@@ -687,6 +693,9 @@ namespace Barotrauma
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
BallastFlora?.Update(deltaTime);
UpdateProjSpecific(deltaTime, cam);
Oxygen -= OxygenDeteriorationSpeed * deltaTime;
@@ -1352,7 +1361,7 @@ namespace Barotrauma
}
#endregion
public static Hull Load(XElement element, Submarine submarine)
public static Hull Load(XElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect;
if (element.Attribute("rect") != null)
@@ -1369,23 +1378,13 @@ namespace Barotrauma
int.Parse(element.Attribute("height").Value));
}
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine)
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element))
{
WaterVolume = element.GetAttributeFloat("pressure", 0.0f),
ID = (ushort)int.Parse(element.Attribute("ID").Value)
WaterVolume = element.GetAttributeFloat("pressure", 0.0f)
};
hull.OriginalID = hull.ID;
hull.linkedToID = new List<ushort>();
string linkedToString = element.GetAttributeString("linked", "");
if (linkedToString != "")
{
string[] linkedToIds = linkedToString.Split(',');
for (int i = 0; i < linkedToIds.Length; i++)
{
hull.linkedToID.Add((ushort)int.Parse(linkedToIds[i]));
}
}
hull.ParseLinks(element, idRemap);
string originalAmbientLight = element.GetAttributeString("originalambientlight", null);
if (!string.IsNullOrWhiteSpace(originalAmbientLight))
@@ -1410,6 +1409,15 @@ namespace Barotrauma
decal.BaseAlpha = baseAlpha;
}
break;
case "ballastflorabehavior":
string identifier = subElement.GetAttributeString("identifier", string.Empty);
BallastFloraPrefab prefab = BallastFloraPrefab.Find(identifier);
if (prefab != null)
{
hull.BallastFlora = new BallastFloraBehavior(hull, prefab, Vector2.Zero);
hull.BallastFlora.LoadSave(subElement);
}
break;
}
}
@@ -1488,6 +1496,8 @@ namespace Barotrauma
));
}
BallastFlora?.Save(element);
SerializableProperty.SerializeProperties(this, element);
parentElement.Add(element);
return element;
@@ -8,5 +8,6 @@ namespace Barotrauma
Vector2 WorldPosition { get; }
Vector2 SimPosition { get; }
Submarine Submarine { get; }
bool IgnoreByAI => false;
}
}
@@ -114,14 +114,16 @@ namespace Barotrauma
#if CLIENT
if (Screen.Selected is SubEditorScreen)
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false));
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false));
}
#endif
}
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false)
{
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, FilePath);
int idOffset = Entity.FindFreeID(1);
if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); }
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, FilePath, idOffset);
if (entities.Count == 0) { return entities; }
Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition;
@@ -132,7 +134,16 @@ namespace Barotrauma
me.Submarine = sub;
if (!(me is Item item)) { continue; }
Wire wire = item.GetComponent<Wire>();
if (wire != null) { wire.MoveNodes(position - offset); }
//Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;
if (wire != null)
{
//fix wires that have been erroneously saved at the "hidden position"
if (sub != null && Vector2.Distance(me.Position, sub.HiddenSubPosition) > sub.HiddenSubPosition.Length() / 2)
{
me.Move(position);
}
wire.MoveNodes(position - offset);
}
}
MapEntity.MapLoaded(entities, true);
@@ -0,0 +1,197 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class CaveGenerationParams : ISerializableEntity
{
public static List<CaveGenerationParams> CaveParams { get; private set; }
public string Name
{
get { return Identifier; }
}
public readonly string Identifier;
private int minWidth, maxWidth;
private int minHeight, maxHeight;
private int minBranchCount, maxBranchCount;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
set;
}
/// <summary>
/// Overrides the commonness of the object in a specific level type.
/// Key = name of the level type, value = commonness in that level type.
/// </summary>
public readonly Dictionary<string, float> OverrideCommonness = new Dictionary<string, float>();
[Editable, Serialize(1.0f, true)]
public float Commonness
{
get;
private set;
}
[Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)]
public int MinWidth
{
get { return minWidth; }
set { minWidth = Math.Max(value, 1000); }
}
[Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)]
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = Math.Max(value, minWidth); }
}
[Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)]
public int MinHeight
{
get { return minHeight; }
set { minHeight = Math.Max(value, 1000); }
}
[Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)]
public int MaxHeight
{
get { return maxHeight; }
set { maxHeight = Math.Max(value, minHeight); }
}
[Serialize(2, true), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MinBranchCount
{
get { return minBranchCount; }
set { minBranchCount = Math.Max(value, 0); }
}
[Serialize(4, true), Editable(MinValueInt = 0, MaxValueInt = 10)]
public int MaxBranchCount
{
get { return maxBranchCount; }
set { maxBranchCount = Math.Max(value, minBranchCount); }
}
[Serialize(50, true), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int LevelObjectAmount
{
get;
set;
}
[Serialize(0.1f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )]
public float DestructibleWallRatio
{
get;
set;
}
public Sprite WallSprite { get; private set; }
public Sprite WallEdgeSprite { get; private set; }
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, Rand.RandSync rand)
{
if (CaveParams.All(p => p.GetCommonness(generationParams) <= 0.0f))
{
return CaveParams.First();
}
return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams)).ToList(), rand);
}
public float GetCommonness(LevelGenerationParams generationParams)
{
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
{
return commonness;
}
return Commonness;
}
private CaveGenerationParams(XElement element)
{
Identifier = element == null ? "default" : element.GetAttributeString("identifier", null) ?? element.Name.ToString();
Identifier = Identifier.ToLowerInvariant();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "wall":
WallSprite = new Sprite(subElement);
break;
case "walledge":
WallEdgeSprite = new Sprite(subElement);
break;
}
}
}
public static void LoadPresets()
{
CaveParams = new List<CaveGenerationParams>();
var files = GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters);
if (!files.Any())
{
files = new List<ContentFile>() { new ContentFile("Content/Map/CaveGenerationParameters.xml", ContentType.CaveGenerationParameters) };
}
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
CaveParams.Clear();
DebugConsole.NewMessage($"Overriding cave generation parameters with '{file.Path}'", Color.Yellow);
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
if (isOverride)
{
string identifier = element.FirstElement().GetAttributeString("identifier", "");
var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (existingParams != null)
{
DebugConsole.NewMessage($"Overriding the cave generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow);
CaveParams.Remove(existingParams);
}
CaveParams.Add(new CaveGenerationParams(element.FirstElement()));
}
else
{
string identifier = element.FirstElement().GetAttributeString("identifier", "");
var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (existingParams != null)
{
DebugConsole.ThrowError($"Duplicate cave generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use <override></override> tags to override the generation parameters.");
continue;
}
else
{
CaveParams.Add(new CaveGenerationParams(element));
}
}
}
}
}
}
}
@@ -1,4 +1,5 @@
using FarseerPhysics;
using FarseerPhysics.Collision.Shapes;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
@@ -80,7 +81,7 @@ namespace Barotrauma
}
}
if (MathUtils.NearlyEqual(ge.Point2.X, borders.X) || MathUtils.NearlyEqual(ge.Point2.X, borders.Right) ||
MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom))
MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom))
{
if (point1 == null)
{
@@ -95,14 +96,43 @@ namespace Barotrauma
if (point1.HasValue && point2.HasValue)
{
Debug.Assert(point1 != point2);
var newEdge = new GraphEdge(point1.Value, point2.Value)
bool point1OnSide = MathUtils.NearlyEqual(point1.Value.X, borders.X) || MathUtils.NearlyEqual(point1.Value.X, borders.Right);
bool point2OnSide = MathUtils.NearlyEqual(point2.Value.X, borders.X) || MathUtils.NearlyEqual(point2.Value.X, borders.Right);
//one point is one the side, another on top/bottom
// -> the cell is in the corner of the level, we need 2 edges
if (point1OnSide != point2OnSide)
{
Cell1 = cell,
IsSolid = true,
Site1 = cell.Site,
OutsideLevel = true
};
cell.Edges.Add(newEdge);
Vector2 cornerPos = new Vector2(
point1.Value.X < borders.Center.X ? borders.X : borders.Right,
point1.Value.Y < borders.Center.Y ? borders.Y : borders.Bottom);
cell.Edges.Add(
new GraphEdge(point1.Value, cornerPos)
{
Cell1 = cell,
IsSolid = true,
Site1 = cell.Site,
OutsideLevel = true
});
cell.Edges.Add(
new GraphEdge(point2.Value, cornerPos)
{
Cell1 = cell,
IsSolid = true,
Site1 = cell.Site,
OutsideLevel = true
});
}
else
{
cell.Edges.Add(
new GraphEdge(point1.Value, point2.Value)
{
Cell1 = cell,
IsSolid = true,
Site1 = cell.Site,
OutsideLevel = true
});
}
break;
}
}
@@ -111,44 +141,16 @@ namespace Barotrauma
return cells;
}
private static Vector2 GetEdgeNormal(GraphEdge edge, VoronoiCell cell = null)
{
if (cell == null) { cell = edge.AdjacentCell(null); }
if (cell == null) { return Vector2.UnitX; }
CompareCCW compare = new CompareCCW(cell.Center);
if (compare.Compare(edge.Point1, edge.Point2) == -1)
{
var temp = edge.Point1;
edge.Point1 = edge.Point2;
edge.Point2 = temp;
}
Vector2 normal = Vector2.Normalize(edge.Point2 - edge.Point1);
Vector2 diffToCell = Vector2.Normalize(cell.Center - edge.Point2);
normal = new Vector2(-normal.Y, normal.X);
if (Vector2.Dot(normal, diffToCell) < 0)
{
normal = -normal;
}
return normal;
}
public static List<VoronoiCell> GeneratePath(
List<Point> pathNodes, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid,
int gridCellSize, Rectangle limits, float wanderAmount = 0.3f, bool mirror = false)
public static void GeneratePath(Level.Tunnel tunnel, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, Rectangle limits)
{
var targetCells = new List<VoronoiCell>();
for (int i = 0; i < pathNodes.Count; i++)
for (int i = 0; i < tunnel.Nodes.Count; i++)
{
//a search depth of 2 is large enough to find a cell in almost all maps, but in case it fails, we increase the depth
int searchDepth = 2;
while (searchDepth < 5)
{
int cellIndex = FindCellIndex(pathNodes[i], cells, cellGrid, gridCellSize, searchDepth);
int cellIndex = FindCellIndex(tunnel.Nodes[i], cells, cellGrid, gridCellSize, searchDepth);
if (cellIndex > -1)
{
targetCells.Add(cells[cellIndex]);
@@ -157,25 +159,15 @@ namespace Barotrauma
searchDepth++;
}
}
return GeneratePath(targetCells, cells, cellGrid, gridCellSize, limits, wanderAmount, mirror);
tunnel.Cells.AddRange(GeneratePath(targetCells, cells, limits));
}
public static List<VoronoiCell> GeneratePath(
List<VoronoiCell> targetCells, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid,
int gridCellSize, Rectangle limits, float wanderAmount = 0.3f, bool mirror = false)
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells, Rectangle limits)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
//how heavily the path "steers" towards the endpoint
//lower values will cause the path to "wander" more, higher will make it head straight to the end
wanderAmount = MathHelper.Clamp(wanderAmount, 0.0f, 1.0f);
List<GraphEdge> allowedEdges = new List<GraphEdge>();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
VoronoiCell currentCell = targetCells[0];
@@ -190,41 +182,24 @@ namespace Barotrauma
{
int edgeIndex = 0;
allowedEdges.Clear();
foreach (GraphEdge edge in currentCell.Edges)
double smallestDist = double.PositiveInfinity;
for (int i = 0; i < currentCell.Edges.Count; i++)
{
var adjacentCell = edge.AdjacentCell(currentCell);
if (adjacentCell != null && limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y))
var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell);
if (adjacentCell == null) { continue; }
double dist = MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y);
dist += MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, currentCell.Site.Coord.X, currentCell.Site.Coord.Y) * 0.5f;
//disfavor small edges to prevent generating a very small passage
if (Vector2.Distance(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 200.0f)
{
allowedEdges.Add(edge);
dist += 1000000;
}
}
//steer towards target
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > wanderAmount || allowedEdges.Count == 0)
{
double smallestDist = double.PositiveInfinity;
for (int i = 0; i < currentCell.Edges.Count; i++)
if (dist < smallestDist)
{
var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell);
if (adjacentCell == null) { continue; }
double dist = MathUtils.Distance(
adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y,
targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y);
if (dist < smallestDist)
{
edgeIndex = i;
smallestDist = dist;
}
edgeIndex = i;
smallestDist = dist;
}
}
//choose random edge (ignoring ones where the adjacent cell is outside limits)
else
{
edgeIndex = Rand.Int(allowedEdges.Count, Rand.RandSync.Server);
if (mirror && edgeIndex > 0) edgeIndex = allowedEdges.Count - edgeIndex;
edgeIndex = currentCell.Edges.IndexOf(allowedEdges[edgeIndex]);
}
currentCell = currentCell.Edges[edgeIndex].AdjacentCell(currentCell);
currentCell.CellType = CellType.Path;
@@ -234,8 +209,8 @@ namespace Barotrauma
if (currentCell == targetCells[currentTargetIndex])
{
currentTargetIndex += 1;
if (currentTargetIndex >= targetCells.Count) break;
currentTargetIndex++;
if (currentTargetIndex >= targetCells.Count) { break; }
}
} while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0);
@@ -256,17 +231,42 @@ namespace Barotrauma
List<GraphEdge> tempEdges = new List<GraphEdge>();
foreach (GraphEdge edge in cell.Edges)
{
if (!edge.IsSolid)
if (!edge.IsSolid || edge.OutsideLevel)
{
tempEdges.Add(edge);
continue;
}
//If the edge is next to an empty cell and there's another solid cell at the other side of the empty one,
//don't touch this edge. Otherwise we may end up closing off small passages between cells.
var adjacentEmptyCell = edge.AdjacentCell(cell);
if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; }
if (adjacentEmptyCell != null)
{
GraphEdge adjacentEdge = null;
//find the edge at the opposite side of the adjacent cell
foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges)
{
if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) < 0 &&
otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType == CellType.Solid)
{
adjacentEdge = otherEdge;
break;
}
}
if (adjacentEdge != null)
{
tempEdges.Add(edge);
continue;
}
}
List<Vector2> edgePoints = new List<Vector2>();
Vector2 edgeNormal = GetEdgeNormal(edge, cell);
Vector2 edgeNormal = edge.GetNormal(cell);
float edgeLength = Vector2.Distance(edge.Point1, edge.Point2);
int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1);
Vector2 edgeDir = (edge.Point2 - edge.Point1);
Vector2 edgeDir = edge.Point2 - edge.Point1;
for (int i = 0; i <= pointCount; i++)
{
if (i == 0)
@@ -279,16 +279,48 @@ namespace Barotrauma
}
else
{
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server);
edgePoints.Add(
Vector2 extrudedPoint =
edge.Point1 +
edgeDir * (i / (float)pointCount) -
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF);
edgeDir * (i / (float)pointCount) +
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
bool isInside = false;
foreach (var nearbyCell in nearbyCells)
{
if (nearbyCell == cell || nearbyCell.CellType != CellType.Solid) { continue; }
//check if extruding the edge causes it to go inside another one
if (nearbyCell.IsPointInside(extrudedPoint))
{
isInside = true;
break;
}
//check if another edge will be inside this cell after the extrusion
Vector2 triangleCenter = (edge.Point1 + edge.Point2 + extrudedPoint) / 3;
foreach (GraphEdge nearbyEdge in nearbyCell.Edges)
{
if (!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) &&
!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) &&
!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2))
{
isInside = true;
break;
}
}
if (isInside) { break; }
}
if (!isInside)
{
edgePoints.Add(extrudedPoint);
}
}
}
for (int i = 0; i < pointCount; i++)
for (int i = 0; i < edgePoints.Count - 1; i++)
{
tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
{
@@ -297,7 +329,10 @@ namespace Barotrauma
IsSolid = edge.IsSolid,
Site1 = edge.Site1,
Site2 = edge.Site2,
OutsideLevel = edge.OutsideLevel
OutsideLevel = edge.OutsideLevel,
NextToCave = edge.NextToCave,
NextToMainPath = edge.NextToMainPath,
NextToSidePath = edge.NextToSidePath
});
}
}
@@ -347,15 +382,27 @@ namespace Barotrauma
continue;
}
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
if (bodyPoints.Count < 2) continue;
Vector2 minVert = tempVertices[0];
Vector2 maxVert = tempVertices[0];
foreach (var vert in tempVertices)
{
minVert = new Vector2(
Math.Min(minVert.X, vert.X),
Math.Min(minVert.Y, vert.Y));
maxVert = new Vector2(
Math.Max(maxVert.X, vert.X),
Math.Max(maxVert.Y, vert.Y));
}
Vector2 center = (minVert + maxVert) / 2;
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center));
if (bodyPoints.Count < 2) { continue; }
if (bodyPoints.Count < 3)
{
foreach (Vector2 vertex in tempVertices)
{
if (bodyPoints.Contains(vertex)) continue;
if (bodyPoints.Contains(vertex)) { continue; }
bodyPoints.Add(vertex);
break;
}
@@ -366,11 +413,11 @@ namespace Barotrauma
cell.BodyVertices.Add(bodyPoints[i]);
bodyPoints[i] = ConvertUnits.ToSimUnits(bodyPoints[i]);
}
if (cell.CellType == CellType.Empty) continue;
if (cell.CellType == CellType.Empty) { continue; }
cellBody.UserData = cell;
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center));
for (int i = 0; i < triangles.Count; i++)
{
@@ -380,13 +427,17 @@ namespace Barotrauma
Vector2 b = triangles[i][1];
Vector2 c = triangles[i][2];
float area = Math.Abs(a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)) / 2.0f;
if (area < 1.0f) continue;
if (area < 1.0f) { continue; }
Vertices bodyVertices = new Vertices(triangles[i]);
var newFixture = cellBody.CreatePolygon(bodyVertices, 5.0f);
newFixture.UserData = cell;
PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f);
Fixture fixture = new Fixture(polygon)
{
UserData = cell
};
cellBody.Add(fixture, resetMassData: false);
if (newFixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon)
if (fixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon)
{
DebugConsole.ThrowError("Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + ")");
GameAnalyticsManager.AddErrorEventOnce(
@@ -394,11 +445,13 @@ namespace Barotrauma
GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
"Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + "). Seed: " + level.Seed);
}
}
}
cell.Body = cellBody;
}
cellBody.CollisionCategories = Physics.CollisionLevel;
cellBody.ResetMassData();
return cellBody;
}
@@ -0,0 +1,214 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using Voronoi2;
namespace Barotrauma
{
partial class DestructibleLevelWall : LevelWall, IDamageable
{
public bool NetworkUpdatePending;
public float Damage
{
get;
private set;
}
public float MaxHealth
{
get;
private set;
} = 1000.0f;
public bool Destroyed
{
get;
private set;
}
public float FadeOutDuration
{
get;
private set;
}
public float FadeOutTimer
{
get;
private set;
}
public Vector2 SimPosition
{
get { return Body.Position; }
}
public Vector2 WorldPosition
{
get { return ConvertUnits.ToDisplayUnits(Body.Position); }
}
public float Health
{
get { return MaxHealth - Damage; }
}
public DestructibleLevelWall(List<Vector2> vertices, Color color, Level level, float? health = null, bool giftWrap = false)
: base (vertices, color, level, giftWrap)
{
MaxHealth = health ?? MathHelper.Clamp(Body.Mass, 100.0f, 1000.0f);
Cells.ForEach(c => c.IsDestructible = true);
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (FadeOutDuration > 0.0f)
{
FadeOutTimer += deltaTime;
if (FadeOutTimer > FadeOutDuration && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsClient)) { Destroy(); }
}
}
public void AddDamage(float damage, Vector2 worldPosition)
{
AddDamageProjSpecific(damage, worldPosition);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Destroyed) { return; }
if (!MathUtils.NearlyEqual(damage, 0.0f)) { NetworkUpdatePending = true; }
Damage += damage;
if (Damage >= MaxHealth)
{
CreateFragments();
Destroy();
}
}
partial void AddDamageProjSpecific(float damage, Vector2 worldPosition);
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
{
AddDamage(attack.StructureDamage, worldPosition);
return new AttackResult(attack.StructureDamage);
}
private void CreateFragments()
{
#if CLIENT
SoundPlayer.PlaySound("icebreak", WorldPosition);
#endif
//generate initial triangles (one triangle from each edge to the center of the cell)
List<List<Vector2>> triangles = new List<List<Vector2>>();
foreach (var cell in Cells)
{
foreach (GraphEdge edge in cell.Edges)
{
List<Vector2> triangleVerts = new List<Vector2>
{
edge.Point1 + cell.Translation,
edge.Point2 + cell.Translation,
cell.Center
};
triangles.Add(triangleVerts);
}
}
//split triangles that have edges more than 1000 units long
Pair<int, int> longestEdge = new Pair<int, int>(-1, -1);
float longestEdgeLength = 0.0f;
do
{
longestEdge.First = -1;
longestEdge.Second = -1;
longestEdgeLength = 0.0f;
for (int i = 0; i < triangles.Count; i++)
{
for (int edge = 0; edge < 3; edge++)
{
float edgeLength = Vector2.Distance(triangles[i][edge], triangles[i][(edge + 1) % 3]);
if (edgeLength > longestEdgeLength)
{
longestEdge.First = i;
longestEdge.Second = edge;
longestEdgeLength = edgeLength;
}
}
}
if (longestEdgeLength < 1000.0f)
{
break;
}
Vector2 p0 = triangles[longestEdge.First][longestEdge.Second];
Vector2 p1 = triangles[longestEdge.First][(longestEdge.Second + 1) % 3];
Vector2 p2 = triangles[longestEdge.First][(longestEdge.Second + 2) % 3];
triangles[longestEdge.First] = new List<Vector2> { p0, (p0 + p1) / 2, p2 };
triangles.Add(new List<Vector2> { (p0 + p1) / 2, p1, p2 });
} while (triangles.Count < 32);
//generate fragments
foreach (var triangle in triangles)
{
Vector2 triangleCenter = (triangle[0] + triangle[1]+ triangle[2]) / 3;
triangle[0] -= triangleCenter;
triangle[1] -= triangleCenter;
triangle[2] -= triangleCenter;
Vector2 simTriangleCenter = ConvertUnits.ToSimUnits(triangleCenter);
DestructibleLevelWall fragment = new DestructibleLevelWall(triangle, Color.White, Level.Loaded, giftWrap: true);
fragment.Damage = fragment.MaxHealth;
fragment.Body.Position = simTriangleCenter;
fragment.Body.BodyType = BodyType.Dynamic;
fragment.Body.FixedRotation = false;
fragment.Body.LinearDamping = Rand.Range(0.2f, 0.3f);
fragment.Body.AngularDamping = Rand.Range(0.1f, 0.2f);
fragment.Body.GravityScale = 0.1f;
fragment.Body.Mass *= 10.0f;
fragment.Body.CollisionCategories = Physics.CollisionNone;
fragment.Body.CollidesWith = Physics.CollisionWall;
fragment.FadeOutDuration = 20.0f;
Vector2 bodyDiff = simTriangleCenter - Body.Position;
fragment.Body.LinearVelocity = (bodyDiff + Rand.Vector(0.5f)).ClampLength(15.0f);
fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f);// MathHelper.Clamp(-bodyDiff.X * 0.1f, -0.5f, 0.5f);
Level.Loaded.UnsyncedExtraWalls.Add(fragment);
#if CLIENT
for (int i = 0; i < 20; i++)
{
int startEdgeIndex = Rand.Int(3);
Vector2 pos1 = triangle[startEdgeIndex];
Vector2 pos2 = triangle[(startEdgeIndex + 1) % 3];
var particle = GameMain.ParticleManager.CreateParticle("iceshards",
triangleCenter + Vector2.Lerp(pos1, pos2, Rand.Range(0.0f, 1.0f)),
Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f);
if (particle != null)
{
particle.Size *= Rand.Range(1.0f, 5.0f);
}
}
#endif
}
}
public void Destroy()
{
if (Destroyed) { return; }
Destroyed = true;
level?.UnsyncedExtraWalls?.Remove(this);
foreach (var cell in Cells)
{
cell.CellType = CellType.Removed;
}
GameMain.World.Remove(Body);
Dispose();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -26,13 +26,33 @@ namespace Barotrauma
public readonly LevelGenerationParams GenerationParams;
public bool HasBeaconStation;
public bool IsBeaconActive;
public OutpostGenerationParams ForceOutpostGenerationParams;
public readonly Point Size;
public readonly int InitialDepth;
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
public float CrushDepth
{
get
{
return Math.Max(Size.Y, Level.DefaultRealWorldCrushDepth / Physics.DisplayToRealWorldRatio) - InitialDepth;
}
}
public float RealWorldCrushDepth
{
get
{
return Math.Max(Size.Y * Physics.DisplayToRealWorldRatio, Level.DefaultRealWorldCrushDepth);
}
}
public LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome)
{
Seed = seed ?? throw new ArgumentException("Seed was null");
@@ -44,6 +64,8 @@ namespace Barotrauma
sizeFactor = MathHelper.Clamp(sizeFactor, 0.0f, 1.0f);
int width = (int)MathHelper.Lerp(generationParams.MinWidth, generationParams.MaxWidth, sizeFactor);
InitialDepth = (int)MathHelper.Lerp(generationParams.InitialDepthMin, generationParams.InitialDepthMax, sizeFactor);
Size = new Point(
(int)MathUtils.Round(width, Level.GridCellSize),
(int)MathUtils.Round(generationParams.Height, Level.GridCellSize));
@@ -56,8 +78,11 @@ namespace Barotrauma
Size = element.GetAttributePoint("size", new Point(1000));
Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type);
HasBeaconStation = element.GetAttributeBool("hasbeaconstation", false);
IsBeaconActive = element.GetAttributeBool("isbeaconactive", false);
string generationParamsId = element.GetAttributeString("generationparams", "");
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId);
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId);
if (GenerationParams == null)
{
DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\".");
@@ -68,8 +93,10 @@ namespace Barotrauma
}
}
InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin);
string biomeIdentifier = element.GetAttributeString("biome", "");
Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier);
Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier || b.OldIdentifier == biomeIdentifier);
if (Biome == null)
{
DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\".");
@@ -104,6 +131,12 @@ namespace Barotrauma
Size = new Point(
(int)MathUtils.Round(width, Level.GridCellSize),
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
var rand = new MTRandom(ToolBox.StringToInt(Seed));
InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble());
HasBeaconStation = rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
IsBeaconActive = false;
}
/// <summary>
@@ -119,6 +152,7 @@ namespace Barotrauma
var rand = new MTRandom(ToolBox.StringToInt(Seed));
int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble());
InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble());
Size = new Point(
(int)MathUtils.Round(width, Level.GridCellSize),
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
@@ -136,16 +170,24 @@ namespace Barotrauma
LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type;
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
var biome =
LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ??
var biome =
LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ??
LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server);
return new LevelData(
var levelData = new LevelData(
seed,
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server),
Rand.Range(0.0f, 1.0f, Rand.RandSync.Server),
generationParams,
biome);
if (type == LevelType.LocationConnection)
{
float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
levelData.HasBeaconStation = beaconRng < 0.5f;
levelData.IsBeaconActive = beaconRng > 0.25f;
}
GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData);
return levelData;
}
public void Save(XElement parentElement)
@@ -156,7 +198,15 @@ namespace Barotrauma
new XAttribute("type", Type.ToString()),
new XAttribute("difficulty", Difficulty.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("size", XMLExtensions.PointToString(Size)),
new XAttribute("generationparams", GenerationParams.Identifier));
new XAttribute("generationparams", GenerationParams.Identifier),
new XAttribute("initialdepth", InitialDepth));
if (HasBeaconStation)
{
newElement.Add(
new XAttribute("hasbeaconstation", HasBeaconStation.ToString()),
new XAttribute("isbeaconactive", IsBeaconActive.ToString()));
}
if (Type == LevelType.Outpost)
{
@@ -1,5 +1,4 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -10,6 +9,7 @@ namespace Barotrauma
class Biome
{
public readonly string Identifier;
public readonly string OldIdentifier;
public readonly string DisplayName;
public readonly string Description;
@@ -26,6 +26,7 @@ namespace Barotrauma
public Biome(XElement element)
{
Identifier = element.GetAttributeString("identifier", "");
OldIdentifier = element.GetAttributeString("oldidentifier", null);
if (string.IsNullOrEmpty(Identifier))
{
Identifier = element.GetAttributeString("name", "");
@@ -61,6 +62,8 @@ namespace Barotrauma
public readonly string Identifier;
public readonly string OldIdentifier;
private int minWidth, maxWidth, height;
private Point voronoiSiteInterval;
@@ -72,9 +75,7 @@ namespace Barotrauma
//x = min interval, y = max interval
private Point mainPathNodeIntervalRange;
private int smallTunnelCount;
//x = min length, y = max length
private Point smallTunnelLengthRange;
private int caveCount;
//how large portion of the bottom of the level should be "carved out"
//if 0.0f, the bottom will be completely solid (making the abyss unreachable)
@@ -96,6 +97,8 @@ namespace Barotrauma
private float waterParticleScale;
private int initialDepthMin, initialDepthMax;
//which biomes can this type of level appear in
private readonly List<Biome> allowedBiomes = new List<Biome>();
@@ -146,7 +149,7 @@ namespace Barotrauma
}
private Vector2 startPosition;
[Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable]
[Serialize("0,0", true, "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
{
get { return startPosition; }
@@ -159,7 +162,7 @@ namespace Barotrauma
}
private Vector2 endPosition;
[Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable]
[Serialize("1,0", true, "End 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 EndPosition
{
get { return endPosition; }
@@ -185,25 +188,46 @@ namespace Barotrauma
set;
}
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
[Serialize(80, true, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int BackgroundCreatureAmount
{
get;
set;
}
[Serialize(100000, true), Editable]
public int MinWidth
{
get { return minWidth; }
set { minWidth = Math.Max(value, 2000); }
set { minWidth = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
[Serialize(100000, true), Editable]
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = Math.Max(value, 2000); }
set { maxWidth = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(50000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
[Serialize(50000, true), Editable]
public int Height
{
get { return height; }
set { height = Math.Max(value, 2000); }
set { height = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int InitialDepthMin
{
get { return initialDepthMin; }
set { initialDepthMin = Math.Max(value, 0); }
}
[Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
public int InitialDepthMax
{
get { return initialDepthMax; }
set { initialDepthMax = Math.Max(value, initialDepthMin); }
}
[Serialize(6500, true), Editable(MinValueInt = 5000, MaxValueInt = 1000000)]
@@ -213,6 +237,29 @@ namespace Barotrauma
set;
}
[Serialize("0,1", true), Editable]
public Point SideTunnelCount
{
get;
set;
}
[Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float SideTunnelVariance
{
get;
set;
}
[Serialize("2000,6000", true), Editable]
public Point MinSideTunnelRadius
{
get;
set;
}
[Editable, Serialize("3000, 3000", true, description: "How far from each other voronoi sites are placed. " +
"Sites determine shape of the voronoi graph which the level walls are generated from. " +
"(Decreasing this value causes the number of sites, and the complexity of the level, to increase exponentially - be careful when adjusting)")]
@@ -239,7 +286,7 @@ namespace Barotrauma
}
}
[Editable(MinValueInt = 100, MaxValueInt = 10000), Serialize(1000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. "
[Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. "
+ "Can be used in conjunction with the rounding values to make the cells rounder. Smaller values will make the cells look smoother, " +
"but make the level more performance-intensive as the number of polygons used in rendering and physics calculations increases.")]
public int CellSubdivisionLength
@@ -287,23 +334,18 @@ namespace Barotrauma
}
}
[Editable, Serialize(5, true, description: "The number of small tunnels placed along the main path.")]
public int SmallTunnelCount
[Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float MainPathVariance
{
get { return smallTunnelCount; }
set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); }
get;
set;
}
[Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }),
Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")]
public Point SmallTunnelLengthRange
[Editable, Serialize(5, true, description: "The number of caves placed along the main path.")]
public int CaveCount
{
get { return smallTunnelLengthRange; }
set
{
smallTunnelLengthRange.X = MathHelper.Clamp(value.X, 100, MinWidth);
smallTunnelLengthRange.Y = MathHelper.Clamp(value.Y, smallTunnelLengthRange.X, MinWidth);
}
get { return caveCount; }
set { caveCount = MathHelper.Clamp(value, 0, 100); }
}
[Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)]
@@ -313,6 +355,34 @@ namespace Barotrauma
set;
}
[Serialize("19200,38400", true, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)]
public Point ResourceIntervalRange
{
get;
set;
}
[Serialize("9600,19200", true, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)]
public Point CaveResourceIntervalRange
{
get;
set;
}
[Serialize("2,8", true, description: "The minimum and maximum amount of resources in a single cluster. " +
"In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)]
public Point ResourceClusterSizeRange
{
get;
set;
}
[Serialize(0.3f, true, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)]
public float ResourceSpawnChance { get; set; }
[Serialize(1.0f, true, 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, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int FloatingIceChunkCount
{
@@ -320,6 +390,20 @@ namespace Barotrauma
set;
}
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 100)]
public int IslandCount
{
get;
set;
}
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int IceSpireCount
{
get;
set;
}
[Serialize(300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
public int SeaFloorDepth
{
@@ -415,12 +499,41 @@ namespace Barotrauma
private set { waterParticleScale = Math.Max(value, 0.01f); }
}
[Serialize(2048.0f, true, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)]
public float WallTextureSize
{
get;
private set;
}
[Serialize(2048.0f, true), Editable(minValue: 10.0f, maxValue: 10000.0f)]
public float WallEdgeTextureWidth
{
get;
private set;
}
[Serialize(120.0f, true, 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
{
get;
private set;
}
[Serialize(1000.0f, true, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)]
public float WallEdgeExpandInwardsAmount
{
get;
private set;
}
public Sprite BackgroundSprite { get; private set; }
public Sprite BackgroundTopSprite { get; private set; }
public Sprite WallSprite { get; private set; }
public Sprite WallSpriteSpecular { get; private set; }
public Sprite WallEdgeSprite { get; private set; }
public Sprite WallEdgeSpriteSpecular { get; private set; }
public Sprite DestructibleWallSprite { get; private set; }
public Sprite DestructibleWallEdgeSprite { get; private set; }
public Sprite WallSpriteDestroyed { get; private set; }
public Sprite WaterParticles { get; private set; }
public static IEnumerable<Biome> GetBiomes()
@@ -469,6 +582,8 @@ namespace Barotrauma
{
Identifier = element == null ? "default" :
element.GetAttributeString("identifier", null) ?? element.Name.ToString();
OldIdentifier = element?.GetAttributeString("oldidentifier", null)?.ToLowerInvariant();
Identifier = Identifier.ToLowerInvariant();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element == null) { return; }
@@ -486,7 +601,8 @@ namespace Barotrauma
string biomeName = biomeNames[i].Trim().ToLowerInvariant();
if (biomeName == "none") { continue; }
Biome matchingBiome = biomes.Find(b => b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase));
Biome matchingBiome = biomes.Find(b =>
b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase) || (b.OldIdentifier?.Equals(biomeName, StringComparison.OrdinalIgnoreCase) ?? false));
if (matchingBiome == null)
{
matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase));
@@ -518,14 +634,17 @@ namespace Barotrauma
case "wall":
WallSprite = new Sprite(subElement);
break;
case "wallspecular":
WallSpriteSpecular = new Sprite(subElement);
break;
case "walledge":
WallEdgeSprite = new Sprite(subElement);
break;
case "walledgespecular":
WallEdgeSpriteSpecular = new Sprite(subElement);
case "destructiblewall":
DestructibleWallSprite = new Sprite(subElement);
break;
case "destructiblewalledge":
DestructibleWallEdgeSprite = new Sprite(subElement);
break;
case "walldestroyed":
WallSpriteDestroyed = new Sprite(subElement);
break;
case "waterparticles":
WaterParticles = new Sprite(subElement);
@@ -546,8 +665,7 @@ namespace Barotrauma
}
List<XElement> biomeElements = new List<XElement>();
List<XElement> levelParamElements = new List<XElement>();
Dictionary<string, XElement> levelParamElements = new Dictionary<string, XElement>();
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
@@ -557,18 +675,18 @@ namespace Barotrauma
{
mainElement = doc.Root.FirstElement();
biomeElements.Clear();
levelParamElements.Clear();
DebugConsole.NewMessage($"Overriding the level generation parameters and biomes with '{file.Path}'", Color.Yellow);
DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow);
}
else if (biomeElements.Any() || levelParamElements.Any())
else if (biomeElements.Any() && mainElement.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use <override></override> tags to override it.");
DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use <override></override> tags to override the biomes.");
break;
}
foreach (XElement element in mainElement.Elements())
{
if (element.IsOverride())
bool isOverride = element.IsOverride();
if (isOverride)
{
if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
@@ -578,18 +696,31 @@ namespace Barotrauma
}
else
{
levelParamElements.Clear();
DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow);
levelParamElements.AddRange(element.Elements());
string identifier = element.FirstElement().GetAttributeString("identifier", null) ?? element.GetAttributeString("name", "");
if (levelParamElements.ContainsKey(identifier))
{
DebugConsole.NewMessage($"Overriding the level generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow);
levelParamElements.Remove(identifier);
}
levelParamElements.Add(identifier, element.FirstElement());
}
}
}
else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
{
biomeElements.AddRange(element.Elements());
}
else
{
levelParamElements.Add(element);
string identifier = element.GetAttributeString("identifier", null) ?? element.GetAttributeString("name", "");
if (levelParamElements.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Duplicate level generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use <override></override> tags to override the generation parameters.");
continue;
}
else
{
levelParamElements.Add(identifier, element);
}
}
}
}
@@ -599,7 +730,7 @@ namespace Barotrauma
biomes.Add(new Biome(biomeElement));
}
foreach (XElement levelParamElement in levelParamElements)
foreach (XElement levelParamElement in levelParamElements.Values)
{
LevelParams.Add(new LevelGenerationParams(levelParamElement));
}
@@ -37,18 +37,23 @@ namespace Barotrauma
public bool NeedsNetworkSyncing
{
get { return Triggers.Any(t => t.NeedsNetworkSyncing); }
set { Triggers.ForEach(t => t.NeedsNetworkSyncing = false); }
get { return Triggers != null && Triggers.Any(t => t.NeedsNetworkSyncing); }
set
{
if (Triggers == null) { return; }
Triggers.ForEach(t => t.NeedsNetworkSyncing = false);
}
}
public bool NeedsUpdate
{
get; private set;
}
public Sprite Sprite
{
get { return spriteIndex < 0 || Prefab.Sprites.Count == 0 ? null : Prefab.Sprites[spriteIndex % Prefab.Sprites.Count]; }
}
public Sprite SpecularSprite
{
get { return spriteIndex < 0 || Prefab.SpecularSprites.Count == 0 ? null : Prefab.SpecularSprites[spriteIndex % Prefab.SpecularSprites.Count]; }
}
Vector2 ISpatialEntity.Position => new Vector2(Position.X, Position.Y);
@@ -60,8 +65,6 @@ namespace Barotrauma
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
{
Triggers = new List<LevelTrigger>();
ActivePrefab = Prefab = prefab;
Position = position;
Scale = scale;
@@ -69,13 +72,26 @@ namespace Barotrauma
spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1;
if (prefab.PhysicsBodyElement != null)
if (Sprite != null && prefab.SpriteSpecificPhysicsBodyElements.ContainsKey(Sprite))
{
PhysicsBody = new PhysicsBody(prefab.SpriteSpecificPhysicsBodyElements[Sprite], ConvertUnits.ToSimUnits(new Vector2(position.X, position.Y)), Scale);
}
else if (prefab.PhysicsBodyElement != null)
{
PhysicsBody = new PhysicsBody(prefab.PhysicsBodyElement, ConvertUnits.ToSimUnits(new Vector2(position.X, position.Y)), Scale);
}
if (PhysicsBody != null)
{
PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition, -Rotation);
PhysicsBody.BodyType = BodyType.Static;
PhysicsBody.CollisionCategories = Physics.CollisionLevel;
PhysicsBody.CollidesWith = Physics.CollisionWall | Physics.CollisionCharacter;
}
foreach (XElement triggerElement in prefab.LevelTriggerElements)
{
Triggers ??= new List<LevelTrigger>();
Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale;
if (rotation != 0.0f)
@@ -90,10 +106,12 @@ namespace Barotrauma
var newTrigger = new LevelTrigger(triggerElement, new Vector2(position.X, position.Y) + triggerPosition, -rotation, scale, prefab.Name);
int parentTriggerIndex = prefab.LevelTriggerElements.IndexOf(triggerElement.Parent);
if (parentTriggerIndex > -1) newTrigger.ParentTrigger = Triggers[parentTriggerIndex];
if (parentTriggerIndex > -1) { newTrigger.ParentTrigger = Triggers[parentTriggerIndex]; }
Triggers.Add(newTrigger);
}
NeedsUpdate = NeedsNetworkSyncing || (Triggers != null && Triggers.Any()) || Prefab.PhysicsBodyTriggerIndex > -1;
InitProjSpecific();
}
@@ -131,9 +149,10 @@ namespace Barotrauma
public void ServerWrite(IWriteMessage msg, Client c)
{
if (Triggers == null) { return; }
for (int j = 0; j < Triggers.Count; j++)
{
if (!Triggers[j].UseNetworkSyncing) continue;
if (!Triggers[j].UseNetworkSyncing) { continue; }
Triggers[j].ServerWrite(msg, c);
}
}
@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -17,9 +18,10 @@ namespace Barotrauma
const int GridSize = 2000;
private List<LevelObject> objects;
private List<LevelObject> updateableObjects;
private List<LevelObject>[,] objectGrid;
public LevelObjectManager() : base(null)
public LevelObjectManager() : base(null, Entity.NullEntityID)
{
}
@@ -27,20 +29,36 @@ namespace Barotrauma
{
public readonly GraphEdge GraphEdge;
public readonly Vector2 Normal;
public readonly LevelObjectPrefab.SpawnPosType SpawnPosType;
public readonly List<LevelObjectPrefab.SpawnPosType> SpawnPosTypes = new List<LevelObjectPrefab.SpawnPosType>();
public readonly Alignment Alignment;
public readonly float Length;
private readonly float noiseVal;
public SpawnPosition(GraphEdge graphEdge, Vector2 normal, LevelObjectPrefab.SpawnPosType spawnPosType, Alignment alignment)
: this(graphEdge, normal, spawnPosType.ToEnumerable(), alignment)
{ }
public SpawnPosition(GraphEdge graphEdge, Vector2 normal, IEnumerable<LevelObjectPrefab.SpawnPosType> spawnPosTypes, Alignment alignment)
{
GraphEdge = graphEdge;
Normal = normal;
SpawnPosType = spawnPosType;
Alignment = alignment;
Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2);
Normal = normal.NearlyEquals(Vector2.Zero) ? Vector2.UnitY : Vector2.Normalize(normal);
SpawnPosTypes.AddRange(spawnPosTypes);
if (spawnPosTypes.Contains(LevelObjectPrefab.SpawnPosType.MainPath) ||
spawnPosTypes.Contains(LevelObjectPrefab.SpawnPosType.LevelStart) ||
spawnPosTypes.Contains(LevelObjectPrefab.SpawnPosType.LevelEnd))
{
Length = 1000.0f;
Normal = Vector2.Zero;
Alignment = Alignment.Any;
}
else
{
Alignment = alignment;
Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2);
}
noiseVal =
(float)(PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 10000.0f, GraphEdge.Point1.Y / 10000.0f, 0.5f) +
@@ -83,7 +101,7 @@ namespace Barotrauma
foreach (var posOfInterest in level.PositionsOfInterest)
{
if (posOfInterest.PositionType != Level.PositionType.MainPath) continue;
if (posOfInterest.PositionType != Level.PositionType.MainPath && posOfInterest.PositionType != Level.PositionType.SidePath) { continue; }
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(posOfInterest.Position.ToVector2(), posOfInterest.Position.ToVector2() + Vector2.UnitX),
@@ -101,49 +119,29 @@ namespace Barotrauma
var availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List);
objects = new List<LevelObject>();
updateableObjects = new List<LevelObject>();
Dictionary<LevelObjectPrefab, List<SpawnPosition>> suitableSpawnPositions = new Dictionary<LevelObjectPrefab, List<SpawnPosition>>();
Dictionary<LevelObjectPrefab, List<float>> spawnPositionWeights = new Dictionary<LevelObjectPrefab, List<float>>();
for (int i = 0; i < amount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams.Identifier, availablePrefabs);
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams, availablePrefabs);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
suitableSpawnPositions.Add(prefab, availableSpawnPositions.Where(sp =>
prefab.SpawnPos.HasFlag(sp.SpawnPosType) && (sp.Length >= prefab.MinSurfaceWidth && prefab.Alignment.HasFlag(sp.Alignment) || sp.SpawnPosType == LevelObjectPrefab.SpawnPosType.LevelEnd || sp.SpawnPosType == LevelObjectPrefab.SpawnPosType.LevelStart)).ToList());
suitableSpawnPositions.Add(prefab,
availableSpawnPositions.Where(sp =>
sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) &&
sp.Length >= prefab.MinSurfaceWidth &&
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
float rotation = 0.0f;
if (prefab.AlignWithSurface && spawnPosition != null)
{
rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X));
}
rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server);
Vector2 position = Vector2.Zero;
Vector2 edgeDir = Vector2.UnitX;
if (spawnPosition == null)
{
position = new Vector2(
Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server),
Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server));
}
else
{
edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length;
position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server);
}
var newObject = new LevelObject(prefab,
new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation);
AddObject(newObject, level);
PlaceObject(prefab, spawnPosition, level);
if (prefab.MaxCount < amount)
{
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
@@ -151,38 +149,119 @@ namespace Barotrauma
availablePrefabs.Remove(prefab);
}
}
}
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
foreach (Level.Cave cave in level.Caves)
{
availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall)));
availableSpawnPositions.Clear();
suitableSpawnPositions.Clear();
spawnPositionWeights.Clear();
var caveCells = cave.Tunnels.SelectMany(t => t.Cells);
List<VoronoiCell> caveWallCells = new List<VoronoiCell>();
foreach (var edge in caveCells.SelectMany(c => c.Edges))
{
int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server);
for (int j = 0; j < childCount; j++)
{
var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name));
int prefabCount = matchingPrefabs.Count();
var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server));
if (childPrefab == null) continue;
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth;
var childObject = new LevelObject(childPrefab,
new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)),
Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server),
rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server));
AddObject(childObject, level);
}
if (!edge.NextToCave) { continue; }
if (edge.Cell1?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell1); }
if (edge.Cell2?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell2); }
}
}
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(caveWallCells.Distinct(), LevelObjectPrefab.SpawnPosType.CaveWall));
for (int i = 0; i < cave.CaveGenerationParams.LevelObjectAmount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
suitableSpawnPositions.Add(prefab,
availableSpawnPositions.Where(sp =>
sp.Length >= prefab.MinSurfaceWidth &&
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
if (prefab.MaxCount < amount)
{
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
{
availablePrefabs.Remove(prefab);
}
}
}
}
}
private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level)
{
float rotation = 0.0f;
if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null)
{
rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X));
}
rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server);
Vector2 position = Vector2.Zero;
Vector2 edgeDir = Vector2.UnitX;
if (spawnPosition == null)
{
position = new Vector2(
Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server),
Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server));
}
else
{
edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length;
position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server);
}
if (!MathUtils.NearlyEqual(prefab.RandomOffset.X, 0.0f) || !MathUtils.NearlyEqual(prefab.RandomOffset.Y, 0.0f))
{
Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.Server);
position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.Server);
}
var newObject = new LevelObject(prefab,
new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation);
AddObject(newObject, level);
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
{
int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server);
for (int j = 0; j < childCount; j++)
{
var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name));
int prefabCount = matchingPrefabs.Count();
var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server));
if (childPrefab == null) continue;
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth;
var childObject = new LevelObject(childPrefab,
new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)),
Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server),
rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server));
AddObject(childObject, level);
}
}
}
private void AddObject(LevelObject newObject, Level level)
{
foreach (LevelTrigger trigger in newObject.Triggers)
{
trigger.OnTriggered += (levelTrigger, obj) =>
if (newObject.Triggers != null)
{
foreach (LevelTrigger trigger in newObject.Triggers)
{
OnObjectTriggered(newObject, levelTrigger, obj);
};
trigger.OnTriggered += (levelTrigger, obj) =>
{
OnObjectTriggered(newObject, levelTrigger, obj);
};
}
}
var spriteCorners = new List<Vector2>
@@ -223,22 +302,24 @@ namespace Barotrauma
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos;
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos;
foreach (LevelTrigger trigger in newObject.Triggers)
if (newObject.Triggers != null)
{
if (trigger.PhysicsBody == null) continue;
for (int i = 0; i < trigger.PhysicsBody.FarseerBody.FixtureList.Count; i++)
foreach (LevelTrigger trigger in newObject.Triggers)
{
trigger.PhysicsBody.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform);
trigger.PhysicsBody.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i);
if (trigger.PhysicsBody == null) { continue; }
for (int i = 0; i < trigger.PhysicsBody.FarseerBody.FixtureList.Count; i++)
{
trigger.PhysicsBody.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform);
trigger.PhysicsBody.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i);
minX = Math.Min(minX, ConvertUnits.ToDisplayUnits(aabb.LowerBound.X));
maxX = Math.Max(maxX, ConvertUnits.ToDisplayUnits(aabb.UpperBound.X));
minY = Math.Min(minY, ConvertUnits.ToDisplayUnits(aabb.LowerBound.Y) - level.BottomPos);
maxY = Math.Max(maxY, ConvertUnits.ToDisplayUnits(aabb.UpperBound.Y) - level.BottomPos);
minX = Math.Min(minX, ConvertUnits.ToDisplayUnits(aabb.LowerBound.X));
maxX = Math.Max(maxX, ConvertUnits.ToDisplayUnits(aabb.UpperBound.X));
minY = Math.Min(minY, ConvertUnits.ToDisplayUnits(aabb.LowerBound.Y) - level.BottomPos);
maxY = Math.Max(maxY, ConvertUnits.ToDisplayUnits(aabb.UpperBound.Y) - level.BottomPos);
}
}
}
#if CLIENT
if (newObject.ParticleEmitters != null)
{
@@ -253,6 +334,7 @@ namespace Barotrauma
}
#endif
objects.Add(newObject);
if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); }
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
int xStart = (int)Math.Floor(minX / GridSize);
@@ -290,7 +372,7 @@ namespace Barotrauma
return objects;
}
private readonly static List<LevelObject> objectsInRange = new List<LevelObject>();
private readonly static HashSet<LevelObject> objectsInRange = new HashSet<LevelObject>();
public IEnumerable<LevelObject> GetAllObjects(Vector2 worldPosition, float radius)
{
var minIndices = GetGridIndices(worldPosition - Vector2.One * radius);
@@ -309,10 +391,10 @@ namespace Barotrauma
{
for (int y = minIndices.Y; y <= maxIndices.Y; y++)
{
if (objectGrid[x, y] == null) continue;
if (objectGrid[x, y] == null) { continue; }
foreach (LevelObject obj in objectGrid[x, y])
{
if (!objectsInRange.Contains(obj)) objectsInRange.Add(obj);
objectsInRange.Add(obj);
}
}
}
@@ -322,12 +404,14 @@ namespace Barotrauma
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType)
{
List<LevelObjectPrefab.SpawnPosType> spawnPosTypes = new List<LevelObjectPrefab.SpawnPosType>(4);
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
foreach (var cell in cells)
{
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid || edge.OutsideLevel) continue;
if (!edge.IsSolid || edge.OutsideLevel) { continue; }
if (spawnPosType != LevelObjectPrefab.SpawnPosType.CaveWall && edge.NextToCave) { continue; }
Vector2 normal = edge.GetNormal(cell);
Alignment edgeAlignment = 0;
@@ -340,7 +424,13 @@ namespace Barotrauma
else if(normal.X > 0.5f)
edgeAlignment |= Alignment.Right;
availableSpawnPositions.Add(new SpawnPosition(edge, normal, spawnPosType, edgeAlignment));
spawnPosTypes.Clear();
spawnPosTypes.Add(spawnPosType);
if (spawnPosType.HasFlag(LevelObjectPrefab.SpawnPosType.MainPathWall) && edge.NextToMainPath) { spawnPosTypes.Add(LevelObjectPrefab.SpawnPosType.MainPathWall); }
if (spawnPosType.HasFlag(LevelObjectPrefab.SpawnPosType.SidePathWall) && edge.NextToSidePath) { spawnPosTypes.Add(LevelObjectPrefab.SpawnPosType.SidePathWall); }
if (spawnPosType.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall) && edge.NextToCave) { spawnPosTypes.Add(LevelObjectPrefab.SpawnPosType.CaveWall); }
availableSpawnPositions.Add(new SpawnPosition(edge, normal, spawnPosTypes, edgeAlignment));
}
}
return availableSpawnPositions;
@@ -348,7 +438,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
foreach (LevelObject obj in objects)
foreach (LevelObject obj in updateableObjects)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
@@ -361,21 +451,24 @@ namespace Barotrauma
}
}
obj.ActivePrefab = obj.Prefab;
for (int i = 0; i < obj.Triggers.Count; i++)
if (obj.Triggers != null)
{
obj.Triggers[i].Update(deltaTime);
if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null)
obj.ActivePrefab = obj.Prefab;
for (int i = 0; i < obj.Triggers.Count; i++)
{
obj.ActivePrefab = obj.Prefab.OverrideProperties[i];
obj.Triggers[i].Update(deltaTime);
if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null)
{
obj.ActivePrefab = obj.Prefab.OverrideProperties[i];
}
}
}
if (obj.PhysicsBody != null)
{
if (obj.Prefab.PhysicsBodyTriggerIndex > -1) obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered;
obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z);
obj.Rotation = obj.PhysicsBody.Rotation;
if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; }
/*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z);
obj.Rotation = -obj.PhysicsBody.Rotation;*/
}
}
@@ -386,10 +479,10 @@ namespace Barotrauma
private void OnObjectTriggered(LevelObject triggeredObject, LevelTrigger trigger, Entity triggerer)
{
if (trigger.TriggerOthersDistance <= 0.0f) return;
if (trigger.TriggerOthersDistance <= 0.0f) { return; }
foreach (LevelObject obj in objects)
{
if (obj == triggeredObject) continue;
if (obj == triggeredObject || obj.Triggers == null) { continue; }
foreach (LevelTrigger otherTrigger in obj.Triggers)
{
otherTrigger.OtherTriggered(triggeredObject, trigger);
@@ -397,16 +490,20 @@ namespace Barotrauma
}
}
private LevelObjectPrefab GetRandomPrefab(string levelType)
{
return GetRandomPrefab(levelType, LevelObjectPrefab.List);
}
private LevelObjectPrefab GetRandomPrefab(string levelType, IList<LevelObjectPrefab> availablePrefabs)
private LevelObjectPrefab GetRandomPrefab(LevelGenerationParams generationParams, IList<LevelObjectPrefab> availablePrefabs)
{
if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(levelType)).ToList(), Rand.RandSync.Server);
availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server);
}
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs)
{
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(caveParams)).ToList(), Rand.RandSync.Server);
}
public override void Remove()
@@ -418,6 +515,7 @@ namespace Barotrauma
obj.Remove();
}
objects.Clear();
updateableObjects.Clear();
}
RemoveProjSpecific();
@@ -8,11 +8,7 @@ namespace Barotrauma
{
partial class LevelObjectPrefab : ISerializableEntity
{
private static List<LevelObjectPrefab> list = new List<LevelObjectPrefab>();
public static List<LevelObjectPrefab> List
{
get { return list; }
}
public static List<LevelObjectPrefab> List { get; } = new List<LevelObjectPrefab>();
public class ChildObject
{
@@ -38,12 +34,15 @@ namespace Barotrauma
public enum SpawnPosType
{
None = 0,
Wall = 1,
RuinWall = 2,
SeaFloor = 4,
MainPath = 8,
LevelStart = 16,
LevelEnd = 32,
MainPathWall = 1,
SidePathWall = 2,
CaveWall = 4,
RuinWall = 8,
SeaFloor = 16,
MainPath = 32,
LevelStart = 64,
LevelEnd = 128,
Wall = MainPathWall | SidePathWall | CaveWall,
}
public List<Sprite> Sprites
@@ -52,12 +51,6 @@ namespace Barotrauma
private set;
} = new List<Sprite>();
public List<Sprite> SpecularSprites
{
get;
private set;
} = new List<Sprite>();
public DeformableSprite DeformableSprite
{
get;
@@ -117,7 +110,13 @@ namespace Barotrauma
{
get;
private set;
}
} = -1;
public Dictionary<Sprite, XElement> SpriteSpecificPhysicsBodyElements
{
get;
private set;
} = new Dictionary<Sprite, XElement>();
[Serialize(10000, false, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
@@ -162,6 +161,13 @@ namespace Barotrauma
private set;
}
[Editable, Serialize("0,0", true, description: "Random offset from the surface the object spawns on.")]
public Vector2 RandomOffset
{
get;
private set;
}
[Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")]
public bool AlignWithSurface
{
@@ -243,12 +249,18 @@ namespace Barotrauma
private set;
}
public string Name
public string Identifier
{
get;
set;
}
public string Name
{
get { return Identifier; }
}
public List<ChildObject> ChildObjects
{
get;
@@ -272,12 +284,12 @@ namespace Barotrauma
public override string ToString()
{
return "LevelObjectPrefab (" + Name + ")";
return "LevelObjectPrefab (" + Identifier + ")";
}
public static void LoadAll()
{
list.Clear();
List.Clear();
var files = GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs);
if (files.Count() > 0)
{
@@ -303,35 +315,62 @@ namespace Barotrauma
{
mainElement = doc.Root.FirstElement();
DebugConsole.NewMessage($"Overriding all level object prefabs with '{configPath}'", Color.Yellow);
list.Clear();
List.Clear();
}
else if (list.Any())
else if (List.Any())
{
DebugConsole.NewMessage($"Loading additional level object prefabs from file '{configPath}'");
DebugConsole.Log($"Loading additional level object prefabs from file '{configPath}'");
}
foreach (XElement element in mainElement.Elements())
foreach (XElement subElement in mainElement.Elements())
{
list.Add(new LevelObjectPrefab(element));
var element = subElement.IsOverride() ? subElement.FirstElement() : subElement;
string identifier = element.GetAttributeString("identifier", "");
var existingPrefab = List.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (existingPrefab != null)
{
if (subElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing level object prefab '{identifier}' using the file '{configPath}'", Color.Yellow);
List.Remove(existingPrefab);
}
else
{
DebugConsole.ThrowError($"Error in '{configPath}': Duplicate level object prefab '{identifier}' found in '{configPath}'! Each level object prefab must have a unique identifier. " +
"Use <override></override> tags to override prefabs.");
continue;
}
}
List.Add(new LevelObjectPrefab(element));
}
}
catch (Exception e)
{
DebugConsole.ThrowError(String.Format("Failed to load LevelObject prefabs from {0}", configPath), e);
DebugConsole.ThrowError(string.Format("Failed to load LevelObject prefabs from {0}", configPath), e);
}
}
public LevelObjectPrefab(XElement element)
public LevelObjectPrefab(XElement element, string identifier = null)
{
ChildObjects = new List<ChildObject>();
LevelTriggerElements = new List<XElement>();
OverrideProperties = new List<LevelObjectPrefab>();
OverrideCommonness = new Dictionary<string, float>();
Identifier = null;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element != null)
{
Config = element;
Name = element.Name.ToString();
Identifier = element.GetAttributeString("identifier", null) ?? identifier;
if (string.IsNullOrEmpty(Identifier))
{
#if DEBUG
DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead...");
#else
DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead...");
#endif
Identifier = element.Name.ToString();
}
LoadElements(element, -1);
InitProjSpecific(element);
}
@@ -346,21 +385,27 @@ namespace Barotrauma
private void LoadElements(XElement element, int parentTriggerIndex)
{
int propertyOverrideCount = 0;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
Sprites.Add( new Sprite(subElement, lazyLoad: true));
break;
case "specularsprite":
SpecularSprites.Add(new Sprite(subElement, lazyLoad: true));
var newSprite = new Sprite(subElement, lazyLoad: true);
Sprites.Add(newSprite);
var spriteSpecificPhysicsBodyElement =
subElement.Element("PhysicsBody") ?? subElement.Element("Body") ??
subElement.Element("physicsbody") ?? subElement.Element("body");
if (spriteSpecificPhysicsBodyElement != null)
{
SpriteSpecificPhysicsBodyElements.Add(newSprite, spriteSpecificPhysicsBodyElement);
}
break;
case "deformablesprite":
DeformableSprite = new DeformableSprite(subElement, lazyLoad: true);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "");
string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant();
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
@@ -376,13 +421,14 @@ namespace Barotrauma
ChildObjects.Add(new ChildObject(subElement));
break;
case "overrideproperties":
var propertyOverride = new LevelObjectPrefab(subElement);
var propertyOverride = new LevelObjectPrefab(subElement, identifier: Identifier + "-" + propertyOverrideCount);
OverrideProperties[OverrideProperties.Count - 1] = propertyOverride;
if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null)
{
propertyOverride.Sprites = Sprites;
propertyOverride.DeformableSprite = DeformableSprite;
}
propertyOverrideCount++;
break;
case "body":
case "physicsbody":
@@ -395,13 +441,26 @@ namespace Barotrauma
partial void InitProjSpecific(XElement element);
public float GetCommonness(string levelType)
public float GetCommonness(CaveGenerationParams generationParams)
{
if (!OverrideCommonness.TryGetValue(levelType, out float commonness))
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
{
return Commonness;
return commonness;
}
return commonness;
return 0.0f;
}
public float GetCommonness(LevelGenerationParams generationParams)
{
if (generationParams?.Identifier != null &&
(OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) ||
(generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness))))
{
return commonness;
}
return Commonness;
}
}
}
@@ -186,6 +186,18 @@ namespace Barotrauma
get;
set;
}
public string InfectIdentifier
{
get;
set;
}
public float InfectionChance
{
get;
set;
}
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
{
@@ -211,6 +223,9 @@ namespace Barotrauma
}
cameraShake = element.GetAttributeFloat("camerashake", 0.0f);
InfectIdentifier = element.GetAttributeString("infectidentifier", null);
InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f);
stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f);
randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f);
@@ -513,9 +528,14 @@ namespace Barotrauma
float structureDamage = attack.GetStructureDamage(deltaTime);
if (structureDamage > 0.0f)
{
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage);
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f);
}
}
if (!string.IsNullOrWhiteSpace(InfectIdentifier))
{
submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance);
}
}
if (Force.LengthSquared() > 0.01f)
@@ -598,7 +618,7 @@ namespace Barotrauma
public Vector2 GetWaterFlowVelocity()
{
if (Force == Vector2.Zero) return Vector2.Zero;
if (Force == Vector2.Zero || ForceMode == TriggerForceMode.LimitVelocity) { return Vector2.Zero; }
Vector2 vel = Force;
if (ForceMode == TriggerForceMode.Acceleration)
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Voronoi2;
using System.Linq;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
@@ -11,18 +12,15 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class LevelWall : IDisposable
{
private List<VoronoiCell> cells;
public List<VoronoiCell> Cells
{
get { return cells; }
}
{
public List<VoronoiCell> Cells { get; private set; }
private Body body;
public Body Body
{
get { return body; }
}
public Body Body { get; private set; }
protected readonly Level level;
private readonly List<Vector2[]> triangles;
private readonly Color color;
private float moveState;
private float moveLength;
@@ -38,6 +36,17 @@ namespace Barotrauma
}
}
private float wallDamageOnTouch;
public float WallDamageOnTouch
{
get { return wallDamageOnTouch; }
set
{
Cells.ForEach(c => c.DoesDamage = !MathUtils.NearlyEqual(value, 0.0f));
wallDamageOnTouch = value;
}
}
public float MoveSpeed;
private Vector2? originalPos;
@@ -50,30 +59,32 @@ namespace Barotrauma
public LevelWall(List<Vector2> vertices, Color color, Level level, bool giftWrap = false)
{
if (giftWrap)
this.level = level;
this.color = color;
List<Vector2> originalVertices = new List<Vector2>(vertices);
if (giftWrap) { vertices = MathUtils.GiftWrap(vertices); }
if (vertices.Count < 3)
{
vertices = MathUtils.GiftWrap(vertices);
throw new ArgumentException("Failed to generate a wall (not enough vertices). Original vertices: " + string.Join(", ", originalVertices.Select(v => v.ToString())));
}
VoronoiCell wallCell = new VoronoiCell(vertices.ToArray());
for (int i = 0; i < wallCell.Edges.Count; i++)
{
wallCell.Edges[i].Cell1 = wallCell;
wallCell.Edges[i].IsSolid = true;
}
cells = new List<VoronoiCell>() { wallCell };
body = CaveGenerator.GeneratePolygons(cells, level, out List<Vector2[]> triangles);
Cells = new List<VoronoiCell>() { wallCell };
Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles);
#if CLIENT
List<VertexPositionTexture> bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles);
SetBodyVertices(bodyVertices.ToArray(), color);
SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color);
GenerateVertices();
#endif
}
public LevelWall(List<Vector2> edgePositions, Vector2 extendAmount, Color color, Level level)
{
cells = new List<VoronoiCell>();
this.level = level;
this.color = color;
Cells = new List<VoronoiCell>();
for (int i = 0; i < edgePositions.Count - 1; i++)
{
Vector2[] vertices = new Vector2[4];
@@ -84,7 +95,7 @@ namespace Barotrauma
VoronoiCell wallCell = new VoronoiCell(vertices)
{
CellType = CellType.Edge
CellType = CellType.Solid
};
wallCell.Edges[0].Cell1 = wallCell;
wallCell.Edges[1].Cell1 = wallCell;
@@ -94,31 +105,28 @@ namespace Barotrauma
if (i > 1)
{
wallCell.Edges[3].Cell2 = cells[i - 1];
cells[i - 1].Edges[1].Cell2 = wallCell;
wallCell.Edges[3].Cell2 = Cells[i - 1];
Cells[i - 1].Edges[1].Cell2 = wallCell;
}
cells.Add(wallCell);
Cells.Add(wallCell);
}
body = CaveGenerator.GeneratePolygons(cells, level, out List<Vector2[]> triangles);
body.CollisionCategories = Physics.CollisionLevel;
Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles);
Body.CollisionCategories = Physics.CollisionLevel;
#if CLIENT
List<VertexPositionTexture> bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles);
SetBodyVertices(bodyVertices.ToArray(), color);
SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color);
GenerateVertices();
#endif
}
public void Update(float deltaTime)
public virtual void Update(float deltaTime)
{
if (body.BodyType == BodyType.Static) return;
if (Body.BodyType == BodyType.Static) { return; }
Vector2 bodyPos = ConvertUnits.ToDisplayUnits(body.Position);
Vector2 bodyPos = ConvertUnits.ToDisplayUnits(Body.Position);
Cells.ForEach(c => c.Translation = bodyPos);
if (!originalPos.HasValue) originalPos = bodyPos;
if (!originalPos.HasValue) { originalPos = bodyPos; }
if (moveLength > 0.0f && MoveSpeed > 0.0f)
{
@@ -126,10 +134,15 @@ namespace Barotrauma
moveState %= MathHelper.TwoPi;
Vector2 targetPos = ConvertUnits.ToSimUnits(originalPos.Value + moveAmount * (float)Math.Sin(moveState));
body.ApplyForce((targetPos - body.Position).ClampLength(1.0f) * body.Mass);
Body.ApplyForce((targetPos - Body.Position).ClampLength(1.0f) * Body.Mass);
}
}
public bool IsPointInside(Vector2 point)
{
return Cells.Any(c => c.IsPointInside(point));
}
public void Dispose()
{
Dispose(true);
@@ -139,16 +152,8 @@ namespace Barotrauma
protected virtual void Dispose(bool disposing)
{
#if CLIENT
if (wallVertices != null)
{
wallVertices.Dispose();
wallVertices = null;
}
if (bodyVertices != null)
{
BodyVertices.Dispose();
bodyVertices = null;
}
VertexBuffer?.Dispose();
VertexBuffer = null;
#endif
}
}
@@ -31,7 +31,7 @@ namespace Barotrauma.RuinGeneration
private string filePath;
private List<RuinRoom> roomTypeList;
private readonly List<RuinRoom> roomTypeList;
public string Name => "RuinGenerationParams";
@@ -71,8 +71,8 @@ namespace Barotrauma
}
}
public LinkedSubmarine(Submarine submarine)
: base(null, submarine)
public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID)
: base(null, submarine, id)
{
linkedToID = new List<ushort>();
@@ -102,9 +102,9 @@ namespace Barotrauma
return sl;
}
public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position)
public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position, ushort id = Entity.NullEntityID)
{
LinkedSubmarine sl = new LinkedSubmarine(mainSub);
LinkedSubmarine sl = new LinkedSubmarine(mainSub, id);
sl.GenerateWallVertices(element);
if (sl.wallVertices.Any())
{
@@ -132,15 +132,7 @@ namespace Barotrauma
public override MapEntity Clone()
{
var path = filePath;
if (string.IsNullOrEmpty(path))
{
var linkedSubmarine = CreateDummy(Submarine, saveElement, Position);
linkedSubmarine.saveElement = saveElement;
return linkedSubmarine;
}
return CreateDummy(Submarine, path, Position);
return CreateDummy(Submarine, filePath, Position);
}
private void GenerateWallVertices(XElement rootElement)
@@ -171,13 +163,13 @@ namespace Barotrauma
}
// LinkedSubmarine.Load() is called from MapEntity.LoadAll()
public static LinkedSubmarine Load(XElement element, Submarine submarine)
public static LinkedSubmarine Load(XElement element, Submarine submarine, IdRemap idRemap)
{
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
LinkedSubmarine linkedSub;
if (Screen.Selected == GameMain.SubEditorScreen)
{
linkedSub = CreateDummy(submarine, element, pos);
linkedSub = CreateDummy(submarine, element, pos, idRemap.AssignMaxId());
linkedSub.saveElement = element;
linkedSub.purchasedLostShuttles = false;
}
@@ -185,7 +177,7 @@ namespace Barotrauma
{
string levelSeed = element.GetAttributeString("location", "");
LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.LevelData;
linkedSub = new LinkedSubmarine(submarine)
linkedSub = new LinkedSubmarine(submarine, idRemap.AssignMaxId())
{
purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles,
saveElement = element
@@ -207,9 +199,9 @@ namespace Barotrauma
int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]);
for (int i = 0; i < linkedToIds.Length; i++)
{
linkedSub.linkedToID.Add((ushort)linkedToIds[i]);
linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i]));
}
linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0);
linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0));
linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0);
return linkedSub.loadSub ? linkedSub : null;
@@ -238,8 +230,11 @@ namespace Barotrauma
return;
}
sub = Submarine.Load(info, false);
IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset);
sub = Submarine.Load(info, false, parentRemap);
IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset);
Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero);
if (worldPos != Vector2.Zero)
{
@@ -273,7 +268,8 @@ namespace Barotrauma
}
originalLinkedPort = linkedPort;
myPort = (FindEntityByID(originalMyPortID) as Item)?.GetComponent<DockingPort>();
ushort originalMyId = childRemap.GetOffsetId(originalMyPortID);
myPort = (FindEntityByID(originalMyId) as Item)?.GetComponent<DockingPort>();
if (myPort == null)
{
float closestDistance = 0.0f;
@@ -33,7 +33,7 @@ namespace Barotrauma
{
OriginalContainerID = item.OriginalContainerID;
}
OriginalID = item.OriginalID;
OriginalID = item.ID;
ModuleIndex = (ushort)item.OriginalModuleIndex;
Identifier = item.prefab.Identifier;
}
@@ -50,7 +50,7 @@ namespace Barotrauma
}
else
{
return item.OriginalID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier;
return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier;
}
}
}
@@ -76,17 +76,6 @@ namespace Barotrauma
public LevelData LevelData { get; set; }
private float normalizedDepth;
public float NormalizedDepth
{
get { return normalizedDepth; }
set
{
if (!MathUtils.IsValid(value)) { return; }
normalizedDepth = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
public int PortraitId { get; private set; }
public Reputation Reputation { get; set; }
@@ -200,7 +189,6 @@ namespace Barotrauma
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
NormalizedDepth = element.GetAttributeFloat("normalizeddepth", 0.0f);
TypeChangeTimer = element.GetAttributeInt("changetimer", 0);
Discovered = element.GetAttributeBool("discovered", false);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
@@ -285,14 +273,15 @@ namespace Barotrauma
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
Type = newType;
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
CreateStore(force: true);
}
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
availableMissions.Add(InstantiateMission(missionPrefab, connection));
var mission = InstantiateMission(missionPrefab, connection);
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#endif
@@ -309,7 +298,7 @@ namespace Barotrauma
}
else
{
var mission = InstantiateMission(missionPrefab);
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])))
{
@@ -342,7 +331,7 @@ namespace Barotrauma
suitableMissions = unusedMissions;
}
MissionPrefab missionPrefab = suitableMissions.GetRandom();
var mission = InstantiateMission(missionPrefab);
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])))
{
@@ -363,24 +352,31 @@ namespace Barotrauma
return null;
}
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection = null)
private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
{
if (connection == null)
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
if (!suitableConnections.Any())
{
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
if (!suitableConnections.Any())
{
suitableConnections = Connections;
}
//prefer connections that haven't been passed through, and connections with fewer available missions
connection = ToolBox.SelectWeightedRandom(
suitableConnections.ToList(),
suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(),
Rand.RandSync.Unsynced);
suitableConnections = Connections.ToList();
}
//prefer connections that haven't been passed through, and connections with fewer available missions
connection = ToolBox.SelectWeightedRandom(
suitableConnections.ToList(),
suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(),
Rand.RandSync.Unsynced);
Location destination = connection.OtherLocation(this);
return prefab.Instantiate(new Location[] { this, destination });
var mission = prefab.Instantiate(new Location[] { this, destination });
mission.AdjustLevelData(connection.LevelData);
return mission;
}
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
{
Location destination = connection.OtherLocation(this);
var mission = prefab.Instantiate(new Location[] { this, destination });
mission.AdjustLevelData(connection.LevelData);
return mission;
}
public void InstantiateLoadedMissions(Map map)
@@ -499,7 +495,7 @@ namespace Barotrauma
{
foreach (Item item in items)
{
if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.OriginalID)) { continue; }
if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; }
if (item.OriginalModuleIndex < 0)
{
DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost.");
@@ -698,7 +694,6 @@ namespace Barotrauma
new XAttribute("name", Name),
new XAttribute("discovered", Discovered),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("normalizeddepth", NormalizedDepth.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("pricemultiplier", PriceMultiplier),
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier));
LevelData.Save(locationElement);
@@ -30,6 +30,8 @@ namespace Barotrauma
public readonly string Identifier;
public readonly string Name;
public readonly float BeaconStationChance;
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public bool UseInMainMenu
@@ -74,6 +76,9 @@ namespace Barotrauma
{
Identifier = element.GetAttributeString("identifier", element.Name.ToString());
Name = TextManager.Get("LocationName." + Identifier, fallBackTag: "unknown");
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
HasOutpost = element.GetAttributeBool("hasoutpost", true);
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -7,9 +8,15 @@ namespace Barotrauma
class LocationTypeChange
{
public readonly string ChangeToType;
public readonly float Probability;
public readonly int RequiredDuration;
public readonly float ProximityProbabilityIncrease;
public readonly int RequiredProximityForProbabilityIncrease;
public readonly bool RequireDiscovered;
public List<string> Messages = new List<string>();
//the change can't happen if there's a location of the given type next to this one
@@ -24,6 +31,11 @@ namespace Barotrauma
Probability = element.GetAttributeFloat("probability", 1.0f);
RequiredDuration = element.GetAttributeInt("requiredduration", 0);
RequireDiscovered = element.GetAttributeBool("requirediscovered", false);
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList();
@@ -35,5 +47,49 @@ namespace Barotrauma
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
}
}
public float DetermineProbability(Location location)
{
float totalProbability = Probability;
if (AnyWithinRequiredProximity(location)) { totalProbability += ProximityProbabilityIncrease; }
return totalProbability;
}
private bool AnyWithinRequiredProximity(Location location, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > RequiredProximityForProbabilityIncrease) { return false; }
if (currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier)) { 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 (AnyWithinRequiredProximity(otherLocation, currentDistance + 1, checkedLocations)) { return true; }
}
}
return false;
}
private int CountWithinRequiredProximity(Location location, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > RequiredProximityForProbabilityIncrease) { return 0; }
int count = currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier) ? 1 : 0;
checkedLocations ??= new HashSet<Location>();
checkedLocations.Add(location);
foreach (var connection in location.Connections)
{
var otherLocation = connection.OtherLocation(location);
if (!checkedLocations.Contains(otherLocation)) { count += CountWithinRequiredProximity(otherLocation, currentDistance+1, checkedLocations); }
}
return count;
}
}
}
@@ -69,6 +69,7 @@ namespace Barotrauma
private Map(CampaignMode campaign, XElement element) : this()
{
Seed = element.GetAttributeString("seed", "a");
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -80,11 +81,14 @@ namespace Barotrauma
Locations.Add(null);
}
Locations[i] = new Location(subElement);
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
break;
}
}
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
for (int i = 0; i < Locations.Count; i++)
{
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
}
foreach (XElement subElement in element.Elements())
{
@@ -101,7 +105,10 @@ namespace Barotrauma
Locations[locationIndices.Y].Connections.Add(connection);
connection.LevelData = new LevelData(subElement.Element("Level"));
string biomeId = subElement.GetAttributeString("biome", "");
connection.Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ?? LevelGenerationParams.GetBiomes().First();
connection.Biome =
LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ??
LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ??
LevelGenerationParams.GetBiomes().First();
Connections.Add(connection);
break;
}
@@ -354,9 +361,8 @@ namespace Barotrauma
CreateEndLocation();
foreach (Location location in Locations)
{
{
location.LevelData = new LevelData(location);
location.NormalizedDepth = location.MapPosition.X / Width;
}
foreach (LocationConnection connection in Connections)
{
@@ -672,9 +678,16 @@ namespace Barotrauma
{
foreach (Location location in Locations)
{
if (!location.Discovered) { continue; }
if (furthestDiscoveredLocation == null ||
location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
}
}
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
foreach (Location location in Locations)
{
if (location.MapPosition.X < furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
}
@@ -682,10 +695,13 @@ namespace Barotrauma
if (location == CurrentLocation || location == SelectedLocation) { continue; }
//find which types of locations this one can change to
var cct = location.Type.CanChangeTo;
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
List<LocationTypeChange> readyTypeChanges = new List<LocationTypeChange>();
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
List<int> readyTypeChanges = new List<int>();
for (int i = 0; i < cct.Count; i++)
{
LocationTypeChange typeChange = cct[i];
if (typeChange.RequireDiscovered && !location.Discovered) { continue; }
//check if there are any adjacent locations that would prevent the change
bool disallowedFound = false;
foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations)
@@ -714,22 +730,26 @@ namespace Barotrauma
if (location.TypeChangeTimer >= typeChange.RequiredDuration)
{
readyTypeChanges.Add(typeChange);
readyTypeChanges.Add(i);
}
}
List<float> readyTypeProbabilities = readyTypeChanges.Select(i => cct[i].DetermineProbability(location)).ToList();
//select a random type change
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(t => t.Probability))
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => readyTypeProbabilities[i]))
{
var selectedTypeChange =
ToolBox.SelectWeightedRandom(readyTypeChanges, readyTypeChanges.Select(t => t.Probability).ToList(), Rand.RandSync.Unsynced);
var selectedTypeChangeIndex =
ToolBox.SelectWeightedRandom(
readyTypeChanges,
readyTypeChanges.Select(i => readyTypeProbabilities[i]).ToList(),
Rand.RandSync.Unsynced);
var selectedTypeChange = cct[selectedTypeChangeIndex];
if (selectedTypeChange != null)
{
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationType(location, prevName, selectedTypeChange);
location.TypeChangeTimer = -1;
break;
}
}
@@ -12,13 +12,14 @@ using Barotrauma.Networking;
namespace Barotrauma
{
abstract partial class MapEntity : Entity
abstract partial class MapEntity : Entity, ISpatialEntity
{
public static List<MapEntity> mapEntityList = new List<MapEntity>();
public readonly MapEntityPrefab prefab;
protected List<ushort> linkedToID;
public List<ushort> unresolvedLinkedToID;
/// <summary>
/// List of upgrades this item has
@@ -249,12 +250,55 @@ namespace Barotrauma
get { return ""; }
}
public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine)
private bool ignoreByAI;
public bool IgnoreByAI => ignoreByAI;
public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore;
public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id)
{
this.prefab = prefab;
Scale = prefab != null ? prefab.Scale : 1;
}
protected void ParseLinks(XElement element, IdRemap idRemap)
{
string linkedToString = element.GetAttributeString("linked", "");
if (!string.IsNullOrEmpty(linkedToString))
{
string[] linkedToIds = linkedToString.Split(',');
for (int i = 0; i < linkedToIds.Length; i++)
{
int srcId = int.Parse(linkedToIds[i]);
int targetId = idRemap.GetOffsetId(srcId);
if (targetId <= 0)
{
unresolvedLinkedToID ??= new List<ushort>();
unresolvedLinkedToID.Add((ushort)srcId);
continue;
}
linkedToID.Add((ushort)targetId);
}
}
}
public void ResolveLinks(IdRemap childRemap)
{
if (unresolvedLinkedToID == null) { return; }
for (int i=0;i<unresolvedLinkedToID.Count;i++)
{
int srcId = unresolvedLinkedToID[i];
int targetId = childRemap.GetOffsetId(srcId);
if (targetId > 0)
{
var otherEntity = FindEntityByID((ushort)targetId) as MapEntity;
linkedTo.Add(otherEntity);
if (otherEntity.Linkable && otherEntity.linkedTo != null) otherEntity.linkedTo.Add(this);
unresolvedLinkedToID.RemoveAt(i);
i--;
}
}
}
public virtual void Move(Vector2 amount)
{
rect.X += (int)amount.X;
@@ -555,8 +599,10 @@ namespace Barotrauma
Move(-relative * 2.0f);
}
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath)
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset)
{
IdRemap idRemap = new IdRemap(parentElement, idOffset);
List<MapEntity> entities = new List<MapEntity>();
foreach (XElement element in parentElement.Elements())
{
@@ -580,7 +626,7 @@ namespace Barotrauma
try
{
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine) });
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) });
if (loadMethod == null)
{
DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + ".");
@@ -591,7 +637,7 @@ namespace Barotrauma
}
else
{
object newEntity = loadMethod.Invoke(t, new object[] { element, submarine });
object newEntity = loadMethod.Invoke(t, new object[] { element, submarine, idRemap });
if (newEntity != null) entities.Add((MapEntity)newEntity);
}
}
@@ -254,6 +254,16 @@ namespace Barotrauma
{
name = name.ToLowerInvariant();
}
//try to search based on identifier first
if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(identifier))
{
foreach (MapEntityPrefab prefab in List)
{
if (prefab.identifier == identifier) { return prefab; }
}
}
foreach (MapEntityPrefab prefab in List)
{
if (identifier != null)
@@ -216,11 +216,13 @@ namespace Barotrauma
List<MapEntity> loadEntities(Submarine sub)
{
Dictionary<PlacedModule, List<MapEntity>> entities = new Dictionary<PlacedModule, List<MapEntity>>();
int idOffset = sub.IdOffset;
for (int i = 0; i < selectedModules.Count; i++)
{
var selectedModule = selectedModules[i];
sub.Info.GameVersion = selectedModule.Info.GameVersion;
var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath);
var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset);
idOffset = moduleEntities.Max(e => e.ID);
MapEntity.InitializeLoadedLinks(moduleEntities);
foreach (MapEntity entity in moduleEntities)
@@ -958,7 +960,7 @@ namespace Barotrauma
return placedEntities;
}
var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath);
var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath, -1);
//remove items that don't fit in the hallway
moduleEntities.Where(e => e is Item item && item.GetComponent<Door>() == null && e.Rect.Width > hallwayLength).ForEach(e => e.Remove());
@@ -1418,7 +1420,7 @@ namespace Barotrauma
}
else
{
idleObjective.Behavior = humanPrefab.BehaviorType;
idleObjective.Behavior = humanPrefab.Behavior;
foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
@@ -14,27 +14,31 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class WallSection
partial class WallSection : ISpatialEntity
{
public Rectangle rect;
public float damage;
public Gap gap;
private bool ignoreByAI;
public WallSection(Rectangle rect)
public Structure Wall { get; }
public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this));
public Vector2 WorldPosition => Wall.SectionPosition(Wall.Sections.IndexOf(this), world: true);
public Vector2 SimPosition => ConvertUnits.ToSimUnits(Position);
public Submarine Submarine => Wall.Submarine;
public Rectangle WorldRect => Submarine == null ? rect :
new Rectangle((int)(rect.X + Submarine.Position.X), (int)(rect.Y + Submarine.Position.Y), rect.Width, rect.Height);
public bool IgnoreByAI => ignoreByAI;
public WallSection(Rectangle rect, Structure wall, float damage = 0.0f)
{
System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0);
this.rect = rect;
damage = 0.0f;
this.damage = damage;
Wall = wall;
}
public WallSection(Rectangle rect, float damage)
{
System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0);
this.rect = rect;
this.damage = 0.0f;
}
public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore;
}
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
@@ -108,7 +112,16 @@ namespace Barotrauma
get => maxHealth ?? Prefab.Health;
set => maxHealth = value;
}
private float crushDepth;
[Serialize(Level.DefaultRealWorldCrushDepth, true)]
public float CrushDepth
{
get => crushDepth;
set => crushDepth = Math.Max(value, Level.DefaultRealWorldCrushDepth);
}
public float Health => MaxHealth;
public override bool DrawBelowWater
@@ -131,10 +144,16 @@ namespace Barotrauma
{
get
{
return Prefab.Body && !IsPlatform;
return Prefab.Body && !IsPlatform;// && HasDamage;
}
}
public bool HasDamage
{
get;
private set;
}
public StructurePrefab Prefab => prefab as StructurePrefab;
public HashSet<string> Tags
@@ -343,8 +362,8 @@ namespace Barotrauma
#endif
}
public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine)
: base(sp, submarine)
public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id = Entity.NullEntityID, XElement element = null)
: base(sp, submarine, id)
{
System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0);
if (rectangle.Width == 0 || rectangle.Height == 0) { return; }
@@ -382,7 +401,6 @@ namespace Barotrauma
StairDirection = Prefab.StairDirection;
NoAITarget = Prefab.NoAITarget;
SerializableProperties = SerializableProperty.GetProperties(this);
InitProjSpecific();
@@ -399,7 +417,7 @@ namespace Barotrauma
else
{
Sections = new WallSection[1];
Sections[0] = new WallSection(rect);
Sections[0] = new WallSection(rect, this);
if (StairDirection != Direction.None)
{
@@ -408,6 +426,8 @@ namespace Barotrauma
}
}
SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this);
// Only add ai targets automatically to submarine/outpost walls
if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget)
{
@@ -552,7 +572,7 @@ namespace Barotrauma
//sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
int xIndex = FlippedX && IsHorizontal ? (xsections - 1 - x) : x;
int yIndex = FlippedY && !IsHorizontal ? (ysections - 1 - y) : y;
Sections[xIndex + yIndex] = new WallSection(sectionRect);
Sections[xIndex + yIndex] = new WallSection(sectionRect, this);
}
else
{
@@ -560,7 +580,7 @@ namespace Barotrauma
sectionRect.Width -= (int)Math.Max(sectionRect.Right - rect.Right, 0.0f);
sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
Sections[x + y] = new WallSection(sectionRect);
Sections[x + y] = new WallSection(sectionRect, this);
}
}
}
@@ -783,33 +803,36 @@ namespace Barotrauma
public void AddDamage(int sectionIndex, float damage, Character attacker = null)
{
if (!Prefab.Body || Prefab.Platform || Indestructible) return;
if (!Prefab.Body || Prefab.Platform || Indestructible ) { return; }
if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) return;
if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; }
var section = Sections[sectionIndex];
#if CLIENT
float dmg = Math.Min(MaxHealth - section.damage, damage);
float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f)));
// Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle
if (particleAmount < 1 && Rand.Value() < 0.10f)
if (damage > 0)
{
particleAmount = 1;
}
for (int i = 1; i <= particleAmount; i++)
{
Vector2 particlePos = new Vector2(
Rand.Range(section.rect.X, section.rect.Right),
Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y));
if (Submarine != null)
float dmg = Math.Min(MaxHealth - section.damage, damage);
float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f)));
// Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle
if (particleAmount < 1 && Rand.Value() < 0.10f)
{
particlePos += Submarine.DrawPosition;
particleAmount = 1;
}
for (int i = 1; i <= particleAmount; i++)
{
Vector2 particlePos = new Vector2(
Rand.Range(section.rect.X, section.rect.Right),
Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y));
if (Submarine != null)
{
particlePos += Submarine.DrawPosition;
}
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)));
if (particle == null) break;
}
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)));
if (particle == null) break;
}
#endif
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -895,15 +918,12 @@ namespace Barotrauma
}
return sectionPos;
}
}
}
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false)
{
if (Submarine != null && Submarine.GodMode) return new AttackResult(0.0f, null);
if (!Prefab.Body || Prefab.Platform || Indestructible) return new AttackResult(0.0f, null);
if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); }
if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); }
Vector2 transformedPos = worldPosition;
if (Submarine != null) transformedPos -= Submarine.Position;
@@ -921,26 +941,24 @@ namespace Barotrauma
GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f);
#endif
}
}
}
#if CLIENT
if (playSound)
if (playSound && damageAmount > 0)
{
SoundPlayer.PlayDamageSound(attack.StructureSoundType, damageAmount, worldPosition, tags: Tags);
}
#endif
return new AttackResult(damageAmount, null);
}
private void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
{
if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
if (!Prefab.Body) { return; }
if (!MathUtils.IsValid(damage)) { return; }
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
#if SERVER
if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
{
@@ -1054,13 +1072,14 @@ namespace Barotrauma
}
float gapOpen = (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen;
Sections[sectionIndex].gap.Open = gapOpen;
}
float damageDiff = damage - Sections[sectionIndex].damage;
bool hadHole = SectionBodyDisabled(sectionIndex);
Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth);
HasDamage = Sections.Any(s => s.damage > 0.0f);
if (attacker != null && damageDiff != 0.0f)
{
OnHealthChangedProjSpecific(attacker, damageDiff);
@@ -1077,7 +1096,7 @@ namespace Barotrauma
bool hasHole = SectionBodyDisabled(sectionIndex);
if (hadHole == hasHole) return;
if (hadHole == hasHole) { return; }
UpdateSections();
}
@@ -1259,7 +1278,7 @@ namespace Barotrauma
}
}
public static Structure Load(XElement element, Submarine submarine)
public static Structure Load(XElement element, Submarine submarine, IdRemap idRemap)
{
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
@@ -1272,14 +1291,10 @@ namespace Barotrauma
}
Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
Structure s = new Structure(rect, prefab, submarine)
Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element), element)
{
Submarine = submarine,
ID = (ushort)int.Parse(element.Attribute("ID").Value)
};
s.OriginalID = s.ID;
SerializableProperty.DeserializeProperties(s, element);
if (submarine?.Info.GameVersion != null)
{
@@ -152,6 +152,47 @@ namespace Barotrauma
}
}
private float? realWorldCrushDepth;
public float RealWorldCrushDepth
{
get
{
if (!realWorldCrushDepth.HasValue)
{
realWorldCrushDepth = float.PositiveInfinity;
foreach (Structure structure in Structure.WallList)
{
if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; }
realWorldCrushDepth = Math.Min(structure.CrushDepth, realWorldCrushDepth.Value);
}
if (Info.SubmarineClass == SubmarineClass.DeepDiver)
{
realWorldCrushDepth *= 1.2f;
}
}
return realWorldCrushDepth.Value;
}
}
/// <summary>
/// How deep down the sub is from the surface of Europa in meters (affected by level type, does not correspond to "actual" coordinate systems)
/// </summary>
public float RealWorldDepth
{
get
{
if (Level.Loaded?.GenerationParams == null)
{
return -WorldPosition.Y * Physics.DisplayToRealWorldRatio;
}
else if (GameMain.GameSession?.Campaign == null)
{
return (-(WorldPosition.Y - Level.Loaded.GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio;
}
return Level.Loaded.GetRealWorldDepth(WorldPosition.Y);
}
}
public bool AtEndPosition
{
get
@@ -210,7 +251,11 @@ namespace Barotrauma
public bool AtDamageDepth
{
get { return subBody != null && subBody.AtDamageDepth; }
get
{
if (Level.Loaded == null || subBody == null) { return false; }
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth;
}
}
public override string ToString()
@@ -236,6 +281,44 @@ namespace Barotrauma
return Math.Max(minPrice, (int)price);
}
private float ballastFloraTimer;
public void AttemptBallastFloraInfection(string identifier, float deltaTime, float probability)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (ballastFloraTimer < 1f)
{
ballastFloraTimer += deltaTime;
return;
}
ballastFloraTimer = 0;
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) >= probability) { return; }
List<Pump> pumps = new List<Pump>();
List<Item> allItems = GetItems(true);
bool anyHasTag = allItems.Any(i => i.HasTag("ballast"));
foreach (Item item in allItems)
{
if ((!anyHasTag || item.HasTag("ballast")) && item.GetComponent<Pump>() is { } pump)
{
if (pump.Infected) { continue; }
pumps.Add(pump);
}
}
if (!pumps.Any()) { return; }
Pump randomPump = pumps.GetRandom(Rand.RandSync.Unsynced);
randomPump.Infected = true;
randomPump.InfectIdentifier = identifier;
#if SERVER
randomPump.Item.CreateServerEvent(randomPump);
#endif
}
public void MakeWreck()
{
Info.Type = SubmarineType.Wreck;
@@ -732,7 +815,7 @@ namespace Barotrauma
/// check visibility between two points (in sim units)
/// </summary>
/// <returns>a physics body that was between the points (or null)</returns>
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true)
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true)
{
Body closestBody = null;
float closestFraction = 1.0f;
@@ -753,6 +836,7 @@ namespace Barotrauma
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionWall)
&& !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; }
if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; }
if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; }
if (fixture.Body.UserData as string == "ruinroom") { return -1; }
if (fixture.Body.UserData is Structure structure)
{
@@ -1171,10 +1255,12 @@ namespace Barotrauma
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
}
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null) : base(null)
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
{
Loading = true;
loaded.Add(this);
Info = new SubmarineInfo(info);
ConnectedDockingPorts = new Dictionary<Submarine, DockingPort>();
@@ -1202,7 +1288,7 @@ namespace Barotrauma
{
if (Info.SubmarineElement != null)
{
newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath);
newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset);
}
}
else
@@ -1211,6 +1297,15 @@ namespace Barotrauma
newEntities.ForEach(me => me.Submarine = this);
}
if (newEntities != null)
{
foreach (var e in newEntities)
{
if (linkedRemap != null) { e.ResolveLinks(linkedRemap); }
e.unresolvedLinkedToID = null;
}
}
Vector2 center = Vector2.Zero;
var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this);
@@ -1279,8 +1374,6 @@ namespace Barotrauma
}
}
loaded.Add(this);
if (entityGrid != null)
{
Hull.EntityGrids.Remove(entityGrid);
@@ -1290,7 +1383,7 @@ namespace Barotrauma
for (int i = 0; i < MapEntity.mapEntityList.Count; i++)
{
if (MapEntity.mapEntityList[i].Submarine != this) continue;
if (MapEntity.mapEntityList[i].Submarine != this) { continue; }
MapEntity.mapEntityList[i].Move(HiddenSubPosition);
}
@@ -1313,6 +1406,11 @@ namespace Barotrauma
}
}
if (GameMain.GameSession?.Campaign?.UpgradeManager != null)
{
GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth;
}
#if CLIENT
GameMain.LightManager.OnMapLoaded();
#endif
@@ -1333,19 +1431,27 @@ namespace Barotrauma
if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f);
}
}
ID = (ushort)(ushort.MaxValue - 1 - Submarine.loaded.IndexOf(this));
}
public static Submarine Load(SubmarineInfo info, bool unloadPrevious)
protected override ushort DetermineID(ushort id, Submarine submarine)
{
return (ushort)(ReservedIDStart - Submarine.loaded.Count);
}
public static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap = null)
{
if (unloadPrevious) { Unload(); }
Submarine sub = new Submarine(info, false);
Submarine sub = new Submarine(info, false, linkedRemap: linkedRemap);
return sub;
}
private void ResetCrushDepth()
{
realWorldCrushDepth = null;
}
public static void RepositionEntities(Vector2 moveAmount, IEnumerable<MapEntity> entities)
{
if (moveAmount.LengthSquared() < 0.00001f) { return; }
@@ -1444,7 +1550,7 @@ namespace Barotrauma
visibleEntities = null;
if (GameMain.GameScreen.Cam != null) GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; }
RemoveAll();
@@ -1482,6 +1588,11 @@ namespace Barotrauma
subBody?.Remove();
subBody = null;
if (GameMain.GameSession?.Campaign?.UpgradeManager != null)
{
GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged -= ResetCrushDepth;
}
if (entityGrid != null)
{
Hull.EntityGrids.Remove(entityGrid);
@@ -1516,7 +1627,7 @@ namespace Barotrauma
}
}
private HashSet<PathNode> obstructedNodes = new HashSet<PathNode>();
private readonly Dictionary<Submarine, HashSet<PathNode>> obstructedNodes = new Dictionary<Submarine, HashSet<PathNode>>();
/// <summary>
/// Permanently disables obstructed waypoints obstructed by the level.
@@ -1553,7 +1664,7 @@ namespace Barotrauma
{
if (otherSub == null) { return; }
if (otherSub == this) { return; }
// Check collisions to other subs. Currently only walls are taken into account.
// Check collisions to other subs.
foreach (var node in OutdoorNodes)
{
if (node == null || node.Waypoint == null) { continue; }
@@ -1572,8 +1683,13 @@ namespace Barotrauma
{
connectedWp.isObstructed = true;
wp.isObstructed = true;
obstructedNodes.Add(node);
obstructedNodes.Add(connection);
if (!obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
{
nodes = new HashSet<PathNode>();
obstructedNodes.Add(otherSub, nodes);
}
nodes.Add(node);
nodes.Add(connection);
break;
}
}
@@ -1584,13 +1700,14 @@ namespace Barotrauma
/// <summary>
/// Only affects temporarily disabled waypoints.
/// </summary>
public void EnableObstructedWaypoints()
public void EnableObstructedWaypoints(Submarine otherSub)
{
foreach (var node in obstructedNodes)
if (obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
{
node.Waypoint.isObstructed = false;
nodes.ForEach(n => n.Waypoint.isObstructed = false);
nodes.Clear();
obstructedNodes.Remove(otherSub);
}
obstructedNodes.Clear();
}
}
}
@@ -23,7 +23,6 @@ namespace Barotrauma
const float VerticalDrag = 0.05f;
const float MaxDrag = 0.1f;
public const float DamageDepth = -30000.0f;
private const float ImpactDamageMultiplier = 10.0f;
//limbs with a mass smaller than this won't cause an impact when they hit the sub
@@ -40,7 +39,7 @@ namespace Barotrauma
private set;
}
private float depthDamageTimer;
private float depthDamageTimer = 10.0f;
private readonly Submarine submarine;
@@ -94,11 +93,6 @@ namespace Barotrauma
get { return positionBuffer; }
}
public bool AtDamageDepth
{
get { return Position.Y < DamageDepth; }
}
public Submarine Submarine
{
get { return submarine; }
@@ -275,7 +269,7 @@ namespace Barotrauma
if (impact.Target.UserData is VoronoiCell cell)
{
HandleLevelCollision(impact);
HandleLevelCollision(impact, cell);
}
else if (impact.Target.Body.UserData is Structure)
{
@@ -451,29 +445,26 @@ namespace Barotrauma
private void UpdateDepthDamage(float deltaTime)
{
if (Position.Y > DamageDepth) { return; }
#if CLIENT
if (GameMain.GameSession.GameMode is TestGameMode) { return; }
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
#endif
float depth = DamageDepth - Position.Y;
if (Level.Loaded == null) { return; }
float submarineDepth = submarine.RealWorldDepth;
if (submarineDepth < Level.Loaded.RealWorldCrushDepth) { return; }
depthDamageTimer -= deltaTime;
if (depthDamageTimer > 0.0f) { return; }
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != submarine) { continue; }
if (wall.Submarine != submarine || wall.CrushDepth > submarineDepth) { continue; }
if (wall.Health < depth * 0.01f)
float pastCrushDepth = submarineDepth - wall.CrushDepth;
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f);
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, depth * 0.01f);
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(depth * 0.001f, 50.0f));
}
}
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(pastCrushDepth * 0.001f, 50.0f));
}
}
depthDamageTimer = 10.0f;
@@ -655,7 +646,7 @@ namespace Barotrauma
}
}
private void HandleLevelCollision(Impact impact)
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
{
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10)
{
@@ -672,6 +663,22 @@ namespace Barotrauma
dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
}
if (cell != null && cell.IsDestructible && wallImpact > 0.0f)
{
var hitWall = Level.Loaded?.ExtraWalls.Find(w => w.Cells.Contains(cell));
if (hitWall != null && hitWall.WallDamageOnTouch > 0.0f)
{
var damagedStructures = Explosion.RangedStructureDamage(
ConvertUnits.ToDisplayUnits(impact.ImpactPos),
500.0f,
hitWall.WallDamageOnTouch,
levelWallDamage: 0.0f);
#if CLIENT
PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash");
#endif
}
}
#if CLIENT
int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50);
for (int i = 0; i < particleAmount; i++)
@@ -836,34 +843,15 @@ namespace Barotrauma
item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f);
}
float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f;
var damagedStructures = Explosion.RangedStructureDamage(
ConvertUnits.ToDisplayUnits(impactPos),
impact * 50.0f,
applyDamage ? impact * ImpactDamageMultiplier : 0.0f);
impact * 50.0f,
dmg, dmg);
#if CLIENT
//play a damage sound for the structure that took the most damage
float maxDamage = 0.0f;
Structure maxDamageStructure = null;
foreach (KeyValuePair<Structure, float> structureDamage in damagedStructures)
{
if (maxDamageStructure == null || structureDamage.Value > maxDamage)
{
maxDamage = structureDamage.Value;
maxDamageStructure = structureDamage.Key;
}
}
if (maxDamageStructure != null)
{
SoundPlayer.PlayDamageSound(
"StructureBlunt",
impact * 10.0f,
ConvertUnits.ToDisplayUnits(impactPos),
MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f),
maxDamageStructure.Tags);
}
PlayDamageSounds(damagedStructures, impactPos, impact, "StructureBlunt");
#endif
}
@@ -23,7 +23,7 @@ namespace Barotrauma
HideInMenus = 2
}
public enum SubmarineType { Player, Outpost, OutpostModule, Wreck }
public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation }
public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver }
partial class SubmarineInfo : IDisposable
@@ -517,7 +517,8 @@ namespace Barotrauma
{
var contentPackageSubs = ContentPackage.GetFilesOfType(
GameMain.Config.AllEnabledPackages,
ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, ContentType.Wreck);
ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule,
ContentType.Wreck, ContentType.BeaconStation);
for (int i = savedSubmarines.Count - 1; i >= 0; i--)
{
@@ -553,7 +554,7 @@ namespace Barotrauma
subDirectories = Directory.GetDirectories(SavePath).Where(s =>
{
DirectoryInfo dir = new DirectoryInfo(s);
return (dir.Attributes & System.IO.FileAttributes.Hidden) == 0;
return !dir.Attributes.HasFlag(System.IO.FileAttributes.Hidden) && !dir.Name.StartsWith(".");
}).ToArray();
}
catch (Exception e)
@@ -44,6 +44,8 @@ namespace Barotrauma
public Hull CurrentHull { get; private set; }
public Level.Tunnel Tunnel;
public SpawnType SpawnType
{
get { return spawnType; }
@@ -105,8 +107,8 @@ namespace Barotrauma
{
}
public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine)
: base (prefab, submarine)
public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID)
: base (prefab, submarine, id)
{
rect = newRect;
idCardTags = new string[0];
@@ -626,7 +628,7 @@ namespace Barotrauma
}
}
public static WayPoint Load(XElement element, Submarine submarine)
public static WayPoint Load(XElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = new Rectangle(
int.Parse(element.Attribute("x").Value),
@@ -635,11 +637,7 @@ namespace Barotrauma
Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine)
{
ID = (ushort)int.Parse(element.Attribute("ID").Value)
};
w.OriginalID = w.ID;
WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine, idRemap.GetOffsetId(element));
w.spawnType = spawnType;
string idCardDescString = element.GetAttributeString("idcarddesc", "");
@@ -663,14 +661,24 @@ namespace Barotrauma
JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase));
}
w.ladderId = (ushort)element.GetAttributeInt("ladders", 0);
w.gapId = (ushort)element.GetAttributeInt("gap", 0);
w.linkedToID = new List<ushort>();
w.ladderId = idRemap.GetOffsetId(element.GetAttributeInt("ladders", 0));
w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0));
int i = 0;
while (element.Attribute("linkedto" + i) != null)
{
w.linkedToID.Add((ushort)int.Parse(element.Attribute("linkedto" + i).Value));
int srcId = int.Parse(element.Attribute("linkedto" + i).Value);
int destId = idRemap.GetOffsetId(srcId);
if (destId > 0)
{
w.linkedToID.Add((ushort)destId);
}
else
{
w.unresolvedLinkedToID ??= new List<ushort>();
w.unresolvedLinkedToID.Add((ushort)srcId);
}
i += 1;
}
return w;