(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,133 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObject
{
public readonly LevelObjectPrefab Prefab;
public Vector3 Position;
public float NetworkUpdateTimer;
public float Scale;
public float Rotation;
private int spriteIndex;
public LevelObjectPrefab ActivePrefab;
public PhysicsBody PhysicsBody
{
get;
private set;
}
public List<LevelTrigger> Triggers
{
get;
private set;
}
public bool NeedsNetworkSyncing
{
get { return Triggers.Any(t => t.NeedsNetworkSyncing); }
set { Triggers.ForEach(t => t.NeedsNetworkSyncing = false); }
}
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]; }
}
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
{
Triggers = new List<LevelTrigger>();
ActivePrefab = Prefab = prefab;
Position = position;
Scale = scale;
Rotation = rotation;
spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1;
if (prefab.PhysicsBodyElement != null)
{
PhysicsBody = new PhysicsBody(prefab.PhysicsBodyElement, ConvertUnits.ToSimUnits(new Vector2(position.X, position.Y)), Scale);
}
foreach (XElement triggerElement in prefab.LevelTriggerElements)
{
Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale;
if (rotation != 0.0f)
{
var ca = (float)Math.Cos(rotation);
var sa = (float)Math.Sin(rotation);
triggerPosition = new Vector2(
ca * triggerPosition.X + sa * triggerPosition.Y,
-sa * triggerPosition.X + ca * triggerPosition.Y);
}
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];
Triggers.Add(newTrigger);
}
InitProjSpecific();
}
partial void InitProjSpecific();
public Vector2 LocalToWorld(Vector2 localPosition, float swingState = 0.0f)
{
Vector2 emitterPos = localPosition * Scale;
if (Rotation != 0.0f || Prefab.SwingAmountRad != 0.0f)
{
float rot = Rotation + swingState * Prefab.SwingAmountRad;
var ca = (float)Math.Cos(rot);
var sa = (float)Math.Sin(rot);
emitterPos = new Vector2(
ca * emitterPos.X + sa * emitterPos.Y,
-sa * emitterPos.X + ca * emitterPos.Y);
}
return new Vector2(Position.X, Position.Y) + emitterPos;
}
public void Remove()
{
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
public override string ToString()
{
return "LevelObject (" + ActivePrefab.Name + ")";
}
public void ServerWrite(IWriteMessage msg, Client c)
{
for (int j = 0; j < Triggers.Count; j++)
{
if (!Triggers[j].UseNetworkSyncing) continue;
Triggers[j].ServerWrite(msg, c);
}
}
}
}
@@ -0,0 +1,416 @@
#if CLIENT
using Barotrauma.Particles;
#endif
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
namespace Barotrauma
{
partial class LevelObjectManager : Entity, IServerSerializable
{
const int GridSize = 2000;
private List<LevelObject> objects;
private List<LevelObject>[,] objectGrid;
public LevelObjectManager() : base(null)
{
}
class SpawnPosition
{
public readonly GraphEdge GraphEdge;
public readonly Vector2 Normal;
public readonly LevelObjectPrefab.SpawnPosType SpawnPosType;
public readonly Alignment Alignment;
public readonly float Length;
public SpawnPosition(GraphEdge graphEdge, Vector2 normal, LevelObjectPrefab.SpawnPosType spawnPosType, Alignment alignment)
{
GraphEdge = graphEdge;
Normal = normal;
SpawnPosType = spawnPosType;
Alignment = alignment;
Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2);
}
public float GetSpawnProbability(LevelObjectPrefab prefab)
{
if (prefab.ClusteringAmount <= 0.0f) return Length;
float noise = (float)(
PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 10000.0f, GraphEdge.Point1.Y / 10000.0f, prefab.ClusteringGroup) +
PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 20000.0f, GraphEdge.Point1.Y / 20000.0f, prefab.ClusteringGroup));
return Length * (float)Math.Pow(noise, prefab.ClusteringAmount);
}
}
public void PlaceObjects(Level level, int amount)
{
objectGrid = new List<LevelObject>[
level.Size.X / GridSize,
(level.Size.Y - level.BottomPos) / GridSize];
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
var levelCells = level.GetAllCells();
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall));
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(level.SeaFloor.Cells, LevelObjectPrefab.SpawnPosType.SeaFloor));
foreach (RuinGeneration.Ruin ruin in level.Ruins)
{
foreach (var ruinShape in ruin.RuinShapes)
{
foreach (var wall in ruinShape.Walls)
{
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(wall.A, wall.B),
(wall.A + wall.B) / 2.0f - ruinShape.Center,
LevelObjectPrefab.SpawnPosType.RuinWall,
ruinShape.GetLineAlignment(wall)));
}
}
}
foreach (var posOfInterest in level.PositionsOfInterest)
{
if (posOfInterest.PositionType != Level.PositionType.MainPath) continue;
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(posOfInterest.Position.ToVector2(), posOfInterest.Position.ToVector2() + Vector2.UnitX),
Vector2.UnitY,
LevelObjectPrefab.SpawnPosType.MainPath,
Alignment.Top));
}
objects = new List<LevelObject>();
for (int i = 0; i < amount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams.Name);
SpawnPosition spawnPosition = FindObjectPosition(availableSpawnPositions, level, prefab);
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);
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) =>
{
OnObjectTriggered(newObject, levelTrigger, obj);
};
}
var spriteCorners = new List<Vector2>
{
Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero
};
Sprite sprite = newObject.Sprite ?? newObject.Prefab.DeformableSprite?.Sprite;
//calculate the positions of the corners of the rotated sprite
if (sprite != null)
{
Vector2 halfSize = sprite.size * newObject.Scale / 2;
spriteCorners[0] = -halfSize;
spriteCorners[1] = new Vector2(-halfSize.X, halfSize.Y);
spriteCorners[2] = halfSize;
spriteCorners[3] = new Vector2(halfSize.X, -halfSize.Y);
Vector2 pivotOffset = sprite.Origin * newObject.Scale - halfSize;
pivotOffset.X = -pivotOffset.X;
pivotOffset = new Vector2(
(float)(pivotOffset.X * Math.Cos(-newObject.Rotation) - pivotOffset.Y * Math.Sin(-newObject.Rotation)),
(float)(pivotOffset.X * Math.Sin(-newObject.Rotation) + pivotOffset.Y * Math.Cos(-newObject.Rotation)));
for (int j = 0; j < 4; j++)
{
spriteCorners[j] = new Vector2(
(float)(spriteCorners[j].X * Math.Cos(-newObject.Rotation) - spriteCorners[j].Y * Math.Sin(-newObject.Rotation)),
(float)(spriteCorners[j].X * Math.Sin(-newObject.Rotation) + spriteCorners[j].Y * Math.Cos(-newObject.Rotation)));
spriteCorners[j] += new Vector2(newObject.Position.X, newObject.Position.Y) + pivotOffset;
}
}
float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z;
float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z;
float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos;
float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos;
foreach (LevelTrigger trigger in newObject.Triggers)
{
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);
}
}
#if CLIENT
if (newObject.ParticleEmitters != null)
{
foreach (ParticleEmitter emitter in newObject.ParticleEmitters)
{
Rectangle particleBounds = emitter.CalculateParticleBounds(new Vector2(newObject.Position.X, newObject.Position.Y));
minX = Math.Min(minX, particleBounds.X);
maxX = Math.Max(maxX, particleBounds.Right);
minY = Math.Min(minY, particleBounds.Y - level.BottomPos);
maxY = Math.Max(maxY, particleBounds.Bottom - level.BottomPos);
}
}
#endif
objects.Add(newObject);
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
int xStart = (int)Math.Floor(minX / GridSize);
int xEnd = (int)Math.Floor(maxX / GridSize);
if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) return;
int yStart = (int)Math.Floor(minY / GridSize);
int yEnd = (int)Math.Floor(maxY / GridSize);
if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) return;
xStart = Math.Max(xStart, 0);
xEnd = Math.Min(xEnd, objectGrid.GetLength(0) - 1);
yStart = Math.Max(yStart, 0);
yEnd = Math.Min(yEnd, objectGrid.GetLength(1) - 1);
for (int x = xStart; x <= xEnd; x++)
{
for (int y = yStart; y <= yEnd; y++)
{
if (objectGrid[x, y] == null) objectGrid[x, y] = new List<LevelObject>();
objectGrid[x, y].Add(newObject);
}
}
}
public Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition)
{
return new Microsoft.Xna.Framework.Point(
(int)Math.Floor(worldPosition.X / GridSize),
(int)Math.Floor((worldPosition.Y - Level.Loaded.BottomPos) / GridSize));
}
public IEnumerable<LevelObject> GetAllObjects()
{
return objects;
}
private readonly static List<LevelObject> objectsInRange = new List<LevelObject>();
public IEnumerable<LevelObject> GetAllObjects(Vector2 worldPosition, float radius)
{
var minIndices = GetGridIndices(worldPosition - Vector2.One * radius);
if (minIndices.X >= objectGrid.GetLength(0) || minIndices.Y >= objectGrid.GetLength(1)) return Enumerable.Empty<LevelObject>();
var maxIndices = GetGridIndices(worldPosition + Vector2.One * radius);
if (maxIndices.X < 0 || maxIndices.Y < 0) return Enumerable.Empty<LevelObject>();
minIndices.X = Math.Max(0, minIndices.X);
minIndices.Y = Math.Max(0, minIndices.Y);
maxIndices.X = Math.Min(objectGrid.GetLength(0) - 1, maxIndices.X);
maxIndices.Y = Math.Min(objectGrid.GetLength(1) - 1, maxIndices.Y);
objectsInRange.Clear();
for (int x = minIndices.X; x <= maxIndices.X; x++)
{
for (int y = minIndices.Y; y <= maxIndices.Y; y++)
{
if (objectGrid[x, y] == null) continue;
foreach (LevelObject obj in objectGrid[x, y])
{
if (!objectsInRange.Contains(obj)) objectsInRange.Add(obj);
}
}
}
return objectsInRange;
}
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType)
{
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
foreach (var cell in cells)
{
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid || edge.OutsideLevel) continue;
Vector2 normal = edge.GetNormal(cell);
Alignment edgeAlignment = 0;
if (normal.Y < -0.5f)
edgeAlignment |= Alignment.Bottom;
else if (normal.Y > 0.5f)
edgeAlignment |= Alignment.Top;
else if (normal.X < -0.5f)
edgeAlignment |= Alignment.Left;
else if(normal.X > 0.5f)
edgeAlignment |= Alignment.Right;
availableSpawnPositions.Add(new SpawnPosition(edge, normal, spawnPosType, edgeAlignment));
}
}
return availableSpawnPositions;
}
private SpawnPosition FindObjectPosition(List<SpawnPosition> availableSpawnPositions, Level level, LevelObjectPrefab prefab)
{
if (prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.None) return null;
var suitableSpawnPositions = availableSpawnPositions.Where(sp =>
prefab.SpawnPos.HasFlag(sp.SpawnPosType) && sp.Length >= prefab.MinSurfaceWidth && prefab.Alignment.HasFlag(sp.Alignment)).ToList();
return ToolBox.SelectWeightedRandom(suitableSpawnPositions, suitableSpawnPositions.Select(sp => sp.GetSpawnProbability(prefab)).ToList(), Rand.RandSync.Server);
}
public void Update(float deltaTime)
{
foreach (LevelObject obj in objects)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
obj.NetworkUpdateTimer -= deltaTime;
if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f)
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { obj });
obj.NeedsNetworkSyncing = false;
obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval;
}
}
obj.ActivePrefab = obj.Prefab;
for (int i = 0; i < obj.Triggers.Count; 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;
}
}
UpdateProjSpecific(deltaTime);
}
partial void UpdateProjSpecific(float deltaTime);
private void OnObjectTriggered(LevelObject triggeredObject, LevelTrigger trigger, Entity triggerer)
{
if (trigger.TriggerOthersDistance <= 0.0f) return;
foreach (LevelObject obj in objects)
{
if (obj == triggeredObject) continue;
foreach (LevelTrigger otherTrigger in obj.Triggers)
{
otherTrigger.OtherTriggered(triggeredObject, trigger);
}
}
}
private LevelObjectPrefab GetRandomPrefab(string levelType)
{
return ToolBox.SelectWeightedRandom(
LevelObjectPrefab.List,
LevelObjectPrefab.List.Select(p => p.GetCommonness(levelType)).ToList(), Rand.RandSync.Server);
}
public override void Remove()
{
if (objects != null)
{
foreach (LevelObject obj in objects)
{
obj.Remove();
}
objects.Clear();
}
RemoveProjSpecific();
base.Remove();
}
partial void RemoveProjSpecific();
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
LevelObject obj = extraData[0] as LevelObject;
msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count);
obj.ServerWrite(msg, c);
}
}
}
@@ -0,0 +1,397 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObjectPrefab : ISerializableEntity
{
private static List<LevelObjectPrefab> list = new List<LevelObjectPrefab>();
public static List<LevelObjectPrefab> List
{
get { return list; }
}
public class ChildObject
{
public List<string> AllowedNames;
public int MinCount, MaxCount;
public ChildObject()
{
AllowedNames = new List<string>();
MinCount = 1;
MaxCount = 1;
}
public ChildObject(XElement element)
{
AllowedNames = element.GetAttributeStringArray("names", new string[0]).ToList();
MinCount = element.GetAttributeInt("mincount", 1);
MaxCount = Math.Max(element.GetAttributeInt("maxcount", 1), MinCount);
}
}
[Flags]
public enum SpawnPosType
{
None = 0,
Wall = 1,
RuinWall = 2,
SeaFloor = 4,
MainPath = 8
}
public List<Sprite> Sprites
{
get;
private set;
} = new List<Sprite>();
public List<Sprite> SpecularSprites
{
get;
private set;
} = new List<Sprite>();
public DeformableSprite DeformableSprite
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public float MinSize
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
public float MaxSize
{
get;
private set;
}
/// <summary>
/// Which sides of a wall the object can appear on.
/// </summary>
[Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), true, description: "Which sides of a wall the object can spawn on."), Editable]
public Alignment Alignment
{
get;
private set;
}
[Serialize(SpawnPosType.Wall, false), Editable()]
public SpawnPosType SpawnPos
{
get;
private set;
}
public XElement Config
{
get;
private set;
}
public readonly List<XElement> LevelTriggerElements;
/// <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 Dictionary<string, float> OverrideCommonness;
public XElement PhysicsBodyElement
{
get;
private set;
}
public int PhysicsBodyTriggerIndex
{
get;
private set;
}
[Serialize("0.0,1.0", true), Editable]
public Vector2 DepthRange
{
get;
private set;
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f),
Serialize(0.0f, true, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")]
/// <summary>
/// The tendency for the prefab to form clusters. Used as an exponent for perlin noise values
/// that are used to determine the probability for an object to spawn at a specific position.
/// </summary>
public float ClusteringAmount
{
get;
private set;
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f),
Serialize(0.0f, true, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " +
" for an object to spawn at a specific position. Using the same (or close) value for different objects means the objects tend " +
"to form clusters in the same areas.")]
/// <summary>
/// A value between 0-1 that determines the z-coordinate to sample perlin noise from when
/// determining the probability for an object to spawn at a specific position.
/// Using the same (or close) value for different objects means the objects tend to form clusters
/// in the same areas.
/// </summary>
public float ClusteringGroup
{
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
{
get;
private set;
}
[Serialize(0.0f, true, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
/// <summary>
/// Minimum length of a graph edge the object can spawn on.
/// </summary>
public float MinSurfaceWidth
{
get;
private set;
}
private Vector2 randomRotation;
[Editable, Serialize("0.0,0.0", true, description: "How much the rotation of the object can vary (min and max values in degrees).")]
public Vector2 RandomRotation
{
get { return new Vector2(MathHelper.ToDegrees(randomRotation.X), MathHelper.ToDegrees(randomRotation.Y)); }
private set
{
randomRotation = new Vector2(MathHelper.ToRadians(value.X), MathHelper.ToRadians(value.Y));
}
}
public Vector2 RandomRotationRad => randomRotation;
private float swingAmount;
[Serialize(0.0f, true, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)]
public float SwingAmount
{
get { return MathHelper.ToDegrees(swingAmount); }
private set
{
swingAmount = MathHelper.ToRadians(value);
}
}
public float SwingAmountRad => swingAmount;
[Serialize(0.0f, true, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SwingFrequency
{
get;
private set;
}
[Editable, Serialize("0.0,0.0", true, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")]
public Vector2 ScaleOscillation
{
get;
private set;
}
[Serialize(0.0f, true, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float ScaleOscillationFrequency
{
get;
private set;
}
[Editable, Serialize(1.0f, true, description: "How likely it is for the object to spawn in a level. " +
"This is relative to the commonness of the other objects - for example, having an object with " +
"a commonness of 1 and another with a commonness of 10 would mean the latter appears in levels 10 times as frequently as the former. " +
"The commonness value can be overridden on specific level types.")]
public float Commonness
{
get;
private set;
}
[Serialize(0.0f, true, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SonarDisruption
{
get;
private set;
}
public string Name
{
get;
set;
}
public List<ChildObject> ChildObjects
{
get;
private set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get; private set;
}
/// <summary>
/// A list of prefabs whose properties override this one's properties when a trigger is active.
/// E.g. if a trigger in the index 1 of the trigger list is active, the properties in index 1 in this list are used (unless it's null)
/// </summary>
public List<LevelObjectPrefab> OverrideProperties
{
get;
private set;
}
public override string ToString()
{
return "LevelObjectPrefab (" + Name + ")";
}
public static void LoadAll()
{
list.Clear();
var files = GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs);
if (files.Count() > 0)
{
foreach (var file in files)
{
LoadConfig(file.Path);
}
}
else
{
LoadConfig("Content/LevelObjects/LevelObject/Prefabs.xml");
}
}
private static void LoadConfig(string configPath)
{
try
{
XDocument doc = XMLExtensions.TryLoadXml(configPath);
if (doc == null) { return; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
DebugConsole.NewMessage($"Overriding all level object prefabs with '{configPath}'", Color.Yellow);
list.Clear();
}
else if (list.Any())
{
DebugConsole.NewMessage($"Loading additional level object prefabs from file '{configPath}'");
}
foreach (XElement element in mainElement.Elements())
{
list.Add(new LevelObjectPrefab(element));
}
}
catch (Exception e)
{
DebugConsole.ThrowError(String.Format("Failed to load LevelObject prefabs from {0}", configPath), e);
}
}
public LevelObjectPrefab(XElement element)
{
ChildObjects = new List<ChildObject>();
LevelTriggerElements = new List<XElement>();
OverrideProperties = new List<LevelObjectPrefab>();
OverrideCommonness = new Dictionary<string, float>();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element != null)
{
Config = element;
Name = element.Name.ToString();
LoadElements(element, -1);
InitProjSpecific(element);
}
//use the maximum width of the sprite as the minimum surface width if no value is given
if (element != null && !element.Attributes("minsurfacewidth").Any())
{
if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize;
if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize);
}
}
private void LoadElements(XElement element, int parentTriggerIndex)
{
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));
break;
case "deformablesprite":
DeformableSprite = new DeformableSprite(subElement, lazyLoad: true);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "");
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
}
break;
case "leveltrigger":
case "trigger":
OverrideProperties.Add(null);
LevelTriggerElements.Add(subElement);
LoadElements(subElement, LevelTriggerElements.Count - 1);
break;
case "childobject":
ChildObjects.Add(new ChildObject(subElement));
break;
case "overrideproperties":
var propertyOverride = new LevelObjectPrefab(subElement);
OverrideProperties[OverrideProperties.Count - 1] = propertyOverride;
if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null)
{
propertyOverride.Sprites = Sprites;
propertyOverride.DeformableSprite = DeformableSprite;
}
break;
case "body":
case "physicsbody":
PhysicsBodyElement = subElement;
PhysicsBodyTriggerIndex = parentTriggerIndex;
break;
}
}
}
partial void InitProjSpecific(XElement element);
public float GetCommonness(string levelType)
{
if (!OverrideCommonness.TryGetValue(levelType, out float commonness))
{
return Commonness;
}
return commonness;
}
}
}
@@ -0,0 +1,612 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelTrigger
{
[Flags]
enum TriggererType
{
None = 0,
Human = 1,
Creature = 2,
Character = Human | Creature,
Submarine = 4,
Item = 8,
OtherTrigger = 16
}
public enum TriggerForceMode
{
Force, //default, apply a force to the object over time
Acceleration, //apply an acceleration to the object, ignoring it's mass
Impulse, //apply an instant force, ignoring deltaTime
LimitVelocity //clamp the velocity of the triggerer to some value
}
public Action<LevelTrigger, Entity> OnTriggered;
private PhysicsBody physicsBody;
/// <summary>
/// Effects applied to entities that are inside the trigger
/// </summary>
private List<StatusEffect> statusEffects = new List<StatusEffect>();
/// <summary>
/// Attacks applied to entities that are inside the trigger
/// </summary>
private List<Attack> attacks = new List<Attack>();
private float cameraShake;
private Vector2 unrotatedForce;
private float forceFluctuationTimer, currentForceFluctuation = 1.0f;
private HashSet<Entity> triggerers = new HashSet<Entity>();
private TriggererType triggeredBy;
private float randomTriggerInterval;
private float randomTriggerProbability;
private float randomTriggerTimer;
private float triggeredTimer;
//how far away this trigger can activate other triggers from
private float triggerOthersDistance;
private HashSet<string> tags = new HashSet<string>();
//other triggers have to have at least one of these tags to trigger this one
private HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
/// <summary>
/// How long the trigger stays in the triggered state after triggerers have left
/// </summary>
private float stayTriggeredDelay;
public LevelTrigger ParentTrigger;
public Dictionary<Entity, Vector2> TriggererPosition
{
get;
private set;
}
private Vector2 worldPosition;
public Vector2 WorldPosition
{
get { return worldPosition; }
set
{
worldPosition = value;
physicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), physicsBody.Rotation);
}
}
public float Rotation
{
get { return physicsBody == null ? 0.0f : physicsBody.Rotation; }
set
{
if (physicsBody == null) return;
physicsBody.SetTransform(physicsBody.Position, value);
CalculateDirectionalForce();
}
}
public PhysicsBody PhysicsBody
{
get { return physicsBody; }
}
public float TriggerOthersDistance
{
get { return triggerOthersDistance; }
}
public IEnumerable<Entity> Triggerers
{
get { return triggerers.AsEnumerable(); }
}
public bool IsTriggered
{
get
{
return (triggerers.Count > 0 || triggeredTimer > 0.0f) &&
(ParentTrigger == null || ParentTrigger.IsTriggered);
}
}
public Vector2 Force
{
get;
private set;
}
/// <summary>
/// does the force diminish by distance
/// </summary>
public bool ForceFalloff
{
get;
private set;
}
public float ForceFluctuationInterval
{
get;
private set;
}
public float ForceFluctuationStrength
{
get;
private set;
}
private TriggerForceMode forceMode;
public TriggerForceMode ForceMode
{
get { return forceMode; }
}
/// <summary>
/// Stop applying forces to objects if they're moving faster than this
/// </summary>
public float ForceVelocityLimit
{
get;
private set;
}
public float ColliderRadius
{
get;
private set;
}
public bool UseNetworkSyncing
{
get;
private set;
}
public bool NeedsNetworkSyncing
{
get;
set;
}
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
{
TriggererPosition = new Dictionary<Entity, Vector2>();
worldPosition = position;
if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any())
{
physicsBody = new PhysicsBody(element, scale)
{
CollisionCategories = Physics.CollisionLevel,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall
};
physicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
physicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
physicsBody.FarseerBody.SetIsSensor(true);
physicsBody.FarseerBody.BodyType = BodyType.Static;
physicsBody.FarseerBody.BodyType = BodyType.Kinematic;
ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
physicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
}
cameraShake = element.GetAttributeFloat("camerashake", 0.0f);
stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f);
randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f);
randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f);
UseNetworkSyncing = element.GetAttributeBool("networksyncing", false);
unrotatedForce =
element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ?
element.GetAttributeVector2("force", Vector2.Zero) :
new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f);
ForceFluctuationInterval = element.GetAttributeFloat("forcefluctuationinterval", 0.01f);
ForceFluctuationStrength = Math.Max(element.GetAttributeFloat("forcefluctuationstrength", 0.0f), 0.0f);
ForceFalloff = element.GetAttributeBool("forcefalloff", true);
ForceVelocityLimit = ConvertUnits.ToSimUnits(element.GetAttributeFloat("forcevelocitylimit", float.MaxValue));
string forceModeStr = element.GetAttributeString("forcemode", "Force");
if (!Enum.TryParse(forceModeStr, out forceMode))
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + forceModeStr + "\" is not a valid force mode.");
}
CalculateDirectionalForce();
string triggeredByStr = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByStr, out triggeredBy))
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
}
UpdateCollisionCategories();
triggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
var tagsArray = element.GetAttributeStringArray("tags", new string[0]);
foreach (string tag in tagsArray)
{
tags.Add(tag.ToLower());
}
if (triggeredBy.HasFlag(TriggererType.OtherTrigger))
{
var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]);
foreach (string tag in otherTagsArray)
{
allowedOtherTriggerTags.Add(tag.ToLower());
}
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in "+ parentDebugName));
break;
case "attack":
case "damage":
var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName);
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
{
attack.Afflictions.Add(affliction, null);
}
attacks.Add(attack);
break;
}
}
forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval);
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
}
private void UpdateCollisionCategories()
{
if (physicsBody == null) return;
var collidesWith = Physics.CollisionNone;
if (triggeredBy.HasFlag(TriggererType.Character) || triggeredBy.HasFlag(TriggererType.Creature)) collidesWith |= Physics.CollisionCharacter;
if (triggeredBy.HasFlag(TriggererType.Item)) collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile;
if (triggeredBy.HasFlag(TriggererType.Submarine)) collidesWith |= Physics.CollisionWall;
physicsBody.CollidesWith = collidesWith;
}
private void CalculateDirectionalForce()
{
var ca = (float)Math.Cos(-Rotation);
var sa = (float)Math.Sin(-Rotation);
Force = new Vector2(
ca * unrotatedForce.X + sa * unrotatedForce.Y,
-sa * unrotatedForce.X + ca * unrotatedForce.Y);
}
private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return false;
if (entity is Character character)
{
if (character.CurrentHull != null) return false;
if (character.IsHuman)
{
if (!triggeredBy.HasFlag(TriggererType.Human)) return false;
}
else
{
if (!triggeredBy.HasFlag(TriggererType.Creature)) return false;
}
}
else if (entity is Item item)
{
if (item.CurrentHull != null) return false;
if (!triggeredBy.HasFlag(TriggererType.Item)) return false;
}
else if (entity is Submarine)
{
if (!triggeredBy.HasFlag(TriggererType.Submarine)) return false;
}
if (!triggerers.Contains(entity))
{
if (!IsTriggered)
{
OnTriggered?.Invoke(this, entity);
}
TriggererPosition[entity] = entity.WorldPosition;
triggerers.Add(entity);
}
return true;
}
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return;
if (entity is Character character &&
(!character.Enabled || character.Removed) &&
triggerers.Contains(entity))
{
TriggererPosition.Remove(entity);
triggerers.Remove(entity);
return;
}
//check if there are any other contacts with the entity
//(the OnSeparation callback happens when two fixtures separate,
//e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body)
ContactEdge contactEdge = fixtureA.Body.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.IsTouching)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) return;
}
contactEdge = contactEdge.Next;
}
if (triggerers.Contains(entity))
{
TriggererPosition.Remove(entity);
triggerers.Remove(entity);
}
}
private Entity GetEntity(Fixture fixture)
{
if (fixture.Body == null || fixture.Body.UserData == null) return null;
if (fixture.Body.UserData is Entity entity) return entity;
if (fixture.Body.UserData is Limb limb) return limb.character;
if (fixture.Body.UserData is SubmarineBody subBody) return subBody.Submarine;
return null;
}
/// <summary>
/// Another trigger was triggered, check if this one should react to it
/// </summary>
public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger)
{
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) return;
//check if the other trigger has appropriate tags
if (allowedOtherTriggerTags.Count > 0)
{
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) return;
}
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.triggerOthersDistance * otherTrigger.triggerOthersDistance)
{
bool wasAlreadyTriggered = IsTriggered;
triggeredTimer = stayTriggeredDelay;
if (!wasAlreadyTriggered)
{
OnTriggered?.Invoke(this, null);
}
}
}
public void Update(float deltaTime)
{
if (ParentTrigger != null && !ParentTrigger.IsTriggered) return;
triggerers.RemoveWhere(t => t.Removed);
bool isNotClient = true;
#if CLIENT
isNotClient = GameMain.Client == null;
#endif
if (!UseNetworkSyncing || isNotClient)
{
if (ForceFluctuationStrength > 0.0f)
{
//no need for force fluctuation (or network updates) if the trigger limits velocity and there are no triggerers
if (forceMode != TriggerForceMode.LimitVelocity || triggerers.Any())
{
forceFluctuationTimer += deltaTime;
if (forceFluctuationTimer > ForceFluctuationInterval)
{
NeedsNetworkSyncing = true;
currentForceFluctuation = Rand.Range(1.0f - ForceFluctuationStrength, 1.0f);
forceFluctuationTimer = 0.0f;
}
}
}
if (randomTriggerProbability > 0.0f)
{
randomTriggerTimer += deltaTime;
if (randomTriggerTimer > randomTriggerInterval)
{
if (Rand.Range(0.0f, 1.0f) < randomTriggerProbability)
{
NeedsNetworkSyncing = true;
triggeredTimer = stayTriggeredDelay;
}
randomTriggerTimer = 0.0f;
}
}
}
if (stayTriggeredDelay > 0.0f)
{
if (triggerers.Count == 0)
{
triggeredTimer -= deltaTime;
}
else
{
triggeredTimer = stayTriggeredDelay;
}
}
foreach (Entity triggerer in triggerers)
{
foreach (StatusEffect effect in statusEffects)
{
if (triggerer is Character)
{
effect.Apply(effect.type, deltaTime, triggerer, (Character)triggerer);
}
else if (triggerer is Item)
{
effect.Apply(effect.type, deltaTime, triggerer, ((Item)triggerer).AllPropertyObjects);
}
}
if (triggerer is IDamageable damageable)
{
foreach (Attack attack in attacks)
{
attack.DoDamage(null, damageable, WorldPosition, deltaTime, false);
}
}
else if (triggerer is Submarine submarine)
{
foreach (Attack attack in attacks)
{
float structureDamage = attack.GetStructureDamage(deltaTime);
if (structureDamage > 0.0f)
{
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage);
}
}
}
if (Force.LengthSquared() > 0.01f)
{
if (triggerer is Character character)
{
ApplyForce(character.AnimController.Collider, deltaTime);
foreach (Limb limb in character.AnimController.Limbs)
{
ApplyForce(limb.body, deltaTime);
}
}
else if (triggerer is Submarine submarine)
{
ApplyForce(submarine.SubBody.Body, deltaTime);
}
}
if (triggerer == Character.Controlled || triggerer == Character.Controlled?.Submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, cameraShake);
}
}
}
private void ApplyForce(PhysicsBody body, float deltaTime)
{
float distFactor = 1.0f;
if (ForceFalloff)
{
distFactor = 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(body.SimPosition, PhysicsBody.SimPosition)) / ColliderRadius;
if (distFactor < 0.0f) return;
}
switch (ForceMode)
{
case TriggerForceMode.Force:
if (ForceVelocityLimit < 1000.0f)
body.ApplyForce(Force * currentForceFluctuation * distFactor, ForceVelocityLimit);
else
body.ApplyForce(Force * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
break;
case TriggerForceMode.Acceleration:
if (ForceVelocityLimit < 1000.0f)
body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, ForceVelocityLimit);
else
body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
break;
case TriggerForceMode.Impulse:
if (ForceVelocityLimit < 1000.0f)
body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: ForceVelocityLimit);
else
body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
break;
case TriggerForceMode.LimitVelocity:
float maxVel = ForceVelocityLimit * currentForceFluctuation * distFactor;
if (body.LinearVelocity.LengthSquared() > maxVel * maxVel)
{
body.ApplyForce(
Vector2.Normalize(-body.LinearVelocity) *
Force.Length() * body.Mass * currentForceFluctuation * distFactor,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
break;
}
}
public Vector2 GetWaterFlowVelocity(Vector2 viewPosition)
{
Vector2 baseVel = GetWaterFlowVelocity();
if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero;
float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
float dist = Vector2.Distance(viewPosition, WorldPosition);
if (dist > triggerSize) return Vector2.Zero;
return baseVel * (1.0f - dist / triggerSize);
}
public Vector2 GetWaterFlowVelocity()
{
if (Force == Vector2.Zero) return Vector2.Zero;
Vector2 vel = Force;
if (ForceMode == TriggerForceMode.Acceleration)
{
vel *= 1000.0f;
}
else if (ForceMode == TriggerForceMode.Impulse)
{
vel /= (float)Timing.Step;
}
return vel.ClampLength(ConvertUnits.ToDisplayUnits(ForceVelocityLimit)) * currentForceFluctuation;
}
public void ServerWrite(IWriteMessage msg, Client c)
{
if (ForceFluctuationStrength > 0.0f)
{
msg.WriteRangedSingle(MathHelper.Clamp(currentForceFluctuation, 0.0f, 1.0f), 0.0f, 1.0f, 8);
}
if (stayTriggeredDelay > 0.0f)
{
msg.WriteRangedSingle(MathHelper.Clamp(triggeredTimer, 0.0f, stayTriggeredDelay), 0.0f, stayTriggeredDelay, 16);
}
}
}
}