(61d00a474) v0.9.7.1
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class CoreEntityPrefab : MapEntityPrefab
|
||||
{
|
||||
public static readonly PrefabCollection<CoreEntityPrefab> Prefabs = new PrefabCollection<CoreEntityPrefab>();
|
||||
|
||||
private bool disposed = false;
|
||||
public override void Dispose()
|
||||
{
|
||||
if (disposed) { return; }
|
||||
disposed = true;
|
||||
Prefabs.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class DummyFireSource : FireSource
|
||||
{
|
||||
private Vector2 maxSize;
|
||||
|
||||
public bool Removed
|
||||
{
|
||||
get { return removed; }
|
||||
}
|
||||
|
||||
public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : base(worldPosition, spawningHull, isNetworkMessage)
|
||||
{
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public override float DamageRange
|
||||
{
|
||||
get { return 5f; }
|
||||
}
|
||||
|
||||
protected override void LimitSize()
|
||||
{
|
||||
if (hull == null) return;
|
||||
|
||||
position.X = Math.Max(hull.Rect.X, position.X);
|
||||
position.Y = Math.Min(hull.Rect.Y, position.Y);
|
||||
|
||||
size.X = Math.Min(maxSize.X, size.X);
|
||||
size.Y = Math.Min(maxSize.Y, size.Y);
|
||||
}
|
||||
|
||||
protected override void AdjustXPos(float growModifier, float deltaTime)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void ReduceOxygen(float deltaTime)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class Entity : ISpatialEntity
|
||||
{
|
||||
public const ushort NullEntityID = 0;
|
||||
public const ushort EntitySpawnerID = ushort.MaxValue;
|
||||
|
||||
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
|
||||
public static List<Entity> GetEntityList()
|
||||
{
|
||||
return dictionary.Values.ToList();
|
||||
}
|
||||
|
||||
public static EntitySpawner Spawner;
|
||||
|
||||
private ushort id;
|
||||
|
||||
protected AITarget aiTarget;
|
||||
|
||||
private bool idFreed;
|
||||
|
||||
public virtual bool Removed
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool IdFreed
|
||||
{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (value == EntitySpawnerID)
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot set the ID of an entity to " + EntitySpawnerID +
|
||||
"! The value is reserved for EntitySpawner.\n" + Environment.StackTrace);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Vector2 SimPosition
|
||||
{
|
||||
get { return Vector2.Zero; }
|
||||
}
|
||||
|
||||
public virtual Vector2 Position
|
||||
{
|
||||
get { return Vector2.Zero; }
|
||||
}
|
||||
|
||||
public virtual Vector2 WorldPosition
|
||||
{
|
||||
get { return Submarine == null ? Position : Submarine.Position + Position; }
|
||||
}
|
||||
|
||||
public virtual Vector2 DrawPosition
|
||||
{
|
||||
get { return Submarine == null ? Position : Submarine.DrawPosition + Position; }
|
||||
}
|
||||
|
||||
public Submarine Submarine
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public AITarget AiTarget
|
||||
{
|
||||
get { return aiTarget; }
|
||||
}
|
||||
|
||||
public double SpawnTime
|
||||
{
|
||||
get { return spawnTime; }
|
||||
}
|
||||
|
||||
private readonly double spawnTime;
|
||||
|
||||
public Entity(Submarine submarine)
|
||||
{
|
||||
this.Submarine = submarine;
|
||||
spawnTime = Timing.TotalTime;
|
||||
|
||||
//give a unique ID
|
||||
id = this is EntitySpawner ?
|
||||
EntitySpawnerID :
|
||||
FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset);
|
||||
|
||||
dictionary.Add(id, this);
|
||||
}
|
||||
|
||||
public static ushort FindFreeID(ushort idOffset = 0)
|
||||
{
|
||||
//ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values
|
||||
if (dictionary.Count >= ushort.MaxValue - 2)
|
||||
{
|
||||
throw new Exception("Maximum amount of entities (" + (ushort.MaxValue - 1) + ") reached!");
|
||||
}
|
||||
|
||||
idOffset = Math.Max(idOffset, (ushort)1);
|
||||
bool IDfound;
|
||||
ushort id = idOffset;
|
||||
do
|
||||
{
|
||||
id += 1;
|
||||
IDfound = dictionary.ContainsKey(id);
|
||||
} while (IDfound);
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find an entity based on the ID
|
||||
/// </summary>
|
||||
public static Entity FindEntityByID(ushort ID)
|
||||
{
|
||||
Entity matchingEntity;
|
||||
dictionary.TryGetValue(ID, out matchingEntity);
|
||||
|
||||
return matchingEntity;
|
||||
}
|
||||
|
||||
public static void RemoveAll()
|
||||
{
|
||||
List<Entity> list = new List<Entity>(dictionary.Values);
|
||||
foreach (Entity e in list)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.Remove();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while removing entity \"" + e.ToString() + "\"", exception);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Entity.RemoveAll:Exception" + e.ToString(),
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace);
|
||||
}
|
||||
}
|
||||
StringBuilder errorMsg = new StringBuilder();
|
||||
if (dictionary.Count > 0)
|
||||
{
|
||||
errorMsg.AppendLine("Some entities were not removed in Entity.RemoveAll:");
|
||||
foreach (Entity e in dictionary.Values)
|
||||
{
|
||||
errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.id + ")");
|
||||
}
|
||||
}
|
||||
if (Item.ItemList.Count > 0)
|
||||
{
|
||||
errorMsg.AppendLine("Some items were not removed in Entity.RemoveAll:");
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
errorMsg.AppendLine(" - " + item.Name + "(ID " + item.id + ")");
|
||||
}
|
||||
|
||||
var items = new List<Item>(Item.ItemList);
|
||||
foreach (Item item in items)
|
||||
{
|
||||
try
|
||||
{
|
||||
item.Remove();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while removing item \"" + item.ToString() + "\"", exception);
|
||||
}
|
||||
}
|
||||
Item.ItemList.Clear();
|
||||
}
|
||||
if (Character.CharacterList.Count > 0)
|
||||
{
|
||||
errorMsg.AppendLine("Some characters were not removed in Entity.RemoveAll:");
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
errorMsg.AppendLine(" - " + character.Name + "(ID " + character.id + ")");
|
||||
}
|
||||
|
||||
var characters = new List<Character>(Character.CharacterList);
|
||||
foreach (Character character in characters)
|
||||
{
|
||||
try
|
||||
{
|
||||
character.Remove();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while removing character \"" + character.ToString() + "\"", exception);
|
||||
}
|
||||
}
|
||||
Character.CharacterList.Clear();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(errorMsg.ToString()))
|
||||
{
|
||||
foreach (string errorLine in errorMsg.ToString().Split('\n'))
|
||||
{
|
||||
DebugConsole.ThrowError(errorLine);
|
||||
}
|
||||
GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg.ToString());
|
||||
}
|
||||
|
||||
dictionary.Clear();
|
||||
Hull.EntityGrids.Clear();
|
||||
Spawner?.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the entity from the entity dictionary and frees up the ID it was using.
|
||||
/// </summary>
|
||||
public void FreeID()
|
||||
{
|
||||
DebugConsole.Log("Removing entity " + ToString() + " (" + ID + ") from entity dictionary.");
|
||||
if (!dictionary.TryGetValue(ID, out Entity existingEntity))
|
||||
{
|
||||
DebugConsole.Log("Entity " + ToString() + " (" + ID + ") not present in entity dictionary.");
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Entity.FreeID:EntityNotFound" + ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace);
|
||||
}
|
||||
else if (existingEntity != this)
|
||||
{
|
||||
DebugConsole.Log("Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")");
|
||||
GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")");
|
||||
|
||||
foreach (var keyValuePair in dictionary.Where(kvp => kvp.Value == this).ToList())
|
||||
{
|
||||
dictionary.Remove(keyValuePair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
dictionary.Remove(ID);
|
||||
idFreed = true;
|
||||
}
|
||||
|
||||
public virtual void Remove()
|
||||
{
|
||||
if (!idFreed) FreeID();
|
||||
Removed = true;
|
||||
}
|
||||
|
||||
public static void DumpIds(int count, string filename)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filename))
|
||||
{
|
||||
File.WriteAllLines(filename, lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class EntityGrid
|
||||
{
|
||||
private List<MapEntity> allEntities;
|
||||
private List<MapEntity>[,] entities;
|
||||
|
||||
private readonly Rectangle limits;
|
||||
|
||||
private readonly float cellSize;
|
||||
|
||||
public readonly Submarine Submarine;
|
||||
|
||||
public Rectangle WorldRect
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Submarine == null)
|
||||
{
|
||||
return limits;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Rectangle(
|
||||
(int)(limits.X + Submarine.WorldPosition.X),
|
||||
(int)(limits.Y + Submarine.WorldPosition.Y),
|
||||
limits.Width, limits.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EntityGrid(Submarine submarine, float cellSize)
|
||||
{
|
||||
//make the grid slightly larger than the borders of the submarine,
|
||||
//because docking ports may create gaps and hulls outside the borders
|
||||
int padding = 128;
|
||||
|
||||
this.limits = new Rectangle(
|
||||
submarine.Borders.X - padding,
|
||||
submarine.Borders.Y + padding,
|
||||
submarine.Borders.Width + padding * 2,
|
||||
submarine.Borders.Height + padding * 2);
|
||||
this.Submarine = submarine;
|
||||
this.cellSize = cellSize;
|
||||
InitializeGrid();
|
||||
}
|
||||
|
||||
public EntityGrid(Rectangle worldRect, float cellSize)
|
||||
{
|
||||
this.limits = worldRect;
|
||||
this.cellSize = cellSize;
|
||||
InitializeGrid();
|
||||
}
|
||||
|
||||
private void InitializeGrid()
|
||||
{
|
||||
allEntities = new List<MapEntity>();
|
||||
entities = new List<MapEntity>[(int)Math.Ceiling(limits.Width / cellSize), (int)Math.Ceiling(limits.Height / cellSize)];
|
||||
for (int x = 0; x < entities.GetLength(0); x++)
|
||||
{
|
||||
for (int y = 0; y < entities.GetLength(1); y++)
|
||||
{
|
||||
entities[x, y] = new List<MapEntity>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void InsertEntity(MapEntity entity)
|
||||
{
|
||||
Rectangle rect = entity.Rect;
|
||||
//if (Submarine.Loaded != null) rect.Offset(-Submarine.HiddenSubPosition);
|
||||
Rectangle indices = GetIndices(rect);
|
||||
|
||||
if (indices.Width < 0 || indices.X >= entities.GetLength(0) ||
|
||||
indices.Height < 0 || indices.Y >= entities.GetLength(1))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in EntityGrid.InsertEntity: " + entity + " is outside the grid");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int x = Math.Max(indices.X, 0); x <= Math.Min(indices.Width, entities.GetLength(0) - 1); x++)
|
||||
{
|
||||
for (int y = Math.Max(indices.Y, 0); y <= Math.Min(indices.Height, entities.GetLength(1) - 1); y++)
|
||||
{
|
||||
entities[x, y].Add(entity);
|
||||
}
|
||||
}
|
||||
allEntities.Add(entity);
|
||||
}
|
||||
|
||||
public void RemoveEntity(MapEntity entity)
|
||||
{
|
||||
for (int x = 0; x < entities.GetLength(0); x++)
|
||||
{
|
||||
for (int y = 0; y < entities.GetLength(1); y++)
|
||||
{
|
||||
if (entities[x, y].Contains(entity)) entities[x, y].Remove(entity);
|
||||
}
|
||||
}
|
||||
allEntities.Remove(entity);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
for (int x = 0; x < entities.GetLength(0); x++)
|
||||
{
|
||||
for (int y = 0; y < entities.GetLength(1); y++)
|
||||
{
|
||||
entities[x, y].Clear();
|
||||
}
|
||||
}
|
||||
allEntities.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<MapEntity> GetAllEntities()
|
||||
{
|
||||
return allEntities;
|
||||
}
|
||||
|
||||
public List<MapEntity> GetEntities(Vector2 position)
|
||||
{
|
||||
if (!MathUtils.IsValid(position)) return null;
|
||||
|
||||
if (Submarine != null) position -= Submarine.HiddenSubPosition;
|
||||
Point indices = GetIndices(position);
|
||||
if (indices.X < 0 || indices.Y < 0 || indices.X >= entities.GetLength(0) || indices.Y >= entities.GetLength(1))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return entities[indices.X, indices.Y];
|
||||
}
|
||||
|
||||
public Rectangle GetIndices(Rectangle rect)
|
||||
{
|
||||
Rectangle indices = Rectangle.Empty;
|
||||
indices.X = (int)Math.Floor((rect.X - limits.X) / cellSize);
|
||||
indices.Y = (int)Math.Floor((limits.Y - rect.Y) / cellSize);
|
||||
|
||||
indices.Width = (int)Math.Floor((rect.Right - limits.X) / cellSize);
|
||||
indices.Height = (int)Math.Floor((limits.Y - (rect.Y - rect.Height)) / cellSize);
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
public Point GetIndices(Vector2 position)
|
||||
{
|
||||
return new Point(
|
||||
(int)Math.Floor((position.X - limits.X) / cellSize),
|
||||
(int)Math.Floor((limits.Y - position.Y) / cellSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
using Barotrauma.Items.Components;
|
||||
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 Explosion
|
||||
{
|
||||
private static readonly List<Triplet<Explosion, Vector2, float>> prevExplosions = new List<Triplet<Explosion, Vector2, float>>();
|
||||
|
||||
private readonly Attack attack;
|
||||
|
||||
private readonly float force;
|
||||
|
||||
private readonly float cameraShake, cameraShakeRange;
|
||||
|
||||
private readonly Color screenColor;
|
||||
private readonly float screenColorRange, screenColorDuration;
|
||||
|
||||
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
|
||||
private float flashDuration;
|
||||
private float? flashRange;
|
||||
private readonly string decal;
|
||||
private readonly float decalSize;
|
||||
|
||||
public float EmpStrength { get; set; }
|
||||
|
||||
public Explosion(float range, float force, float damage, float structureDamage, float empStrength = 0.0f)
|
||||
{
|
||||
attack = new Attack(damage, 0.0f, 0.0f, structureDamage, range)
|
||||
{
|
||||
SeverLimbsProbability = 1.0f
|
||||
};
|
||||
this.force = force;
|
||||
this.EmpStrength = empStrength;
|
||||
sparks = true;
|
||||
shockwave = true;
|
||||
smoke = true;
|
||||
flames = true;
|
||||
underwaterBubble = true;
|
||||
}
|
||||
|
||||
public Explosion(XElement element, string parentDebugName)
|
||||
{
|
||||
attack = new Attack(element, parentDebugName + ", Explosion");
|
||||
|
||||
force = element.GetAttributeFloat("force", 0.0f);
|
||||
|
||||
sparks = element.GetAttributeBool("sparks", true);
|
||||
shockwave = element.GetAttributeBool("shockwave", true);
|
||||
flames = element.GetAttributeBool("flames", true);
|
||||
underwaterBubble = element.GetAttributeBool("underwaterbubble", true);
|
||||
smoke = element.GetAttributeBool("smoke", true);
|
||||
|
||||
flash = element.GetAttributeBool("flash", true);
|
||||
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
|
||||
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
|
||||
|
||||
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
|
||||
|
||||
decal = element.GetAttributeString("decal", "");
|
||||
decalSize = element.GetAttributeFloat("decalSize", 1.0f);
|
||||
|
||||
cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f);
|
||||
cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range);
|
||||
|
||||
screenColorRange = element.GetAttributeFloat("screencolorrange", attack.Range * 0.1f);
|
||||
screenColor = element.GetAttributeColor("screencolor", Color.Transparent);
|
||||
screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f);
|
||||
}
|
||||
|
||||
public void DisableParticles()
|
||||
{
|
||||
sparks = false;
|
||||
shockwave = false;
|
||||
smoke = false;
|
||||
flash = false;
|
||||
flames = false;
|
||||
underwaterBubble = false;
|
||||
}
|
||||
|
||||
public List<Triplet<Explosion, Vector2, float>> GetRecentExplosions(float maxSecondsAgo)
|
||||
{
|
||||
return prevExplosions.FindAll(e => e.Third >= Timing.TotalTime - maxSecondsAgo);
|
||||
}
|
||||
|
||||
public void Explode(Vector2 worldPosition, Entity damageSource, Character attacker = null)
|
||||
{
|
||||
prevExplosions.Add(new Triplet<Explosion, Vector2, float>(this, worldPosition, (float)Timing.TotalTime));
|
||||
if (prevExplosions.Count > 100)
|
||||
{
|
||||
prevExplosions.RemoveAt(0);
|
||||
}
|
||||
|
||||
Hull hull = Hull.FindHull(worldPosition);
|
||||
|
||||
ExplodeProjSpecific(worldPosition, hull);
|
||||
|
||||
float displayRange = attack.Range;
|
||||
|
||||
Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position;
|
||||
float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f;
|
||||
GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f);
|
||||
#if CLIENT
|
||||
if (screenColor != Color.Transparent)
|
||||
{
|
||||
Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f));
|
||||
Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (displayRange < 0.1f) { return; }
|
||||
|
||||
if (attack.GetStructureDamage(1.0f) > 0.0f)
|
||||
{
|
||||
RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attacker);
|
||||
}
|
||||
|
||||
if (EmpStrength > 0.0f)
|
||||
{
|
||||
float displayRangeSqr = displayRange * displayRange;
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
|
||||
if (distSqr > displayRangeSqr) continue;
|
||||
|
||||
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
|
||||
|
||||
//damage repairable power-consuming items
|
||||
var powered = item.GetComponent<Powered>();
|
||||
if (powered == null || !powered.VulnerableToEMP) continue;
|
||||
if (item.Repairables.Any())
|
||||
{
|
||||
item.Condition -= 100 * EmpStrength * distFactor;
|
||||
}
|
||||
|
||||
//discharge batteries
|
||||
var powerContainer = item.GetComponent<PowerContainer>();
|
||||
if (powerContainer != null)
|
||||
{
|
||||
powerContainer.Charge -= powerContainer.Capacity * EmpStrength * distFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DamageCharacters(worldPosition, attack, force, damageSource, attacker);
|
||||
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
if (flames)
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; }
|
||||
|
||||
//don't apply OnFire effects if the item is inside a fireproof container
|
||||
//(or if it's inside a container that's inside a fireproof container, etc)
|
||||
Item container = item.Container;
|
||||
bool fireProof = false;
|
||||
while (container != null)
|
||||
{
|
||||
if (container.FireProof) { fireProof = true; break; }
|
||||
container = container.Container;
|
||||
}
|
||||
|
||||
if (fireProof || Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; }
|
||||
|
||||
item.ApplyStatusEffects(ActionType.OnFire, 1.0f);
|
||||
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
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.Condition -= damageAmount * distFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
|
||||
|
||||
|
||||
public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
|
||||
{
|
||||
if (attack.Range <= 0.0f) return;
|
||||
|
||||
//long range for the broad distance check, because large characters may still be in range even if their collider isn't
|
||||
float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f);
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!c.Enabled ||
|
||||
Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange ||
|
||||
Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 explosionPos = worldPosition;
|
||||
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
|
||||
|
||||
Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false);
|
||||
bool underWater = hull == null || explosionPos.Y < hull.Surface;
|
||||
|
||||
explosionPos = ConvertUnits.ToSimUnits(explosionPos);
|
||||
|
||||
Dictionary<Limb, float> distFactors = new Dictionary<Limb, float>();
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
float dist = Vector2.Distance(limb.WorldPosition, worldPosition);
|
||||
|
||||
//calculate distance from the "outer surface" of the physics body
|
||||
//doesn't take the rotation of the limb into account, but should be accurate enough for this purpose
|
||||
float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius);
|
||||
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
|
||||
|
||||
if (dist > attack.Range) { continue; }
|
||||
|
||||
float distFactor = 1.0f - dist / attack.Range;
|
||||
|
||||
//solid obstacles between the explosion and the limb reduce the effect of the explosion by 90%
|
||||
if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) distFactor *= 0.1f;
|
||||
|
||||
distFactors.Add(limb, distFactor);
|
||||
|
||||
List<Affliction> modifiedAfflictions = new List<Affliction>();
|
||||
foreach (Affliction affliction in attack.Afflictions.Keys)
|
||||
{
|
||||
modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length));
|
||||
}
|
||||
c.LastDamageSource = damageSource;
|
||||
if (attacker == null)
|
||||
{
|
||||
if (damageSource is Item item)
|
||||
{
|
||||
attacker = item.GetComponent<Projectile>()?.User;
|
||||
if (attacker == null) attacker = item.GetComponent<MeleeWeapon>()?.User;
|
||||
}
|
||||
}
|
||||
|
||||
//use a position slightly from the limb's position towards the explosion
|
||||
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
|
||||
Vector2 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f;
|
||||
c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker);
|
||||
|
||||
if (attack.StatusEffects != null && attack.StatusEffects.Any())
|
||||
{
|
||||
attack.SetUser(attacker);
|
||||
var statusEffectTargets = new List<ISerializableEntity>() { c, limb };
|
||||
foreach (StatusEffect statusEffect in attack.StatusEffects)
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets);
|
||||
statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets);
|
||||
statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets);
|
||||
}
|
||||
}
|
||||
|
||||
if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f))
|
||||
{
|
||||
Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition);
|
||||
if (!MathUtils.IsValid(limbDiff)) limbDiff = Rand.Vector(1.0f);
|
||||
Vector2 impulse = limbDiff * distFactor * force;
|
||||
Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius;
|
||||
limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
//sever joints
|
||||
if (c.IsDead && attack.SeverLimbsProbability > 0.0f)
|
||||
{
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
if (!distFactors.ContainsKey(limb)) { continue; }
|
||||
if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb])
|
||||
{
|
||||
c.TrySeverLimbJoints(limb, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
List<Structure> structureList = new List<Structure>();
|
||||
float dist = 600.0f;
|
||||
foreach (MapEntity entity in MapEntity.mapEntityList)
|
||||
{
|
||||
if (!(entity is Structure structure)) { continue; }
|
||||
|
||||
if (structure.HasBody &&
|
||||
!structure.IsPlatform &&
|
||||
Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f)
|
||||
{
|
||||
structureList.Add(structure);
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<Structure, float> damagedStructures = new Dictionary<Structure, float>();
|
||||
foreach (Structure structure in structureList)
|
||||
{
|
||||
for (int i = 0; i < structure.SectionCount; i++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return damagedStructures;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#if CLIENT
|
||||
using Barotrauma.Sounds;
|
||||
using Barotrauma.Lights;
|
||||
using Barotrauma.Particles;
|
||||
#endif
|
||||
using FarseerPhysics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class FireSource : ISpatialEntity
|
||||
{
|
||||
const float OxygenConsumption = 50.0f;
|
||||
const float GrowSpeed = 20.0f;
|
||||
|
||||
protected Hull hull;
|
||||
|
||||
protected Vector2 position;
|
||||
protected Vector2 size;
|
||||
|
||||
private readonly Submarine submarine;
|
||||
public Submarine Submarine => submarine;
|
||||
|
||||
protected bool removed;
|
||||
|
||||
#if CLIENT
|
||||
private List<Decal> burnDecals = new List<Decal>();
|
||||
#endif
|
||||
|
||||
public Vector2 Position
|
||||
{
|
||||
get { return position; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
|
||||
position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 WorldPosition
|
||||
{
|
||||
get { return Submarine == null ? position : Submarine.Position + position; }
|
||||
}
|
||||
|
||||
public Vector2 SimPosition => ConvertUnits.ToSimUnits(Position);
|
||||
|
||||
public Vector2 Size
|
||||
{
|
||||
get { return size; }
|
||||
set
|
||||
{
|
||||
if (value == size) return;
|
||||
|
||||
Vector2 sizeChange = value - size;
|
||||
|
||||
size = value;
|
||||
position.X -= sizeChange.X * 0.5f;
|
||||
LimitSize();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual float DamageRange
|
||||
{
|
||||
get { return (float)Math.Sqrt(size.X) * 20.0f; }
|
||||
}
|
||||
|
||||
public Hull Hull
|
||||
{
|
||||
get { return hull; }
|
||||
}
|
||||
|
||||
public FireSource(Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false)
|
||||
{
|
||||
hull = Hull.FindHull(worldPosition, spawningHull);
|
||||
if (hull == null || worldPosition.Y < hull.WorldSurface) return;
|
||||
|
||||
#if CLIENT
|
||||
if (!isNetworkMessage && GameMain.Client != null) return;
|
||||
#endif
|
||||
|
||||
hull.AddFireSource(this);
|
||||
|
||||
position = worldPosition - new Vector2(-5.0f, 5.0f);
|
||||
if (hull.Submarine != null)
|
||||
{
|
||||
submarine = hull.Submarine;
|
||||
position -= Submarine.Position;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
lightSource = new LightSource(this.position, 50.0f, new Color(1.0f, 0.9f, 0.7f), hull?.Submarine);
|
||||
#endif
|
||||
|
||||
size = new Vector2(10.0f, 10.0f);
|
||||
}
|
||||
|
||||
protected virtual void LimitSize()
|
||||
{
|
||||
if (hull == null) return;
|
||||
|
||||
position.X = Math.Max(hull.Rect.X, position.X);
|
||||
position.Y = Math.Min(hull.Rect.Y, position.Y);
|
||||
|
||||
size.X = Math.Min(hull.Rect.Width - (position.X - hull.Rect.X), size.X);
|
||||
size.Y = Math.Min(hull.Rect.Height - (hull.Rect.Y - position.Y), size.Y);
|
||||
}
|
||||
|
||||
public static void UpdateAll(List<FireSource> fireSources, float deltaTime)
|
||||
{
|
||||
for (int i = fireSources.Count - 1; i >= 0; i--)
|
||||
{
|
||||
fireSources[i].Update(deltaTime);
|
||||
}
|
||||
|
||||
//combine overlapping fires
|
||||
for (int i = fireSources.Count - 1; i >= 0; i--)
|
||||
{
|
||||
for (int j = i - 1; j >= 0; j--)
|
||||
{
|
||||
i = Math.Min(i, fireSources.Count - 1);
|
||||
j = Math.Min(j, i - 1);
|
||||
|
||||
if (!fireSources[i].CheckOverLap(fireSources[j])) continue;
|
||||
|
||||
float leftEdge = Math.Min(fireSources[i].position.X, fireSources[j].position.X);
|
||||
|
||||
fireSources[j].size.X =
|
||||
Math.Max(fireSources[i].position.X + fireSources[i].size.X, fireSources[j].position.X + fireSources[j].size.X)
|
||||
- leftEdge;
|
||||
|
||||
fireSources[j].position.X = leftEdge;
|
||||
|
||||
#if CLIENT
|
||||
fireSources[j].burnDecals.AddRange(fireSources[i].burnDecals);
|
||||
fireSources[j].burnDecals.Sort((d1, d2) => { return Math.Sign(d1.WorldPosition.X - d2.WorldPosition.X); });
|
||||
#endif
|
||||
|
||||
fireSources[i].Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckOverLap(FireSource fireSource)
|
||||
{
|
||||
return !(position.X > fireSource.position.X + fireSource.size.X ||
|
||||
position.X + size.X < fireSource.position.X);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
//the firesource will start to shrink if oxygen percentage is below 10
|
||||
float growModifier = Math.Min((hull.OxygenPercentage / 10.0f) - 1.0f, 1.0f);
|
||||
|
||||
DamageCharacters(deltaTime);
|
||||
DamageItems(deltaTime);
|
||||
|
||||
if (hull.WaterVolume > 0.0f)
|
||||
{
|
||||
HullWaterExtinguish(deltaTime);
|
||||
if (removed) { return; }
|
||||
}
|
||||
|
||||
ReduceOxygen(deltaTime);
|
||||
|
||||
AdjustXPos(growModifier, deltaTime);
|
||||
|
||||
size.X += GrowSpeed * growModifier * deltaTime;
|
||||
size.Y = MathHelper.Clamp(size.Y + GrowSpeed * growModifier * deltaTime, 10.0f, 50.0f);
|
||||
|
||||
if (size.X > 50.0f)
|
||||
{
|
||||
this.position.Y = MathHelper.Lerp(this.position.Y, hull.Rect.Y - hull.Rect.Height + size.Y, deltaTime);
|
||||
}
|
||||
|
||||
LimitSize();
|
||||
|
||||
UpdateProjSpecific(growModifier);
|
||||
|
||||
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
Remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ReduceOxygen(float deltaTime)
|
||||
{
|
||||
hull.Oxygen -= size.X * deltaTime * OxygenConsumption;
|
||||
}
|
||||
|
||||
protected virtual void AdjustXPos(float growModifier, float deltaTime)
|
||||
{
|
||||
position.X -= GrowSpeed * growModifier * 0.5f * deltaTime;
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float growModifier);
|
||||
|
||||
private void OnChangeHull(Vector2 pos, Hull particleHull)
|
||||
{
|
||||
if (particleHull == hull || particleHull == null) return;
|
||||
|
||||
//hull already has a firesource roughly at the particles position -> don't create a new one
|
||||
if (particleHull.FireSources.Find(fs => pos.X > fs.position.X - 100.0f && pos.X < fs.position.X + fs.size.X + 100.0f) != null) return;
|
||||
|
||||
new FireSource(new Vector2(pos.X, particleHull.WorldRect.Y - particleHull.Rect.Height + 5.0f));
|
||||
}
|
||||
|
||||
private void DamageCharacters(float deltaTime)
|
||||
{
|
||||
if (size.X <= 0.0f) return;
|
||||
|
||||
for (int i = 0; i < Character.CharacterList.Count; i++)
|
||||
{
|
||||
Character c = Character.CharacterList[i];
|
||||
if (c.AnimController.CurrentHull == null || c.IsDead) continue;
|
||||
|
||||
if (!IsInDamageRange(c, DamageRange)) continue;
|
||||
|
||||
float dmg = (float)Math.Sqrt(size.X) * deltaTime / c.AnimController.Limbs.Length;
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
c.LastDamageSource = null;
|
||||
c.DamageLimb(WorldPosition, limb, new List<Affliction>() { AfflictionPrefab.Burn.Instantiate(dmg) }, 0.0f, false, 0.0f);
|
||||
}
|
||||
c.ApplyStatusEffects(ActionType.OnFire, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInDamageRange(Character c, float damageRange)
|
||||
{
|
||||
if (c.Position.X < position.X - damageRange || c.Position.X > position.X + size.X + damageRange) return false;
|
||||
if (c.Position.Y < position.Y - size.Y || c.Position.Y > hull.Rect.Y) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsInDamageRange(Vector2 worldPosition, float damageRange)
|
||||
{
|
||||
if (worldPosition.X < WorldPosition.X - damageRange || worldPosition.X > WorldPosition.X + size.X + damageRange) return false;
|
||||
if (worldPosition.Y < WorldPosition.Y - size.Y || worldPosition.Y > hull.WorldRect.Y) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DamageItems(float deltaTime)
|
||||
{
|
||||
if (size.X <= 0.0f) return;
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null) return;
|
||||
#endif
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue;
|
||||
|
||||
//don't apply OnFire effects if the item is inside a fireproof container
|
||||
//(or if it's inside a container that's inside a fireproof container, etc)
|
||||
Item container = item.Container;
|
||||
while (container != null)
|
||||
{
|
||||
if (container.FireProof) return;
|
||||
container = container.Container;
|
||||
}
|
||||
|
||||
float range = (float)Math.Sqrt(size.X) * 10.0f;
|
||||
if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) continue;
|
||||
if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) continue;
|
||||
|
||||
item.ApplyStatusEffects(ActionType.OnFire, deltaTime);
|
||||
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HullWaterExtinguish(float deltaTime)
|
||||
{
|
||||
//the higher the surface of the water is relative to the firesource, the faster it puts out the fire
|
||||
float extinguishAmount = (hull.Surface - (position.Y - size.Y)) * deltaTime;
|
||||
|
||||
if (extinguishAmount < 0.0f) return;
|
||||
|
||||
#if CLIENT
|
||||
float steamCount = Rand.Range(-5.0f, Math.Min(extinguishAmount * 100.0f, 10));
|
||||
for (int i = 0; i < steamCount; i++)
|
||||
{
|
||||
Vector2 spawnPos = new Vector2(
|
||||
WorldPosition.X + Rand.Range(0.0f, size.X),
|
||||
WorldPosition.Y + 10.0f);
|
||||
|
||||
Vector2 speed = new Vector2((spawnPos.X - (WorldPosition.X + size.X / 2.0f)), (float)Math.Sqrt(size.X) * Rand.Range(20.0f, 25.0f));
|
||||
|
||||
var particle = GameMain.ParticleManager.CreateParticle("steam",
|
||||
spawnPos, speed, 0.0f, hull);
|
||||
}
|
||||
#endif
|
||||
extinguishAmount = Math.Min(size.X, extinguishAmount);
|
||||
|
||||
position.X += extinguishAmount / 2.0f;
|
||||
size.X -= extinguishAmount;
|
||||
|
||||
//evaporate some of the water
|
||||
hull.WaterVolume -= extinguishAmount;
|
||||
|
||||
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public void Extinguish(float deltaTime, float amount)
|
||||
{
|
||||
float extinguishAmount = amount * deltaTime;
|
||||
|
||||
#if CLIENT
|
||||
float steamCount = Rand.Range(-5.0f, (float)Math.Sqrt(amount));
|
||||
for (int i = 0; i < steamCount; i++)
|
||||
{
|
||||
Vector2 spawnPos = new Vector2(Rand.Range(position.X, position.X + size.X), Rand.Range(position.Y - size.Y, position.Y) + 10.0f);
|
||||
|
||||
Vector2 speed = new Vector2((spawnPos.X - (position.X + size.X / 2.0f)), (float)Math.Sqrt(size.X) * Rand.Range(20.0f, 25.0f));
|
||||
|
||||
var particle = GameMain.ParticleManager.CreateParticle("steam",
|
||||
spawnPos, speed, 0.0f, hull);
|
||||
}
|
||||
#endif
|
||||
extinguishAmount = Math.Min(size.X, extinguishAmount);
|
||||
|
||||
position.X += extinguishAmount / 2.0f;
|
||||
size.X -= extinguishAmount;
|
||||
|
||||
hull.WaterVolume -= extinguishAmount;
|
||||
|
||||
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public void Extinguish(float deltaTime, float amount, Vector2 worldPosition)
|
||||
{
|
||||
if (IsInDamageRange(worldPosition, 100.0f))
|
||||
{
|
||||
Extinguish(deltaTime, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
#if CLIENT
|
||||
lightSource?.Remove();
|
||||
lightSource = null;
|
||||
|
||||
foreach (Decal d in burnDecals)
|
||||
{
|
||||
d.StopFadeIn();
|
||||
}
|
||||
#endif
|
||||
hull?.RemoveFire(this);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,772 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Gap : MapEntity
|
||||
{
|
||||
public static List<Gap> GapList = new List<Gap>();
|
||||
|
||||
const float MaxFlowForce = 500.0f;
|
||||
|
||||
public static bool ShowGaps = true;
|
||||
|
||||
const float OutsideColliderRaycastIntervalLowPrio = 1.5f;
|
||||
const float OutsideColliderRaycastIntervalHighPrio = 0.1f;
|
||||
|
||||
public bool IsHorizontal
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
|
||||
private float open;
|
||||
|
||||
//the force of the water flow which is exerted on physics bodies
|
||||
private Vector2 flowForce;
|
||||
private Hull flowTargetHull;
|
||||
|
||||
private float higherSurface;
|
||||
private float lowerSurface;
|
||||
|
||||
private Vector2 lerpedFlowForce;
|
||||
|
||||
//if set to true, hull connections of this gap won't be updated when changes are being done to hulls
|
||||
public bool DisableHullRechecks;
|
||||
|
||||
//can ambient light get through the gap even if it's not open
|
||||
public bool PassAmbientLight;
|
||||
|
||||
|
||||
//a collider outside the gap (for example an ice wall next to the sub)
|
||||
//used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub
|
||||
private Body outsideCollisionBlocker;
|
||||
private float outsideColliderRaycastTimer;
|
||||
|
||||
public float Open
|
||||
{
|
||||
get { return open; }
|
||||
set { open = MathHelper.Clamp(value, 0.0f, 1.0f); }
|
||||
}
|
||||
|
||||
public float Size => IsHorizontal ? Rect.Height : Rect.Width;
|
||||
|
||||
public Door ConnectedDoor;
|
||||
|
||||
public Structure ConnectedWall;
|
||||
|
||||
public Vector2 LerpedFlowForce
|
||||
{
|
||||
get { return lerpedFlowForce; }
|
||||
}
|
||||
|
||||
public Hull FlowTargetHull
|
||||
{
|
||||
get { return flowTargetHull; }
|
||||
}
|
||||
|
||||
public bool IsRoomToRoom
|
||||
{
|
||||
get
|
||||
{
|
||||
return linkedTo.Count == 2;
|
||||
}
|
||||
}
|
||||
|
||||
public override Rectangle Rect
|
||||
{
|
||||
get
|
||||
{
|
||||
return base.Rect;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Rect = value;
|
||||
|
||||
FindHulls();
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Gap";
|
||||
}
|
||||
}
|
||||
|
||||
public Gap(MapEntityPrefab prefab, Rectangle rectangle)
|
||||
: this (rectangle, Submarine.MainSub)
|
||||
{ }
|
||||
|
||||
public Gap(Rectangle rect, Submarine submarine)
|
||||
: this(rect, rect.Width < rect.Height, submarine)
|
||||
{ }
|
||||
|
||||
public Gap(Rectangle rect, bool isHorizontal, Submarine submarine)
|
||||
: base(MapEntityPrefab.Find(null, "gap"), submarine)
|
||||
{
|
||||
this.rect = rect;
|
||||
flowForce = Vector2.Zero;
|
||||
IsHorizontal = isHorizontal;
|
||||
open = 1.0f;
|
||||
|
||||
FindHulls();
|
||||
GapList.Add(this);
|
||||
InsertToList();
|
||||
|
||||
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f);
|
||||
outsideCollisionBlocker.BodyType = BodyType.Static;
|
||||
outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall;
|
||||
outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter;
|
||||
outsideCollisionBlocker.Enabled = false;
|
||||
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
|
||||
|
||||
DebugConsole.Log("Created gap (" + ID + ")");
|
||||
}
|
||||
|
||||
public override MapEntity Clone()
|
||||
{
|
||||
return new Gap(rect, IsHorizontal, Submarine);
|
||||
}
|
||||
|
||||
public override void Move(Vector2 amount)
|
||||
{
|
||||
base.Move(amount);
|
||||
|
||||
if (!DisableHullRechecks) FindHulls();
|
||||
}
|
||||
|
||||
public static void UpdateHulls()
|
||||
{
|
||||
foreach (Gap g in GapList)
|
||||
{
|
||||
for (int i = g.linkedTo.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (g.linkedTo[i].Removed)
|
||||
{
|
||||
g.linkedTo.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (g.DisableHullRechecks) continue;
|
||||
g.FindHulls();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsMouseOn(Vector2 position)
|
||||
{
|
||||
return ShowGaps && Submarine.RectContains(WorldRect, position) &&
|
||||
!Submarine.RectContains(MathUtils.ExpandRect(WorldRect, -5), position);
|
||||
}
|
||||
|
||||
public void AutoOrient()
|
||||
{
|
||||
Vector2 searchPosLeft = new Vector2(rect.X, rect.Y - rect.Height / 2);
|
||||
Hull hullLeft = Hull.FindHullOld(searchPosLeft, null, false);
|
||||
Vector2 searchPosRight = new Vector2(rect.Right, rect.Y - rect.Height / 2);
|
||||
Hull hullRight = Hull.FindHullOld(searchPosRight, null, false);
|
||||
|
||||
if (hullLeft != null && hullRight != null && hullLeft != hullRight)
|
||||
{
|
||||
IsHorizontal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 searchPosTop = new Vector2(rect.Center.X, rect.Y);
|
||||
Hull hullTop = Hull.FindHullOld(searchPosTop, null, false);
|
||||
Vector2 searchPosBottom = new Vector2(rect.Center.X, rect.Y - rect.Height);
|
||||
Hull hullBottom = Hull.FindHullOld(searchPosBottom, null, false);
|
||||
|
||||
if (hullTop != null && hullBottom != null && hullTop != hullBottom)
|
||||
{
|
||||
IsHorizontal = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((hullLeft == null) != (hullRight == null))
|
||||
{
|
||||
IsHorizontal = true;
|
||||
}
|
||||
else if ((hullTop == null) != (hullBottom == null))
|
||||
{
|
||||
IsHorizontal = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void FindHulls()
|
||||
{
|
||||
Hull[] hulls = new Hull[2];
|
||||
|
||||
linkedTo.Clear();
|
||||
|
||||
Vector2[] searchPos = new Vector2[2];
|
||||
if (IsHorizontal)
|
||||
{
|
||||
searchPos[0] = new Vector2(rect.X, rect.Y - rect.Height / 2);
|
||||
searchPos[1] = new Vector2(rect.Right, rect.Y - rect.Height / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
searchPos[0] = new Vector2(rect.Center.X, rect.Y);
|
||||
searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
hulls[i] = Hull.FindHullOld(searchPos[i], null, false);
|
||||
if (hulls[i] == null) hulls[i] = Hull.FindHullOld(searchPos[i], null, false, true);
|
||||
}
|
||||
|
||||
if (hulls[1] == hulls[0]) { hulls[1] = null; }
|
||||
|
||||
if (hulls[0] == null && hulls[1] == null) { return; }
|
||||
|
||||
if (hulls[0] == null && hulls[1] != null)
|
||||
{
|
||||
Hull temp = hulls[0];
|
||||
hulls[0] = hulls[1];
|
||||
hulls[1] = temp;
|
||||
}
|
||||
|
||||
flowTargetHull = hulls[0];
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (hulls[i] == null) continue;
|
||||
linkedTo.Add(hulls[i]);
|
||||
if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
flowForce = Vector2.Zero;
|
||||
|
||||
outsideColliderRaycastTimer -= deltaTime;
|
||||
|
||||
if (open == 0.0f || linkedTo.Count == 0)
|
||||
{
|
||||
lerpedFlowForce = Vector2.Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateOxygen();
|
||||
|
||||
if (linkedTo.Count == 1)
|
||||
{
|
||||
//gap leading from a room to outside
|
||||
UpdateRoomToOut(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
//gap leading from a room to another
|
||||
UpdateRoomToRoom(deltaTime);
|
||||
}
|
||||
|
||||
flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce);
|
||||
flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce);
|
||||
lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f);
|
||||
|
||||
EmitParticles(deltaTime);
|
||||
|
||||
if (flowTargetHull != null && lerpedFlowForce.LengthSquared() > 0.0001f)
|
||||
{
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.CurrentHull == null) continue;
|
||||
if (character.CurrentHull != linkedTo[0] as Hull &&
|
||||
(linkedTo.Count < 2 || character.CurrentHull != linkedTo[1] as Hull))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (!limb.inWater) continue;
|
||||
|
||||
float dist = Vector2.Distance(limb.WorldPosition, WorldPosition);
|
||||
if (dist > lerpedFlowForce.Length()) continue;
|
||||
|
||||
Vector2 force = lerpedFlowForce / (float)Math.Max(Math.Sqrt(dist), 20.0f) * 0.025f;
|
||||
|
||||
//vertical gaps only apply forces if the character is roughly above/below the gap
|
||||
if (!IsHorizontal)
|
||||
{
|
||||
float xDist = Math.Abs(limb.WorldPosition.X - WorldPosition.X);
|
||||
if (xDist > rect.Width || rect.Width == 0) break;
|
||||
|
||||
force *= 1.0f - xDist / rect.Width;
|
||||
}
|
||||
|
||||
if (!MathUtils.IsValid(force))
|
||||
{
|
||||
string errorMsg = "Attempted to apply invalid flow force to the character \"" + character.Name +
|
||||
"\", gap pos: " + WorldPosition +
|
||||
", limb pos: " + limb.WorldPosition +
|
||||
", flowforce: " + flowForce + ", lerpedFlowForce:" + lerpedFlowForce +
|
||||
", dist: " + dist;
|
||||
|
||||
DebugConsole.Log(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Gap.Update:InvalidFlowForce:" + character.Name,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
errorMsg);
|
||||
continue;
|
||||
}
|
||||
character.AnimController.Collider.ApplyForce(force * limb.body.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void EmitParticles(float deltaTime);
|
||||
|
||||
void UpdateRoomToRoom(float deltaTime)
|
||||
{
|
||||
if (linkedTo.Count < 2) return;
|
||||
Hull hull1 = (Hull)linkedTo[0];
|
||||
Hull hull2 = (Hull)linkedTo[1];
|
||||
|
||||
Vector2 subOffset = Vector2.Zero;
|
||||
if (hull1.Submarine != Submarine)
|
||||
{
|
||||
subOffset = Submarine.Position - hull1.Submarine.Position;
|
||||
}
|
||||
else if (hull2.Submarine != Submarine)
|
||||
{
|
||||
subOffset = hull2.Submarine.Position - Submarine.Position;
|
||||
}
|
||||
|
||||
if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) return;
|
||||
|
||||
float size = IsHorizontal ? rect.Height : rect.Width;
|
||||
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = size / 100.0f * open;
|
||||
|
||||
//horizontal gap (such as a regular door)
|
||||
if (IsHorizontal)
|
||||
{
|
||||
higherSurface = Math.Max(hull1.Surface, hull2.Surface + subOffset.Y);
|
||||
float delta = 0.0f;
|
||||
|
||||
//water level is above the lower boundary of the gap
|
||||
if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - size)
|
||||
{
|
||||
int dir = (hull1.Pressure > hull2.Pressure + subOffset.Y) ? 1 : -1;
|
||||
|
||||
//water flowing from the righthand room to the lefthand room
|
||||
if (dir == -1)
|
||||
{
|
||||
if (!(hull2.WaterVolume > 0.0f)) return;
|
||||
lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1];
|
||||
//delta = Math.Min((room2.water.pressure - room1.water.pressure) * sizeModifier, Math.Min(room2.water.Volume, room2.Volume));
|
||||
//delta = Math.Min(delta, room1.Volume - room1.water.Volume + Water.MaxCompress);
|
||||
|
||||
flowTargetHull = hull1;
|
||||
|
||||
//make sure not to move more than what the room contains
|
||||
delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume));
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume));
|
||||
hull1.WaterVolume += delta;
|
||||
hull2.WaterVolume -= delta;
|
||||
if (hull1.WaterVolume > hull1.Volume)
|
||||
{
|
||||
hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + hull2.Pressure+subOffset.Y) / 2);
|
||||
}
|
||||
|
||||
flowForce = new Vector2(-delta, 0.0f);
|
||||
}
|
||||
else if (dir == 1)
|
||||
{
|
||||
if (!(hull1.WaterVolume > 0.0f)) return;
|
||||
lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1];
|
||||
|
||||
flowTargetHull = hull2;
|
||||
|
||||
//make sure not to move more than what the room contains
|
||||
delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 5.0f * sizeModifier, Math.Min(hull1.WaterVolume, hull1.Volume));
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - (hull2.WaterVolume));
|
||||
hull1.WaterVolume -= delta;
|
||||
hull2.WaterVolume += delta;
|
||||
if (hull2.WaterVolume > hull2.Volume)
|
||||
{
|
||||
hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2);
|
||||
}
|
||||
|
||||
flowForce = new Vector2(delta, 0.0f);
|
||||
}
|
||||
|
||||
if (delta > 100.0f && subOffset == Vector2.Zero)
|
||||
{
|
||||
float avg = (hull1.Surface + hull2.Surface) / 2.0f;
|
||||
|
||||
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
|
||||
hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1] < rect.Y)
|
||||
{
|
||||
hull1.WaveVel[hull1.WaveY.Length - 1] = (avg - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f;
|
||||
hull1.WaveVel[hull1.WaveY.Length - 2] = hull1.WaveVel[hull1.WaveY.Length - 1];
|
||||
}
|
||||
|
||||
if (hull2.WaterVolume < hull2.Volume / Hull.MaxCompress &&
|
||||
hull2.Surface + hull2.WaveY[0] < rect.Y)
|
||||
{
|
||||
hull2.WaveVel[0] = (avg - (hull2.Surface + hull2.WaveY[0])) * 0.1f;
|
||||
hull2.WaveVel[1] = hull2.WaveVel[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//lower room is full of water
|
||||
if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f)
|
||||
{
|
||||
float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier);
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress)
|
||||
{
|
||||
delta -= (hull1.WaterVolume + delta) - (hull1.Volume * Hull.MaxCompress);
|
||||
}
|
||||
|
||||
delta = Math.Max(delta, 0.0f);
|
||||
hull1.WaterVolume += delta;
|
||||
hull2.WaterVolume -= delta;
|
||||
|
||||
flowForce = new Vector2(
|
||||
0.0f,
|
||||
Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta));
|
||||
|
||||
flowTargetHull = hull1;
|
||||
|
||||
if (hull1.WaterVolume > hull1.Volume)
|
||||
{
|
||||
hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + (hull2.Pressure + subOffset.Y)) / 2);
|
||||
}
|
||||
|
||||
}
|
||||
//there's water in the upper room, drop to lower
|
||||
else if (hull1.WaterVolume > 0)
|
||||
{
|
||||
flowTargetHull = hull2;
|
||||
|
||||
//make sure the amount of water moved isn't more than what the room contains
|
||||
float delta = Math.Min(hull1.WaterVolume, deltaTime * 25000f * sizeModifier);
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
if (hull2.WaterVolume + delta > hull2.Volume * Hull.MaxCompress)
|
||||
{
|
||||
delta -= (hull2.WaterVolume + delta) - (hull2.Volume * Hull.MaxCompress);
|
||||
}
|
||||
hull1.WaterVolume -= delta;
|
||||
hull2.WaterVolume += delta;
|
||||
|
||||
flowForce = new Vector2(
|
||||
hull1.WaveY[hull1.GetWaveIndex(rect.X)] - hull1.WaveY[hull1.GetWaveIndex(rect.Right)],
|
||||
MathHelper.Clamp(-delta, -200.0f, 0.0f));
|
||||
|
||||
if (hull2.WaterVolume > hull2.Volume)
|
||||
{
|
||||
hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure - subOffset.Y) + hull2.Pressure) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open > 0.0f)
|
||||
{
|
||||
if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress)
|
||||
{
|
||||
float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f;
|
||||
hull1.LethalPressure = avgLethality;
|
||||
hull2.LethalPressure = avgLethality;
|
||||
}
|
||||
else
|
||||
{
|
||||
hull1.LethalPressure = 0.0f;
|
||||
hull2.LethalPressure = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateRoomToOut(float deltaTime)
|
||||
{
|
||||
if (linkedTo.Count != 1) return;
|
||||
|
||||
float size = (IsHorizontal) ? rect.Height : rect.Width;
|
||||
|
||||
Hull hull1 = (Hull)linkedTo[0];
|
||||
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = size * open * open;
|
||||
|
||||
float delta = hull1.Volume * Hull.MaxCompress * sizeModifier * deltaTime;
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume);
|
||||
hull1.WaterVolume += delta;
|
||||
|
||||
if (hull1.WaterVolume > hull1.Volume) hull1.Pressure += 0.5f;
|
||||
|
||||
flowTargetHull = hull1;
|
||||
|
||||
if (IsHorizontal)
|
||||
{
|
||||
//water flowing from right to left
|
||||
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
|
||||
{
|
||||
flowForce = new Vector2(-delta, 0.0f);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
flowForce = new Vector2(delta, 0.0f);
|
||||
}
|
||||
|
||||
higherSurface = hull1.Surface;
|
||||
lowerSurface = rect.Y;
|
||||
|
||||
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
|
||||
hull1.Surface < rect.Y)
|
||||
{
|
||||
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
|
||||
{
|
||||
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f;
|
||||
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
|
||||
|
||||
hull1.WaveVel[hull1.WaveY.Length - 1] += vel;
|
||||
hull1.WaveVel[hull1.WaveY.Length - 2] += vel;
|
||||
}
|
||||
else
|
||||
{
|
||||
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 0.1f;
|
||||
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
|
||||
|
||||
hull1.WaveVel[0] += vel;
|
||||
hull1.WaveVel[1] += vel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rect.Y > hull1.Rect.Y - hull1.Rect.Height / 2.0f)
|
||||
{
|
||||
flowForce = new Vector2(0.0f, -delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
flowForce = new Vector2(0.0f, delta);
|
||||
}
|
||||
if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress)
|
||||
{
|
||||
hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RefreshOutsideCollider()
|
||||
{
|
||||
if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false;
|
||||
|
||||
if (outsideColliderRaycastTimer <= 0.0f)
|
||||
{
|
||||
UpdateOutsideColliderPos((Hull)linkedTo[0]);
|
||||
outsideColliderRaycastTimer = outsideCollisionBlocker.Enabled ?
|
||||
OutsideColliderRaycastIntervalHighPrio :
|
||||
OutsideColliderRaycastIntervalLowPrio;
|
||||
}
|
||||
|
||||
return outsideCollisionBlocker.Enabled;
|
||||
}
|
||||
|
||||
private void UpdateOutsideColliderPos(Hull hull)
|
||||
{
|
||||
if (Submarine == null || IsRoomToRoom) { return; }
|
||||
|
||||
Vector2 rayDir;
|
||||
if (IsHorizontal)
|
||||
{
|
||||
rayDir = new Vector2(Math.Sign(rect.Center.X - hull.Rect.Center.X), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
rayDir = new Vector2(0, Math.Sign((rect.Y - rect.Height / 2) - (hull.Rect.Y - hull.Rect.Height / 2)));
|
||||
}
|
||||
|
||||
Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition);
|
||||
Vector2 rayEnd = rayStart + rayDir * 500.0f;
|
||||
|
||||
var blockingBody = Submarine.CheckVisibility(rayStart, rayEnd);
|
||||
if (blockingBody != null)
|
||||
{
|
||||
//if the ray hit the body of the submarine itself (for example, if there's 2 layers of walls) we can ignore it
|
||||
if (blockingBody.UserData == Submarine) { return; }
|
||||
outsideCollisionBlocker.Enabled = true;
|
||||
Vector2 colliderPos = Submarine.LastPickedPosition - Submarine.SimPosition;
|
||||
float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2;
|
||||
outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
outsideCollisionBlocker.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOxygen()
|
||||
{
|
||||
if (linkedTo.Count < 2) { return; }
|
||||
Hull hull1 = (Hull)linkedTo[0];
|
||||
Hull hull2 = (Hull)linkedTo[1];
|
||||
|
||||
if (IsHorizontal)
|
||||
{
|
||||
//if the water level is above the gap, oxygen doesn't circulate
|
||||
if (Math.Max(hull1.WorldSurface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.WorldSurface + hull2.WaveY[0]) > WorldRect.Y) { return; }
|
||||
}
|
||||
|
||||
float totalOxygen = hull1.Oxygen + hull2.Oxygen;
|
||||
float totalVolume = (hull1.Volume + hull2.Volume);
|
||||
|
||||
float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen;
|
||||
deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed, Hull.OxygenDistributionSpeed);
|
||||
|
||||
hull1.Oxygen += deltaOxygen;
|
||||
hull2.Oxygen -= deltaOxygen;
|
||||
}
|
||||
|
||||
public static Gap FindAdjacent(IEnumerable<Gap> gaps, Vector2 worldPos, float allowedOrthogonalDist)
|
||||
{
|
||||
foreach (Gap gap in gaps)
|
||||
{
|
||||
if (gap.Open == 0.0f || gap.IsRoomToRoom) continue;
|
||||
|
||||
if (gap.ConnectedWall != null)
|
||||
{
|
||||
int sectionIndex = gap.ConnectedWall.FindSectionIndex(gap.Position);
|
||||
if (sectionIndex > -1 && !gap.ConnectedWall.SectionBodyDisabled(sectionIndex)) continue;
|
||||
}
|
||||
|
||||
if (gap.IsHorizontal)
|
||||
{
|
||||
if (worldPos.Y < gap.WorldRect.Y && worldPos.Y > gap.WorldRect.Y - gap.WorldRect.Height &&
|
||||
Math.Abs(gap.WorldRect.Center.X - worldPos.X) < allowedOrthogonalDist)
|
||||
{
|
||||
return gap;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (worldPos.X > gap.WorldRect.X && worldPos.X < gap.WorldRect.Right &&
|
||||
Math.Abs(gap.WorldRect.Y - gap.WorldRect.Height / 2 - worldPos.Y) < allowedOrthogonalDist)
|
||||
{
|
||||
return gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void ShallowRemove()
|
||||
{
|
||||
base.ShallowRemove();
|
||||
GapList.Remove(this);
|
||||
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
hull.ConnectedGaps.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
base.Remove();
|
||||
GapList.Remove(this);
|
||||
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
hull.ConnectedGaps.Remove(this);
|
||||
}
|
||||
|
||||
if (outsideCollisionBlocker != null)
|
||||
{
|
||||
GameMain.World.Remove(outsideCollisionBlocker);
|
||||
outsideCollisionBlocker = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
if (!DisableHullRechecks) FindHulls();
|
||||
}
|
||||
|
||||
public static Gap Load(XElement element, Submarine submarine)
|
||||
{
|
||||
Rectangle rect = Rectangle.Empty;
|
||||
|
||||
if (element.Attribute("rect") != null)
|
||||
{
|
||||
rect = element.GetAttributeRect("rect", Rectangle.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
//backwards compatibility
|
||||
rect = new Rectangle(
|
||||
int.Parse(element.Attribute("x").Value),
|
||||
int.Parse(element.Attribute("y").Value),
|
||||
int.Parse(element.Attribute("width").Value),
|
||||
int.Parse(element.Attribute("height").Value));
|
||||
}
|
||||
|
||||
bool isHorizontal = rect.Height > rect.Width;
|
||||
|
||||
var horizontalAttribute = element.Attribute("horizontal");
|
||||
if (horizontalAttribute != null)
|
||||
{
|
||||
isHorizontal = horizontalAttribute.Value.ToString() == "true";
|
||||
}
|
||||
|
||||
Gap g = new Gap(rect, isHorizontal, submarine);
|
||||
g.ID = (ushort)int.Parse(element.Attribute("ID").Value);
|
||||
g.linkedToID = new List<ushort>();
|
||||
return g;
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement element = new XElement("Gap");
|
||||
|
||||
element.Add(
|
||||
new XAttribute("ID", ID),
|
||||
new XAttribute("horizontal", IsHorizontal ? "true" : "false"));
|
||||
|
||||
element.Add(new XAttribute("rect",
|
||||
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
|
||||
(int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
|
||||
rect.Width + "," + rect.Height));
|
||||
|
||||
parentElement.Add(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Hull : MapEntity, ISerializableEntity, IServerSerializable
|
||||
{
|
||||
public static List<Hull> hullList = new List<Hull>();
|
||||
public static List<EntityGrid> EntityGrids { get; } = new List<EntityGrid>();
|
||||
|
||||
public static bool ShowHulls = true;
|
||||
|
||||
public static bool EditWater, EditFire;
|
||||
public const float OxygenDistributionSpeed = 500.0f;
|
||||
public const float OxygenDeteriorationSpeed = 0.3f;
|
||||
public const float OxygenConsumptionSpeed = 700.0f;
|
||||
|
||||
public const int WaveWidth = 32;
|
||||
public static float WaveStiffness = 0.02f;
|
||||
public static float WaveSpread = 0.05f;
|
||||
public static float WaveDampening = 0.05f;
|
||||
|
||||
//how much excess water the room can contain, relative to the volume of the room.
|
||||
//needed to make it possible for pressure to "push" water up through U-shaped hull configurations
|
||||
public const float MaxCompress = 1.05f;
|
||||
|
||||
public readonly Dictionary<string, SerializableProperty> properties;
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get { return properties; }
|
||||
}
|
||||
|
||||
private float lethalPressure;
|
||||
|
||||
private float surface, drawSurface;
|
||||
private float waterVolume;
|
||||
private float pressure;
|
||||
|
||||
private float oxygen;
|
||||
|
||||
private bool update;
|
||||
|
||||
public bool Visible = true;
|
||||
|
||||
private float[] waveY; //displacement from the surface of the water
|
||||
private float[] waveVel; //velocity of the point
|
||||
|
||||
private float[] leftDelta;
|
||||
private float[] rightDelta;
|
||||
|
||||
public readonly List<Gap> ConnectedGaps = new List<Gap>();
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Hull";
|
||||
}
|
||||
}
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private string roomName;
|
||||
[Editable, Serialize("", true, translationTextTag: "RoomName.")]
|
||||
public string RoomName
|
||||
{
|
||||
get { return roomName; }
|
||||
set
|
||||
{
|
||||
if (roomName == value) { return; }
|
||||
roomName = value;
|
||||
DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName;
|
||||
}
|
||||
}
|
||||
|
||||
public override Rectangle Rect
|
||||
{
|
||||
get
|
||||
{
|
||||
return base.Rect;
|
||||
}
|
||||
set
|
||||
{
|
||||
float prevOxygenPercentage = OxygenPercentage;
|
||||
base.Rect = value;
|
||||
|
||||
if (Submarine == null || !Submarine.Loading)
|
||||
{
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
}
|
||||
|
||||
OxygenPercentage = prevOxygenPercentage;
|
||||
surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width;
|
||||
Pressure = surface;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Linkable
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public float LethalPressure
|
||||
{
|
||||
get { return lethalPressure; }
|
||||
set { lethalPressure = MathHelper.Clamp(value, 0.0f, 100.0f); }
|
||||
}
|
||||
|
||||
public Vector2 Size
|
||||
{
|
||||
get { return new Vector2(rect.Width, rect.Height); }
|
||||
}
|
||||
|
||||
public float CeilingHeight
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public float Surface
|
||||
{
|
||||
get { return surface; }
|
||||
}
|
||||
|
||||
public float DrawSurface
|
||||
{
|
||||
get { return drawSurface; }
|
||||
set
|
||||
{
|
||||
if (Math.Abs(drawSurface - value) < 0.00001f) return;
|
||||
drawSurface = MathHelper.Clamp(value, rect.Y - rect.Height, rect.Y);
|
||||
update = true;
|
||||
}
|
||||
}
|
||||
|
||||
public float WorldSurface
|
||||
{
|
||||
get { return Submarine == null ? surface : surface + Submarine.Position.Y; }
|
||||
}
|
||||
|
||||
public float WaterVolume
|
||||
{
|
||||
get { return waterVolume; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress);
|
||||
if (waterVolume < Volume) Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
|
||||
if (waterVolume > 0.0f) update = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(100000.0f, true)]
|
||||
public float Oxygen
|
||||
{
|
||||
get { return oxygen; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
oxygen = MathHelper.Clamp(value, 0.0f, Volume);
|
||||
}
|
||||
}
|
||||
|
||||
public float WaterPercentage => MathUtils.Percentage(WaterVolume, Volume);
|
||||
|
||||
public float OxygenPercentage
|
||||
{
|
||||
get { return Volume <= 0.0f ? 100.0f : oxygen / Volume * 100.0f; }
|
||||
set { Oxygen = (value / 100.0f) * Volume; }
|
||||
}
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get { return rect.Width * rect.Height; }
|
||||
}
|
||||
|
||||
public float Pressure
|
||||
{
|
||||
get { return pressure; }
|
||||
set { pressure = value; }
|
||||
}
|
||||
|
||||
public float[] WaveY
|
||||
{
|
||||
get { return waveY; }
|
||||
}
|
||||
|
||||
public float[] WaveVel
|
||||
{
|
||||
get { return waveVel; }
|
||||
}
|
||||
|
||||
public List<FireSource> FireSources { get; private set; }
|
||||
|
||||
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
|
||||
: this (prefab, rectangle, Submarine.MainSub)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine)
|
||||
: base (prefab, submarine)
|
||||
{
|
||||
rect = rectangle;
|
||||
|
||||
OxygenPercentage = 100.0f;
|
||||
|
||||
FireSources = new List<FireSource>();
|
||||
|
||||
properties = SerializableProperty.GetProperties(this);
|
||||
|
||||
int arraySize = (int)Math.Ceiling((float)rectangle.Width / WaveWidth + 1);
|
||||
waveY = new float[arraySize];
|
||||
waveVel = new float[arraySize];
|
||||
|
||||
leftDelta = new float[arraySize];
|
||||
rightDelta = new float[arraySize];
|
||||
|
||||
surface = rect.Y - rect.Height;
|
||||
|
||||
if (submarine != null)
|
||||
{
|
||||
aiTarget = new AITarget(this)
|
||||
{
|
||||
MinSightRange = 2000,
|
||||
MaxSightRange = 5000,
|
||||
MaxSoundRange = 5000,
|
||||
SoundRange = 0
|
||||
};
|
||||
}
|
||||
|
||||
hullList.Add(this);
|
||||
|
||||
if (submarine == null || !submarine.Loading)
|
||||
{
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
}
|
||||
|
||||
WaterVolume = 0.0f;
|
||||
|
||||
InsertToList();
|
||||
|
||||
DebugConsole.Log("Created hull (" + ID + ")");
|
||||
}
|
||||
|
||||
public static Rectangle GetBorders()
|
||||
{
|
||||
if (!hullList.Any()) return Rectangle.Empty;
|
||||
|
||||
Rectangle rect = hullList[0].rect;
|
||||
|
||||
foreach (Hull hull in hullList)
|
||||
{
|
||||
if (hull.Rect.X < rect.X)
|
||||
{
|
||||
rect.Width += rect.X - hull.rect.X;
|
||||
rect.X = hull.rect.X;
|
||||
|
||||
}
|
||||
if (hull.rect.Right > rect.Right) rect.Width = hull.rect.Right - rect.X;
|
||||
|
||||
if (hull.rect.Y > rect.Y)
|
||||
{
|
||||
rect.Height += hull.rect.Y - rect.Y;
|
||||
|
||||
rect.Y = hull.rect.Y;
|
||||
}
|
||||
if (hull.rect.Y - hull.rect.Height < rect.Y - rect.Height) rect.Height = rect.Y - (hull.rect.Y - hull.rect.Height);
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
public override MapEntity Clone()
|
||||
{
|
||||
return new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine);
|
||||
}
|
||||
|
||||
public static EntityGrid GenerateEntityGrid(Rectangle worldRect)
|
||||
{
|
||||
var newGrid = new EntityGrid(worldRect, 200.0f);
|
||||
EntityGrids.Add(newGrid);
|
||||
return newGrid;
|
||||
}
|
||||
|
||||
public static EntityGrid GenerateEntityGrid(Submarine submarine)
|
||||
{
|
||||
var newGrid = new EntityGrid(submarine, 200.0f);
|
||||
EntityGrids.Add(newGrid);
|
||||
foreach (Hull hull in hullList)
|
||||
{
|
||||
if (hull.Submarine == submarine) newGrid.InsertEntity(hull);
|
||||
}
|
||||
return newGrid;
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
CeilingHeight = Rect.Height;
|
||||
|
||||
Body lowerPickedBody = Submarine.PickBody(SimPosition, SimPosition - new Vector2(0.0f, ConvertUnits.ToSimUnits(rect.Height / 2.0f + 0.1f)), null, Physics.CollisionWall);
|
||||
if (lowerPickedBody != null)
|
||||
{
|
||||
Vector2 lowerPickedPos = Submarine.LastPickedPosition;
|
||||
|
||||
if (Submarine.PickBody(SimPosition, SimPosition + new Vector2(0.0f, ConvertUnits.ToSimUnits(rect.Height / 2.0f + 0.1f)), null, Physics.CollisionWall) != null)
|
||||
{
|
||||
Vector2 upperPickedPos = Submarine.LastPickedPosition;
|
||||
|
||||
CeilingHeight = ConvertUnits.ToDisplayUnits(upperPickedPos.Y - lowerPickedPos.Y);
|
||||
}
|
||||
}
|
||||
Pressure = rect.Y - rect.Height + waterVolume / rect.Width;
|
||||
}
|
||||
|
||||
public void AddToGrid(Submarine submarine)
|
||||
{
|
||||
foreach (EntityGrid grid in EntityGrids)
|
||||
{
|
||||
if (grid.Submarine != submarine) continue;
|
||||
|
||||
rect.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
|
||||
|
||||
grid.InsertEntity(this);
|
||||
|
||||
rect.Location += MathUtils.ToPoint(submarine.HiddenSubPosition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetWaveIndex(Vector2 position)
|
||||
{
|
||||
return GetWaveIndex(position.X);
|
||||
}
|
||||
|
||||
public int GetWaveIndex(float xPos)
|
||||
{
|
||||
int index = (int)(xPos - rect.X) / WaveWidth;
|
||||
index = (int)MathHelper.Clamp(index, 0, waveY.Length - 1);
|
||||
return index;
|
||||
}
|
||||
|
||||
public override void Move(Vector2 amount)
|
||||
{
|
||||
rect.X += (int)amount.X;
|
||||
rect.Y += (int)amount.Y;
|
||||
|
||||
if (Submarine == null || !Submarine.Loading)
|
||||
{
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
}
|
||||
|
||||
surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width;
|
||||
Pressure = surface;
|
||||
}
|
||||
|
||||
public override void ShallowRemove()
|
||||
{
|
||||
base.Remove();
|
||||
hullList.Remove(this);
|
||||
|
||||
if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading))
|
||||
{
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
}
|
||||
|
||||
List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
|
||||
foreach (FireSource fireSource in fireSourcesToRemove)
|
||||
{
|
||||
fireSource.Remove();
|
||||
}
|
||||
FireSources.Clear();
|
||||
|
||||
if (EntityGrids != null)
|
||||
{
|
||||
foreach (EntityGrid entityGrid in EntityGrids)
|
||||
{
|
||||
entityGrid.RemoveEntity(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
base.Remove();
|
||||
hullList.Remove(this);
|
||||
|
||||
if (Submarine != null && !Submarine.Loading && !Submarine.Unloading)
|
||||
{
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
}
|
||||
|
||||
List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
|
||||
foreach (FireSource fireSource in fireSourcesToRemove)
|
||||
{
|
||||
fireSource.Remove();
|
||||
}
|
||||
FireSources.Clear();
|
||||
|
||||
if (EntityGrids != null)
|
||||
{
|
||||
foreach (EntityGrid entityGrid in EntityGrids)
|
||||
{
|
||||
entityGrid.RemoveEntity(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFireSource(FireSource fireSource)
|
||||
{
|
||||
FireSources.Add(fireSource);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
UpdateProjSpecific(deltaTime, cam);
|
||||
|
||||
Oxygen -= OxygenDeteriorationSpeed * deltaTime;
|
||||
|
||||
FireSource.UpdateAll(FireSources, deltaTime);
|
||||
|
||||
if (aiTarget != null)
|
||||
{
|
||||
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
|
||||
aiTarget.SoundRange -= deltaTime * 1000.0f;
|
||||
}
|
||||
|
||||
if (!update)
|
||||
{
|
||||
lethalPressure = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
surface = Math.Max(MathHelper.Lerp(
|
||||
surface,
|
||||
rect.Y - rect.Height + WaterVolume / rect.Width,
|
||||
deltaTime * 10.0f), rect.Y - rect.Height);
|
||||
//interpolate the position of the rendered surface towards the "target surface"
|
||||
drawSurface = Math.Max(MathHelper.Lerp(
|
||||
drawSurface,
|
||||
rect.Y - rect.Height + WaterVolume / rect.Width,
|
||||
deltaTime * 10.0f), rect.Y - rect.Height);
|
||||
|
||||
for (int i = 0; i < waveY.Length; i++)
|
||||
{
|
||||
//apply velocity
|
||||
waveY[i] = waveY[i] + waveVel[i];
|
||||
|
||||
//if the wave attempts to go "through" the top of the hull, make it bounce back
|
||||
if (surface + waveY[i] > rect.Y)
|
||||
{
|
||||
float excess = (surface + waveY[i]) - rect.Y;
|
||||
waveY[i] -= excess;
|
||||
waveVel[i] = waveVel[i] * -0.5f;
|
||||
}
|
||||
//if the wave attempts to go "through" the bottom of the hull, make it bounce back
|
||||
else if (surface + waveY[i] < rect.Y - rect.Height)
|
||||
{
|
||||
float excess = (surface + waveY[i]) - (rect.Y - rect.Height);
|
||||
waveY[i] -= excess;
|
||||
waveVel[i] = waveVel[i] * -0.5f;
|
||||
}
|
||||
|
||||
//acceleration
|
||||
float a = -WaveStiffness * waveY[i] - waveVel[i] * WaveDampening;
|
||||
waveVel[i] = waveVel[i] + a;
|
||||
}
|
||||
|
||||
//apply spread (two iterations)
|
||||
for (int j = 0; j < 2; j++)
|
||||
{
|
||||
for (int i = 1; i < waveY.Length - 1; i++)
|
||||
{
|
||||
leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]);
|
||||
waveVel[i - 1] += leftDelta[i];
|
||||
|
||||
rightDelta[i] = WaveSpread * (waveY[i] - waveY[i + 1]);
|
||||
waveVel[i + 1] += rightDelta[i];
|
||||
}
|
||||
|
||||
for (int i = 1; i < waveY.Length - 1; i++)
|
||||
{
|
||||
waveY[i - 1] += leftDelta[i];
|
||||
waveY[i + 1] += rightDelta[i];
|
||||
}
|
||||
}
|
||||
|
||||
//make waves propagate through horizontal gaps
|
||||
foreach (Gap gap in ConnectedGaps)
|
||||
{
|
||||
if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) continue;
|
||||
if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) continue;
|
||||
|
||||
Hull hull2 = this == gap.linkedTo[0] as Hull ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0];
|
||||
float otherSurfaceY = hull2.surface;
|
||||
if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) continue;
|
||||
|
||||
float surfaceDiff = (surface - otherSurfaceY) * gap.Open;
|
||||
if (this != gap.linkedTo[0] as Hull)
|
||||
{
|
||||
//the first hull linked to the gap handles the wave propagation,
|
||||
//the second just updates the surfaces to the same level
|
||||
if (surfaceDiff < 32.0f)
|
||||
{
|
||||
hull2.waveY[hull2.waveY.Length - 1] = surfaceDiff * 0.5f;
|
||||
waveY[0] = -surfaceDiff * 0.5f;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < 2; j++)
|
||||
{
|
||||
int i = waveY.Length - 1;
|
||||
|
||||
leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]);
|
||||
waveVel[i - 1] += leftDelta[i];
|
||||
|
||||
rightDelta[i] = WaveSpread * (waveY[i] - hull2.waveY[0] + surfaceDiff);
|
||||
hull2.waveVel[0] += rightDelta[i];
|
||||
|
||||
i = 0;
|
||||
|
||||
hull2.leftDelta[i] = WaveSpread * (hull2.waveY[i] - waveY[waveY.Length - 1] - surfaceDiff);
|
||||
waveVel[waveVel.Length - 1] += hull2.leftDelta[i];
|
||||
|
||||
hull2.rightDelta[i] = WaveSpread * (hull2.waveY[i] - hull2.waveY[i + 1]);
|
||||
hull2.waveVel[i + 1] += hull2.rightDelta[i];
|
||||
}
|
||||
|
||||
if (surfaceDiff < 32.0f)
|
||||
{
|
||||
//update surfaces to the same level
|
||||
hull2.waveY[0] = surfaceDiff * 0.5f;
|
||||
waveY[waveY.Length - 1] = -surfaceDiff * 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
hull2.waveY[0] += rightDelta[waveY.Length - 1];
|
||||
waveY[waveY.Length - 1] += hull2.leftDelta[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (waterVolume < Volume)
|
||||
{
|
||||
LethalPressure -= 10.0f * deltaTime;
|
||||
if (WaterVolume <= 0.0f)
|
||||
{
|
||||
//wait for the surface to be lerped back to bottom and the waves to settle until disabling update
|
||||
if (drawSurface > rect.Y - rect.Height + 1) return;
|
||||
for (int i = 1; i < waveY.Length - 1; i++)
|
||||
{
|
||||
if (waveY[i] > 0.1f) return;
|
||||
}
|
||||
|
||||
update = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime, Camera cam);
|
||||
|
||||
public void ApplyFlowForces(float deltaTime, Item item)
|
||||
{
|
||||
if (item.body.Mass <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var gap in ConnectedGaps.Where(gap => gap.Open > 0))
|
||||
{
|
||||
var distance = MathHelper.Max(Vector2.DistanceSquared(item.Position, gap.Position) / 1000, 1f);
|
||||
item.body.ApplyForce((gap.LerpedFlowForce / distance) * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
public void Extinguish(float deltaTime, float amount, Vector2 position)
|
||||
{
|
||||
for (int i = FireSources.Count - 1; i >= 0; i--)
|
||||
{
|
||||
FireSources[i].Extinguish(deltaTime, amount, position);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveFire(FireSource fire)
|
||||
{
|
||||
FireSources.Remove(fire);
|
||||
}
|
||||
|
||||
private HashSet<Hull> adjacentHulls = new HashSet<Hull>();
|
||||
public IEnumerable<Hull> GetConnectedHulls(bool includingThis, int? searchDepth = null)
|
||||
{
|
||||
adjacentHulls.Clear();
|
||||
int startStep = 0;
|
||||
searchDepth = searchDepth ?? 100;
|
||||
return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value);
|
||||
}
|
||||
|
||||
private HashSet<Hull> GetAdjacentHulls(bool includingThis, HashSet<Hull> connectedHulls, ref int step, int searchDepth)
|
||||
{
|
||||
if (includingThis)
|
||||
{
|
||||
connectedHulls.Add(this);
|
||||
}
|
||||
if (step > searchDepth)
|
||||
{
|
||||
return connectedHulls;
|
||||
}
|
||||
foreach (Gap g in ConnectedGaps)
|
||||
{
|
||||
for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
|
||||
{
|
||||
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
|
||||
{
|
||||
step++;
|
||||
hull.GetAdjacentHulls(true, connectedHulls, ref step, searchDepth);
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectedHulls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
|
||||
/// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found.
|
||||
/// </summary>
|
||||
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance)
|
||||
{
|
||||
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance);
|
||||
}
|
||||
|
||||
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance)
|
||||
{
|
||||
if (distance >= maxDistance) return float.MaxValue;
|
||||
if (this == target)
|
||||
{
|
||||
return distance + Vector2.Distance(startPos, endPos);
|
||||
}
|
||||
|
||||
connectedHulls.Add(this);
|
||||
|
||||
foreach (Gap g in ConnectedGaps)
|
||||
{
|
||||
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
|
||||
{
|
||||
//gap blocked if the door is not open or the predicted state is not open
|
||||
if (!g.ConnectedDoor.IsOpen || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
|
||||
{
|
||||
if (g.ConnectedDoor.OpenState < 0.1f) continue;
|
||||
}
|
||||
}
|
||||
else if (g.Open <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
|
||||
{
|
||||
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
|
||||
{
|
||||
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance);
|
||||
if (dist < float.MaxValue) { return dist; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return float.MaxValue;
|
||||
}
|
||||
|
||||
//returns the water block which contains the point (or null if it isn't inside any)
|
||||
public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
|
||||
{
|
||||
if (EntityGrids == null) return null;
|
||||
|
||||
if (guess != null)
|
||||
{
|
||||
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
|
||||
}
|
||||
|
||||
foreach (EntityGrid entityGrid in EntityGrids)
|
||||
{
|
||||
if (entityGrid.Submarine != null && !entityGrid.Submarine.Loading)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(!entityGrid.Submarine.Removed);
|
||||
Rectangle borders = entityGrid.Submarine.Borders;
|
||||
if (useWorldCoordinates)
|
||||
{
|
||||
Vector2 worldPos = entityGrid.Submarine.WorldPosition;
|
||||
borders.Location += new Point((int)worldPos.X, (int)worldPos.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
borders.Location += new Point((int)entityGrid.Submarine.HiddenSubPosition.X, (int)entityGrid.Submarine.HiddenSubPosition.Y);
|
||||
}
|
||||
|
||||
const float padding = 128.0f;
|
||||
if (position.X < borders.X - padding || position.X > borders.Right + padding ||
|
||||
position.Y > borders.Y + padding || position.Y < borders.Y - borders.Height - padding)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 transformedPosition = position;
|
||||
if (useWorldCoordinates && entityGrid.Submarine != null) transformedPosition -= entityGrid.Submarine.Position;
|
||||
|
||||
var entities = entityGrid.GetEntities(transformedPosition);
|
||||
if (entities == null) continue;
|
||||
foreach (Hull hull in entities)
|
||||
{
|
||||
if (Submarine.RectContains(hull.rect, transformedPosition, inclusive)) return hull;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//returns the water block which contains the point (or null if it isn't inside any)
|
||||
public static Hull FindHullOld(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
|
||||
{
|
||||
return FindHullOld(position, hullList, guess, useWorldCoordinates, inclusive);
|
||||
}
|
||||
|
||||
public static Hull FindHullOld(Vector2 position, List<Hull> hulls, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
|
||||
{
|
||||
if (guess != null && hulls.Contains(guess))
|
||||
{
|
||||
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
|
||||
}
|
||||
|
||||
foreach (Hull hull in hulls)
|
||||
{
|
||||
if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void DetectItemVisibility(Character c=null)
|
||||
{
|
||||
if (c==null)
|
||||
{
|
||||
foreach (Item it in Item.ItemList)
|
||||
{
|
||||
it.Visible = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Hull h = c.CurrentHull;
|
||||
hullList.ForEach(j => j.Visible = false);
|
||||
List<Hull> visibleHulls;
|
||||
if (h == null || c.Submarine == null)
|
||||
{
|
||||
visibleHulls = hullList.FindAll(j => j.CanSeeOther(null, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
visibleHulls = hullList.FindAll(j => h.CanSeeOther(j, true));
|
||||
}
|
||||
visibleHulls.ForEach(j => j.Visible = true);
|
||||
foreach (Item it in Item.ItemList)
|
||||
{
|
||||
if (it.CurrentHull == null || visibleHulls.Contains(it.CurrentHull)) it.Visible = true;
|
||||
else it.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanSeeOther(Hull other, bool allowIndirect = true)
|
||||
{
|
||||
if (other == this) return true;
|
||||
|
||||
if (other != null && other.Submarine == Submarine)
|
||||
{
|
||||
bool retVal = false;
|
||||
foreach (Gap g in ConnectedGaps)
|
||||
{
|
||||
if (g.ConnectedWall != null && g.ConnectedWall.CastShadow) continue;
|
||||
List<Hull> otherHulls = hullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this);
|
||||
retVal = otherHulls.Any(h => h == other);
|
||||
if (!retVal && allowIndirect) retVal = otherHulls.Any(h => h.CanSeeOther(other, false));
|
||||
if (retVal) return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Gap g in ConnectedGaps)
|
||||
{
|
||||
if (g.ConnectedDoor != null && !hullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true;
|
||||
}
|
||||
List<MapEntity> structures = mapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect));
|
||||
return structures.Any(st => !(st as Structure).CastShadow);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public string CreateRoomName()
|
||||
{
|
||||
List<string> roomItems = new List<string>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.CurrentHull != this) continue;
|
||||
if (item.GetComponent<Items.Components.Reactor>() != null) roomItems.Add("reactor");
|
||||
if (item.GetComponent<Items.Components.Engine>() != null) roomItems.Add("engine");
|
||||
if (item.GetComponent<Items.Components.Steering>() != null) roomItems.Add("steering");
|
||||
if (item.GetComponent<Items.Components.Sonar>() != null) roomItems.Add("sonar");
|
||||
if (item.HasTag("ballast")) roomItems.Add("ballast");
|
||||
}
|
||||
|
||||
if (roomItems.Contains("reactor"))
|
||||
return "RoomName.ReactorRoom";
|
||||
else if (roomItems.Contains("engine"))
|
||||
return "RoomName.EngineRoom";
|
||||
else if (roomItems.Contains("steering") && roomItems.Contains("sonar"))
|
||||
return "RoomName.CommandRoom";
|
||||
else if (roomItems.Contains("ballast"))
|
||||
return "RoomName.Ballast";
|
||||
|
||||
if (ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
|
||||
{
|
||||
return "RoomName.Airlock";
|
||||
}
|
||||
|
||||
Rectangle subRect = Submarine.CalculateDimensions();
|
||||
|
||||
Alignment roomPos;
|
||||
if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.66f)
|
||||
roomPos = Alignment.Top;
|
||||
else if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.33f)
|
||||
roomPos = Alignment.CenterY;
|
||||
else
|
||||
roomPos = Alignment.Bottom;
|
||||
|
||||
if (rect.Center.X < subRect.X + subRect.Width * 0.33f)
|
||||
roomPos |= Alignment.Left;
|
||||
else if (rect.Center.X < subRect.X + subRect.Width * 0.66f)
|
||||
roomPos |= Alignment.CenterX;
|
||||
else
|
||||
roomPos |= Alignment.Right;
|
||||
|
||||
return "RoomName.Sub" + roomPos.ToString();
|
||||
}
|
||||
|
||||
public static Hull Load(XElement element, Submarine submarine)
|
||||
{
|
||||
Rectangle rect = Rectangle.Empty;
|
||||
if (element.Attribute("rect") != null)
|
||||
{
|
||||
rect = element.GetAttributeRect("rect", Rectangle.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
//backwards compatibility
|
||||
rect = new Rectangle(
|
||||
int.Parse(element.Attribute("x").Value),
|
||||
int.Parse(element.Attribute("y").Value),
|
||||
int.Parse(element.Attribute("width").Value),
|
||||
int.Parse(element.Attribute("height").Value));
|
||||
}
|
||||
|
||||
var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine)
|
||||
{
|
||||
WaterVolume = element.GetAttributeFloat("pressure", 0.0f),
|
||||
ID = (ushort)int.Parse(element.Attribute("ID").Value)
|
||||
};
|
||||
|
||||
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]));
|
||||
}
|
||||
}
|
||||
|
||||
SerializableProperty.DeserializeProperties(hull, element);
|
||||
if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
|
||||
|
||||
return hull;
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
if (Submarine == null)
|
||||
{
|
||||
string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace;
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
XElement element = new XElement("Hull");
|
||||
element.Add
|
||||
(
|
||||
new XAttribute("ID", ID),
|
||||
new XAttribute("rect",
|
||||
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
|
||||
(int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
|
||||
rect.Width + "," + rect.Height),
|
||||
new XAttribute("water", waterVolume)
|
||||
);
|
||||
|
||||
if (linkedTo != null && linkedTo.Count > 0)
|
||||
{
|
||||
var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved).ToList();
|
||||
element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
|
||||
}
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
parentElement.Add(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
interface IDamageable
|
||||
{
|
||||
Vector2 SimPosition
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
Vector2 WorldPosition
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
float Health
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
interface ISpatialEntity
|
||||
{
|
||||
Vector2 Position { get; }
|
||||
Vector2 WorldPosition { get; }
|
||||
Vector2 SimPosition { get; }
|
||||
Submarine Submarine { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class ItemAssemblyPrefab : MapEntityPrefab
|
||||
{
|
||||
private string name;
|
||||
public override string Name { get { return name; } }
|
||||
|
||||
public static readonly PrefabCollection<ItemAssemblyPrefab> Prefabs = new PrefabCollection<ItemAssemblyPrefab>();
|
||||
|
||||
private bool disposed = false;
|
||||
public override void Dispose()
|
||||
{
|
||||
if (disposed) { return; }
|
||||
disposed = true;
|
||||
Prefabs.Remove(this);
|
||||
}
|
||||
|
||||
private readonly XElement configElement;
|
||||
|
||||
public List<Pair<MapEntityPrefab, Rectangle>> DisplayEntities
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Rectangle Bounds;
|
||||
|
||||
public ItemAssemblyPrefab(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
||||
if (doc == null) { return; }
|
||||
|
||||
originalName = doc.Root.GetAttributeString("name", "");
|
||||
identifier = doc.Root.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", "");
|
||||
configElement = doc.Root;
|
||||
|
||||
Category = MapEntityCategory.ItemAssembly;
|
||||
|
||||
SerializableProperty.DeserializeProperties(this, configElement);
|
||||
|
||||
name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName;
|
||||
Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description;
|
||||
|
||||
int minX = int.MaxValue, minY = int.MaxValue;
|
||||
int maxX = int.MinValue, maxY = int.MinValue;
|
||||
DisplayEntities = new List<Pair<MapEntityPrefab, Rectangle>>();
|
||||
foreach (XElement entityElement in doc.Root.Elements())
|
||||
{
|
||||
string identifier = entityElement.GetAttributeString("identifier", "");
|
||||
MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier);
|
||||
if (mapEntity == null)
|
||||
{
|
||||
string entityName = entityElement.GetAttributeString("name", "");
|
||||
mapEntity = List.FirstOrDefault(p => p.Name == entityName);
|
||||
}
|
||||
|
||||
Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty);
|
||||
if (mapEntity != null && !entityElement.GetAttributeBool("hideinassemblypreview", false))
|
||||
{
|
||||
DisplayEntities.Add(new Pair<MapEntityPrefab, Rectangle>(mapEntity, rect));
|
||||
minX = Math.Min(minX, rect.X);
|
||||
minY = Math.Min(minY, rect.Y - rect.Height);
|
||||
maxX = Math.Max(maxX, rect.Right);
|
||||
maxY = Math.Max(maxY, rect.Y);
|
||||
}
|
||||
}
|
||||
|
||||
Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY);
|
||||
|
||||
Prefabs.Add(this, false);
|
||||
}
|
||||
|
||||
public static void Remove(string filePath)
|
||||
{
|
||||
Prefabs.RemoveByFile(filePath);
|
||||
}
|
||||
|
||||
protected override void CreateInstance(Rectangle rect)
|
||||
{
|
||||
CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
|
||||
}
|
||||
|
||||
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub)
|
||||
{
|
||||
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, FilePath);
|
||||
if (entities.Count == 0) return entities;
|
||||
|
||||
Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition;
|
||||
|
||||
foreach (MapEntity me in entities)
|
||||
{
|
||||
me.Move(position);
|
||||
Item item = me as Item;
|
||||
if (item == null) continue;
|
||||
Wire wire = item.GetComponent<Wire>();
|
||||
if (wire != null) wire.MoveNodes(position - offset);
|
||||
}
|
||||
|
||||
MapEntity.MapLoaded(entities, true);
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
MapEntity.SelectedList.Clear();
|
||||
entities.ForEach(e => MapEntity.AddSelection(e));
|
||||
}
|
||||
#endif
|
||||
return entities;
|
||||
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
Dispose();
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(FilePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Deleting item assembly \"" + name + "\" failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LoadAll()
|
||||
{
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.Log("Loading item assembly prefabs: ");
|
||||
}
|
||||
|
||||
List<string> itemAssemblyFiles = new List<string>();
|
||||
|
||||
//find assembly files in the item assembly folder
|
||||
string directoryPath = Path.Combine("Content", "Items", "Assemblies");
|
||||
if (Directory.Exists(directoryPath))
|
||||
{
|
||||
itemAssemblyFiles.AddRange(Directory.GetFiles(directoryPath));
|
||||
}
|
||||
|
||||
//find assembly files in selected content packages
|
||||
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
|
||||
{
|
||||
foreach (string filePath in cp.GetFilesOfType(ContentType.ItemAssembly))
|
||||
{
|
||||
//ignore files that have already been added (= file saved to item assembly folder)
|
||||
if (itemAssemblyFiles.Any(f => Path.GetFullPath(f) == Path.GetFullPath(filePath))) { continue; }
|
||||
itemAssemblyFiles.Add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string file in itemAssemblyFiles)
|
||||
{
|
||||
new ItemAssemblyPrefab(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Common;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Voronoi2;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
static partial class CaveGenerator
|
||||
{
|
||||
public static List<VoronoiCell> GraphEdgesToCells(List<GraphEdge> graphEdges, Rectangle borders, float gridCellSize, out List<VoronoiCell>[,] cellGrid)
|
||||
{
|
||||
List<VoronoiCell> cells = new List<VoronoiCell>();
|
||||
|
||||
cellGrid = new List<VoronoiCell>[(int)Math.Ceiling(borders.Width / gridCellSize), (int)Math.Ceiling(borders.Height / gridCellSize)];
|
||||
for (int x = 0; x < borders.Width / gridCellSize; x++)
|
||||
{
|
||||
for (int y = 0; y < borders.Height / gridCellSize; y++)
|
||||
{
|
||||
cellGrid[x, y] = new List<VoronoiCell>();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (GraphEdge ge in graphEdges)
|
||||
{
|
||||
if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) continue;
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
Site site = (i == 0) ? ge.Site1 : ge.Site2;
|
||||
|
||||
int x = (int)(Math.Floor((site.Coord.X-borders.X) / gridCellSize));
|
||||
int y = (int)(Math.Floor((site.Coord.Y-borders.Y) / gridCellSize));
|
||||
|
||||
x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0)-1);
|
||||
y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1)-1);
|
||||
|
||||
VoronoiCell cell = cellGrid[x,y].Find(c => c.Site == site);
|
||||
|
||||
if (cell == null)
|
||||
{
|
||||
cell = new VoronoiCell(site);
|
||||
cellGrid[x, y].Add(cell);
|
||||
cells.Add(cell);
|
||||
}
|
||||
|
||||
if (ge.Cell1 == null)
|
||||
{
|
||||
ge.Cell1 = cell;
|
||||
}
|
||||
else
|
||||
{
|
||||
ge.Cell2 = cell;
|
||||
}
|
||||
cell.Edges.Add(ge);
|
||||
}
|
||||
}
|
||||
|
||||
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.Zero;
|
||||
|
||||
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)
|
||||
{
|
||||
var targetCells = new List<VoronoiCell>();
|
||||
for (int i = 0; i < pathNodes.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);
|
||||
if (cellIndex > -1)
|
||||
{
|
||||
targetCells.Add(cells[cellIndex]);
|
||||
break;
|
||||
}
|
||||
|
||||
searchDepth++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return GeneratePath(targetCells, cells, cellGrid, gridCellSize, limits, wanderAmount, mirror);
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
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];
|
||||
currentCell.CellType = CellType.Path;
|
||||
pathCells.Add(currentCell);
|
||||
|
||||
int currentTargetIndex = 1;
|
||||
|
||||
int iterationsLeft = cells.Count;
|
||||
|
||||
do
|
||||
{
|
||||
int edgeIndex = 0;
|
||||
|
||||
allowedEdges.Clear();
|
||||
foreach (GraphEdge edge in currentCell.Edges)
|
||||
{
|
||||
var adjacentCell = edge.AdjacentCell(currentCell);
|
||||
if (limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y))
|
||||
{
|
||||
allowedEdges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
//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++)
|
||||
{
|
||||
var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
//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;
|
||||
pathCells.Add(currentCell);
|
||||
|
||||
iterationsLeft--;
|
||||
|
||||
if (currentCell == targetCells[currentTargetIndex])
|
||||
{
|
||||
currentTargetIndex += 1;
|
||||
if (currentTargetIndex >= targetCells.Count) break;
|
||||
}
|
||||
|
||||
} while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0);
|
||||
|
||||
|
||||
Debug.WriteLine("gettooclose: " + sw2.ElapsedMilliseconds + " ms");
|
||||
sw2.Restart();
|
||||
|
||||
return pathCells;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the cell rounder by subdividing the edges and offsetting them at the middle
|
||||
/// </summary>
|
||||
/// <param name="minEdgeLength">How small the individual subdivided edges can be (smaller values produce rounder shapes, but require more geometry)</param>
|
||||
public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f)
|
||||
{
|
||||
List<GraphEdge> tempEdges = new List<GraphEdge>();
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
if (!edge.IsSolid)
|
||||
{
|
||||
tempEdges.Add(edge);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Vector2> edgePoints = new List<Vector2>();
|
||||
Vector2 edgeNormal = GetEdgeNormal(edge, 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);
|
||||
for (int i = 0; i <= pointCount; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
edgePoints.Add(edge.Point1);
|
||||
}
|
||||
else if (i == pointCount)
|
||||
{
|
||||
edgePoints.Add(edge.Point2);
|
||||
}
|
||||
else
|
||||
{
|
||||
float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
|
||||
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server);
|
||||
edgePoints.Add(
|
||||
edge.Point1 +
|
||||
edgeDir * (i / (float)pointCount) -
|
||||
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < pointCount; i++)
|
||||
{
|
||||
tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
|
||||
{
|
||||
Cell1 = edge.Cell1,
|
||||
Cell2 = edge.Cell2,
|
||||
IsSolid = edge.IsSolid,
|
||||
Site1 = edge.Site1,
|
||||
Site2 = edge.Site2,
|
||||
OutsideLevel = edge.OutsideLevel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cell.Edges = tempEdges;
|
||||
}
|
||||
|
||||
public static Body GeneratePolygons(List<VoronoiCell> cells, Level level, out List<Vector2[]> renderTriangles)
|
||||
{
|
||||
renderTriangles = new List<Vector2[]>();
|
||||
|
||||
List<Vector2> tempVertices = new List<Vector2>();
|
||||
List<Vector2> bodyPoints = new List<Vector2>();
|
||||
|
||||
Body cellBody = new Body()
|
||||
{
|
||||
SleepingAllowed = false,
|
||||
BodyType = BodyType.Static,
|
||||
CollisionCategories = Physics.CollisionLevel
|
||||
};
|
||||
GameMain.World.Add(cellBody);
|
||||
|
||||
for (int n = cells.Count - 1; n >= 0; n-- )
|
||||
{
|
||||
VoronoiCell cell = cells[n];
|
||||
|
||||
bodyPoints.Clear();
|
||||
tempVertices.Clear();
|
||||
foreach (GraphEdge ge in cell.Edges)
|
||||
{
|
||||
if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.01f) continue;
|
||||
if (!tempVertices.Any(v => Vector2.DistanceSquared(ge.Point1, v) < 1.0f))
|
||||
{
|
||||
tempVertices.Add(ge.Point1);
|
||||
bodyPoints.Add(ge.Point1);
|
||||
}
|
||||
if (!tempVertices.Any(v => Vector2.DistanceSquared(ge.Point2, v) < 1.0f))
|
||||
{
|
||||
tempVertices.Add(ge.Point2);
|
||||
bodyPoints.Add(ge.Point2);
|
||||
}
|
||||
}
|
||||
|
||||
if (tempVertices.Count < 3 || bodyPoints.Count < 2)
|
||||
{
|
||||
cells.RemoveAt(n);
|
||||
continue;
|
||||
}
|
||||
|
||||
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
|
||||
|
||||
if (bodyPoints.Count < 2) continue;
|
||||
|
||||
if (bodyPoints.Count < 3)
|
||||
{
|
||||
foreach (Vector2 vertex in tempVertices)
|
||||
{
|
||||
if (bodyPoints.Contains(vertex)) continue;
|
||||
bodyPoints.Add(vertex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < bodyPoints.Count; i++)
|
||||
{
|
||||
cell.BodyVertices.Add(bodyPoints[i]);
|
||||
bodyPoints[i] = ConvertUnits.ToSimUnits(bodyPoints[i]);
|
||||
}
|
||||
|
||||
if (cell.CellType == CellType.Empty) continue;
|
||||
|
||||
cellBody.UserData = cell;
|
||||
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
|
||||
|
||||
for (int i = 0; i < triangles.Count; i++)
|
||||
{
|
||||
//don't create a triangle if the area of the triangle is too small
|
||||
//(apparently Farseer doesn't like polygons with a very small area, see Shape.ComputeProperties)
|
||||
Vector2 a = triangles[i][0];
|
||||
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;
|
||||
|
||||
Vertices bodyVertices = new Vertices(triangles[i]);
|
||||
var newFixture = cellBody.CreatePolygon(bodyVertices, 5.0f);
|
||||
newFixture.UserData = cell;
|
||||
|
||||
if (newFixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon)
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + ")");
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"CaveGenerator.GeneratePolygons:InvalidTriangle",
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
|
||||
"Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + "). Seed: " + level.Seed);
|
||||
}
|
||||
}
|
||||
|
||||
cell.Body = cellBody;
|
||||
}
|
||||
|
||||
return cellBody;
|
||||
}
|
||||
|
||||
public static List<Vector2> CreateRandomChunk(float radius, int vertexCount, float radiusVariance)
|
||||
{
|
||||
Debug.Assert(radiusVariance < radius);
|
||||
Debug.Assert(vertexCount >= 3);
|
||||
|
||||
List<Vector2> verts = new List<Vector2>();
|
||||
float angleStep = MathHelper.TwoPi / vertexCount;
|
||||
float angle = 0.0f;
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
verts.Add(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) *
|
||||
(radius + Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)));
|
||||
angle += angleStep;
|
||||
}
|
||||
return verts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// find the index of the cell which the point is inside
|
||||
/// (actually finds the cell whose center is closest, but it's always the correct cell assuming the point is inside the borders of the diagram)
|
||||
/// </summary>
|
||||
public static int FindCellIndex(Vector2 position,List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1, Vector2? offset = null)
|
||||
{
|
||||
float closestDist = float.PositiveInfinity;
|
||||
VoronoiCell closestCell = null;
|
||||
|
||||
Vector2 gridOffset = offset == null ? Vector2.Zero : (Vector2)offset;
|
||||
position -= gridOffset;
|
||||
|
||||
int gridPosX = (int)Math.Floor(position.X / gridCellSize);
|
||||
int gridPosY = (int)Math.Floor(position.Y / gridCellSize);
|
||||
|
||||
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
|
||||
{
|
||||
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
|
||||
{
|
||||
for (int i = 0; i < cellGrid[x, y].Count; i++)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(cellGrid[x, y][i].Center, position);
|
||||
if (dist > closestDist) continue;
|
||||
|
||||
closestDist = dist;
|
||||
closestCell = cellGrid[x, y][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cells.IndexOf(closestCell);
|
||||
}
|
||||
|
||||
public static int FindCellIndex(Point position, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1)
|
||||
{
|
||||
int closestDist = int.MaxValue;
|
||||
VoronoiCell closestCell = null;
|
||||
|
||||
int gridPosX = position.X / gridCellSize;
|
||||
int gridPosY = position.Y / gridCellSize;
|
||||
|
||||
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
|
||||
{
|
||||
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
|
||||
{
|
||||
for (int i = 0; i < cellGrid[x, y].Count; i++)
|
||||
{
|
||||
int dist = MathUtils.DistanceSquared(
|
||||
(int)cellGrid[x, y][i].Site.Coord.X, (int)cellGrid[x, y][i].Site.Coord.Y,
|
||||
position.X, position.Y);
|
||||
if (dist > closestDist) continue;
|
||||
|
||||
closestDist = dist;
|
||||
closestCell = cellGrid[x, y][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cells.IndexOf(closestCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,537 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class Biome
|
||||
{
|
||||
|
||||
public readonly string Identifier;
|
||||
public readonly string DisplayName;
|
||||
public readonly string Description;
|
||||
|
||||
public readonly List<int> AllowedZones = new List<int>();
|
||||
|
||||
public Biome(string name, string description)
|
||||
{
|
||||
Identifier = name;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public Biome(XElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeString("identifier", "");
|
||||
if (string.IsNullOrEmpty(Identifier))
|
||||
{
|
||||
Identifier = element.GetAttributeString("name", "");
|
||||
DebugConsole.ThrowError("Error in biome \"" + Identifier + "\": identifier missing, using name as the identifier.");
|
||||
}
|
||||
|
||||
DisplayName =
|
||||
TextManager.Get("biomename." + Identifier, returnNull: true) ??
|
||||
element.GetAttributeString("name", "Biome") ??
|
||||
TextManager.Get("biomename." + Identifier);
|
||||
|
||||
Description =
|
||||
TextManager.Get("biomedescription." + Identifier, returnNull: true) ??
|
||||
element.GetAttributeString("description", "") ??
|
||||
TextManager.Get("biomedescription." + Identifier);
|
||||
|
||||
string allowedZonesStr = element.GetAttributeString("AllowedZones", "1,2,3,4,5,6,7,8,9");
|
||||
string[] zoneIndices = allowedZonesStr.Split(',');
|
||||
for (int i = 0; i < zoneIndices.Length; i++)
|
||||
{
|
||||
int zoneIndex = -1;
|
||||
if (!int.TryParse(zoneIndices[i].Trim(), out zoneIndex))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in biome config \"" + Identifier + "\" - \"" + zoneIndices[i] + "\" is not a valid zone index.");
|
||||
continue;
|
||||
}
|
||||
AllowedZones.Add(zoneIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LevelGenerationParams : ISerializableEntity
|
||||
{
|
||||
public static List<LevelGenerationParams> LevelParams
|
||||
{
|
||||
get { return levelParams; }
|
||||
}
|
||||
|
||||
private static List<LevelGenerationParams> levelParams;
|
||||
private static List<Biome> biomes;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private int minWidth, maxWidth, height;
|
||||
|
||||
private Point voronoiSiteInterval;
|
||||
//how much the sites are "scattered" on x- and y-axis
|
||||
//if Vector2.Zero, the sites will just be placed in a regular grid pattern
|
||||
private Point voronoiSiteVariance;
|
||||
|
||||
//how far apart the nodes of the main path can be
|
||||
//x = min interval, y = max interval
|
||||
private Point mainPathNodeIntervalRange;
|
||||
|
||||
private int smallTunnelCount;
|
||||
//x = min length, y = max length
|
||||
private Point smallTunnelLengthRange;
|
||||
|
||||
//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)
|
||||
//if 1.0f, the bottom will be completely open
|
||||
private float bottomHoleProbability;
|
||||
|
||||
//the y-position of the ocean floor (= the position from which the bottom formations extend upwards)
|
||||
private int seaFloorBaseDepth;
|
||||
//how much random variance there can be in the height of the formations
|
||||
private int seaFloorVariance;
|
||||
|
||||
private int cellSubdivisionLength;
|
||||
private float cellRoundingAmount;
|
||||
private float cellIrregularity;
|
||||
|
||||
private int mountainCountMin, mountainCountMax;
|
||||
|
||||
private int mountainHeightMin, mountainHeightMax;
|
||||
|
||||
private int ruinCount;
|
||||
|
||||
private float waterParticleScale;
|
||||
|
||||
//which biomes can this type of level appear in
|
||||
private List<Biome> allowedBiomes = new List<Biome>();
|
||||
|
||||
public IEnumerable<Biome> AllowedBiomes
|
||||
{
|
||||
get { return allowedBiomes; }
|
||||
}
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("27,30,36", true), Editable]
|
||||
public Color AmbientLightColor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("20,40,50", true), Editable()]
|
||||
public Color BackgroundTextureColor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("20,40,50", true), Editable]
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("255,255,255", true), Editable]
|
||||
public Color WallColor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)]
|
||||
public int LevelObjectAmount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
|
||||
public int MinWidth
|
||||
{
|
||||
get { return minWidth; }
|
||||
set { minWidth = Math.Max(value, 2000); }
|
||||
}
|
||||
|
||||
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
|
||||
public int MaxWidth
|
||||
{
|
||||
get { return maxWidth; }
|
||||
set { maxWidth = Math.Max(value, 2000); }
|
||||
}
|
||||
|
||||
[Serialize(50000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
|
||||
public int Height
|
||||
{
|
||||
get { return height; }
|
||||
set { height = Math.Max(value, 2000); }
|
||||
}
|
||||
|
||||
[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)")]
|
||||
public Point VoronoiSiteInterval
|
||||
{
|
||||
get { return voronoiSiteInterval; }
|
||||
set
|
||||
{
|
||||
voronoiSiteInterval.X = MathHelper.Clamp(value.X, 100, MinWidth / 2);
|
||||
voronoiSiteInterval.Y = MathHelper.Clamp(value.Y, 100, height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize("700,700", true, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " +
|
||||
"Small values produce roughly rectangular level walls. The larger the values are, the less uniform the shapes get.")]
|
||||
public Point VoronoiSiteVariance
|
||||
{
|
||||
get { return voronoiSiteVariance; }
|
||||
set
|
||||
{
|
||||
voronoiSiteVariance = new Point(
|
||||
MathHelper.Clamp(value.X, 0, voronoiSiteInterval.X),
|
||||
MathHelper.Clamp(value.Y, 0, voronoiSiteInterval.Y));
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(MinValueInt = 100, MaxValueInt = 10000), Serialize(1000, 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
|
||||
{
|
||||
get { return cellSubdivisionLength; }
|
||||
set
|
||||
{
|
||||
cellSubdivisionLength = Math.Max(value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, true, description: "How much the individual wall cells are rounded. "
|
||||
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
|
||||
public float CellRoundingAmount
|
||||
{
|
||||
get { return cellRoundingAmount; }
|
||||
set
|
||||
{
|
||||
cellRoundingAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, true, description: "How much random variance is applied to the edges of the cells. "
|
||||
+ "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")]
|
||||
public float CellIrregularity
|
||||
{
|
||||
get { return cellIrregularity; }
|
||||
set
|
||||
{
|
||||
cellIrregularity = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Editable, Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")]
|
||||
public Point MainPathNodeIntervalRange
|
||||
{
|
||||
get { return mainPathNodeIntervalRange; }
|
||||
set
|
||||
{
|
||||
mainPathNodeIntervalRange.X = MathHelper.Clamp(value.X, 100, MinWidth / 2);
|
||||
mainPathNodeIntervalRange.Y = MathHelper.Clamp(value.Y, mainPathNodeIntervalRange.X, MinWidth / 2);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(5, true, description: "The number of small tunnels placed along the main path.")]
|
||||
public int SmallTunnelCount
|
||||
{
|
||||
get { return smallTunnelCount; }
|
||||
set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); }
|
||||
}
|
||||
|
||||
[Editable, Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")]
|
||||
public Point SmallTunnelLengthRange
|
||||
{
|
||||
get { return smallTunnelLengthRange; }
|
||||
set
|
||||
{
|
||||
smallTunnelLengthRange.X = MathHelper.Clamp(value.X, 100, MinWidth);
|
||||
smallTunnelLengthRange.Y = MathHelper.Clamp(value.Y, smallTunnelLengthRange.X, MinWidth);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)]
|
||||
public int ItemCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int FloatingIceChunkCount
|
||||
{
|
||||
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
|
||||
{
|
||||
get { return seaFloorBaseDepth; }
|
||||
set { seaFloorBaseDepth = MathHelper.Clamp(value, Level.MaxEntityDepth, 0); }
|
||||
}
|
||||
|
||||
[Serialize(1000, true, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)]
|
||||
public int SeaFloorVariance
|
||||
{
|
||||
get { return seaFloorVariance; }
|
||||
set { seaFloorVariance = value; }
|
||||
}
|
||||
|
||||
[Serialize(0, true, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int MountainCountMin
|
||||
{
|
||||
get { return mountainCountMin; }
|
||||
set
|
||||
{
|
||||
mountainCountMin = Math.Max(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0, true, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)]
|
||||
public int MountainCountMax
|
||||
{
|
||||
get { return mountainCountMax; }
|
||||
set
|
||||
{
|
||||
mountainCountMax = Math.Max(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(1000, true, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
|
||||
public int MountainHeightMin
|
||||
{
|
||||
get { return mountainHeightMin; }
|
||||
set
|
||||
{
|
||||
mountainHeightMin = Math.Max(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(5000, true, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
|
||||
public int MountainHeightMax
|
||||
{
|
||||
get { return mountainHeightMax; }
|
||||
set
|
||||
{
|
||||
mountainHeightMax = Math.Max(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 50)]
|
||||
public int RuinCount
|
||||
{
|
||||
get { return ruinCount; }
|
||||
set { ruinCount = MathHelper.Clamp(value, 0, 10); }
|
||||
}
|
||||
|
||||
[Serialize(0.4f, true, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()]
|
||||
public float BottomHoleProbability
|
||||
{
|
||||
get { return bottomHoleProbability; }
|
||||
set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); }
|
||||
}
|
||||
|
||||
[Serialize(1.0f, true, description: "Scale of the water particle texture."), Editable]
|
||||
public float WaterParticleScale
|
||||
{
|
||||
get { return waterParticleScale; }
|
||||
private set { waterParticleScale = Math.Max(value, 0.01f); }
|
||||
}
|
||||
|
||||
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 WaterParticles { get; private set; }
|
||||
|
||||
public static List<Biome> GetBiomes()
|
||||
{
|
||||
return biomes;
|
||||
}
|
||||
|
||||
public static LevelGenerationParams GetRandom(string seed, Biome biome = null)
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
if (levelParams == null || !levelParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError("Level generation presets not found - using default presets");
|
||||
return new LevelGenerationParams(null);
|
||||
}
|
||||
|
||||
if (biome == null)
|
||||
{
|
||||
return levelParams.GetRandom(lp => lp.allowedBiomes.Count > 0, Rand.RandSync.Server);
|
||||
}
|
||||
|
||||
var matchingLevelParams = levelParams.FindAll(lp => lp.allowedBiomes.Contains(biome));
|
||||
if (matchingLevelParams.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Level generation presets not found for the biome \"" + biome.Identifier + "\"!");
|
||||
return new LevelGenerationParams(null);
|
||||
}
|
||||
|
||||
return matchingLevelParams[Rand.Range(0, matchingLevelParams.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
|
||||
private LevelGenerationParams(XElement element)
|
||||
{
|
||||
Name = element == null ? "default" : element.Name.ToString();
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
string biomeStr = element.GetAttributeString("biomes", "");
|
||||
if (string.IsNullOrWhiteSpace(biomeStr))
|
||||
{
|
||||
allowedBiomes = new List<Biome>(biomes);
|
||||
}
|
||||
else
|
||||
{
|
||||
string[] biomeNames = biomeStr.Split(',');
|
||||
for (int i = 0; i < biomeNames.Length; i++)
|
||||
{
|
||||
string biomeName = biomeNames[i].Trim().ToLowerInvariant();
|
||||
if (biomeName == "none") { continue; }
|
||||
|
||||
Biome matchingBiome = biomes.Find(b => b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase));
|
||||
if (matchingBiome == null)
|
||||
{
|
||||
matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase));
|
||||
if (matchingBiome == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in level generation parameters: biome \"" + biomeName + "\" not found.");
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.NewMessage("Please use biome identifiers instead of names in level generation parameter \"" + Name + "\".", Color.Orange);
|
||||
}
|
||||
}
|
||||
|
||||
allowedBiomes.Add(matchingBiome);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "background":
|
||||
BackgroundSprite = new Sprite(subElement);
|
||||
break;
|
||||
case "backgroundtop":
|
||||
BackgroundTopSprite = new Sprite(subElement);
|
||||
break;
|
||||
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);
|
||||
break;
|
||||
case "waterparticles":
|
||||
WaterParticles = new Sprite(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LoadPresets()
|
||||
{
|
||||
levelParams = new List<LevelGenerationParams>();
|
||||
biomes = new List<Biome>();
|
||||
|
||||
var files = GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters);
|
||||
if (!files.Any())
|
||||
{
|
||||
files = new List<ContentFile>() { new ContentFile("Content/Map/LevelGenerationParameters.xml", ContentType.LevelGenerationParameters) };
|
||||
}
|
||||
|
||||
List<XElement> biomeElements = new List<XElement>();
|
||||
List<XElement> levelParamElements = new List<XElement>();
|
||||
|
||||
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();
|
||||
biomeElements.Clear();
|
||||
levelParamElements.Clear();
|
||||
DebugConsole.NewMessage($"Overriding the level generation parameters and biomes with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
else if (biomeElements.Any() || levelParamElements.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use <override></override> tags to override it.");
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (XElement element in mainElement.Elements())
|
||||
{
|
||||
if (element.IsOverride())
|
||||
{
|
||||
if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
biomeElements.Clear();
|
||||
biomeElements.AddRange(element.FirstElement().Elements());
|
||||
DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
else
|
||||
{
|
||||
levelParamElements.Clear();
|
||||
DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow);
|
||||
levelParamElements.AddRange(element.Elements());
|
||||
}
|
||||
}
|
||||
else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
biomeElements.AddRange(element.Elements());
|
||||
}
|
||||
else
|
||||
{
|
||||
levelParamElements.Add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XElement biomeElement in biomeElements)
|
||||
{
|
||||
biomes.Add(new Biome(biomeElement));
|
||||
}
|
||||
|
||||
foreach (XElement levelParamElement in levelParamElements)
|
||||
{
|
||||
levelParams.Add(new LevelGenerationParams(levelParamElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+416
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+397
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Voronoi2;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
#endif
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LevelWall : IDisposable
|
||||
{
|
||||
private List<VoronoiCell> cells;
|
||||
public List<VoronoiCell> Cells
|
||||
{
|
||||
get { return cells; }
|
||||
}
|
||||
|
||||
private Body body;
|
||||
public Body Body
|
||||
{
|
||||
get { return body; }
|
||||
}
|
||||
|
||||
private float moveState;
|
||||
private float moveLength;
|
||||
|
||||
private Vector2 moveAmount;
|
||||
public Vector2 MoveAmount
|
||||
{
|
||||
get { return moveAmount; }
|
||||
set
|
||||
{
|
||||
moveAmount = value;
|
||||
moveLength = moveAmount.Length();
|
||||
}
|
||||
}
|
||||
|
||||
public float MoveSpeed;
|
||||
|
||||
private Vector2? originalPos;
|
||||
|
||||
public float MoveState
|
||||
{
|
||||
get { return moveState; }
|
||||
set { moveState = MathHelper.Clamp(value, 0.0f, MathHelper.TwoPi); }
|
||||
}
|
||||
|
||||
public LevelWall(List<Vector2> vertices, Color color, Level level, bool giftWrap = false)
|
||||
{
|
||||
if (giftWrap)
|
||||
{
|
||||
vertices = MathUtils.GiftWrap(vertices);
|
||||
}
|
||||
|
||||
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);
|
||||
#if CLIENT
|
||||
List<VertexPositionTexture> bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles);
|
||||
SetBodyVertices(bodyVertices.ToArray(), color);
|
||||
SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color);
|
||||
#endif
|
||||
}
|
||||
|
||||
public LevelWall(List<Vector2> edgePositions, Vector2 extendAmount, Color color, Level level)
|
||||
{
|
||||
cells = new List<VoronoiCell>();
|
||||
for (int i = 0; i < edgePositions.Count - 1; i++)
|
||||
{
|
||||
Vector2[] vertices = new Vector2[4];
|
||||
vertices[0] = edgePositions[i];
|
||||
vertices[1] = edgePositions[i + 1];
|
||||
vertices[2] = vertices[0] + extendAmount;
|
||||
vertices[3] = vertices[1] + extendAmount;
|
||||
|
||||
VoronoiCell wallCell = new VoronoiCell(vertices);
|
||||
wallCell.CellType = CellType.Edge;
|
||||
wallCell.Edges[0].Cell1 = wallCell;
|
||||
wallCell.Edges[1].Cell1 = wallCell;
|
||||
wallCell.Edges[2].Cell1 = wallCell;
|
||||
wallCell.Edges[3].Cell1 = wallCell;
|
||||
wallCell.Edges[0].IsSolid = true;
|
||||
|
||||
if (i > 1)
|
||||
{
|
||||
wallCell.Edges[3].Cell2 = cells[i - 1];
|
||||
cells[i - 1].Edges[1].Cell2 = wallCell;
|
||||
}
|
||||
|
||||
cells.Add(wallCell);
|
||||
}
|
||||
|
||||
body = CaveGenerator.GeneratePolygons(cells, level, out List<Vector2[]> triangles);
|
||||
body.CollisionCategories = Physics.CollisionLevel;
|
||||
|
||||
#if CLIENT
|
||||
List<VertexPositionTexture> bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles);
|
||||
SetBodyVertices(bodyVertices.ToArray(), color);
|
||||
SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (body.BodyType == BodyType.Static) return;
|
||||
|
||||
Vector2 bodyPos = ConvertUnits.ToDisplayUnits(body.Position);
|
||||
Cells.ForEach(c => c.Translation = bodyPos);
|
||||
|
||||
if (!originalPos.HasValue) originalPos = bodyPos;
|
||||
|
||||
if (moveLength > 0.0f && MoveSpeed > 0.0f)
|
||||
{
|
||||
moveState += MoveSpeed / moveLength * deltaTime;
|
||||
moveState %= MathHelper.TwoPi;
|
||||
|
||||
Vector2 targetPos = ConvertUnits.ToSimUnits(originalPos.Value + moveAmount * (float)Math.Sin(moveState));
|
||||
body.ApplyForce((targetPos - body.Position).ClampLength(1.0f) * body.Mass);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
#if CLIENT
|
||||
if (wallVertices != null)
|
||||
{
|
||||
wallVertices.Dispose();
|
||||
wallVertices = null;
|
||||
}
|
||||
if (bodyVertices != null)
|
||||
{
|
||||
BodyVertices.Dispose();
|
||||
bodyVertices = null;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.RuinGeneration
|
||||
{
|
||||
/// <summary>
|
||||
/// nodes of a binary tree used for generating underwater "dungeons"
|
||||
/// </summary>
|
||||
class BTRoom : RuinShape
|
||||
{
|
||||
private BTRoom[] subRooms;
|
||||
|
||||
public BTRoom Parent
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Corridor Corridor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public BTRoom[] SubRooms
|
||||
{
|
||||
get { return subRooms; }
|
||||
}
|
||||
|
||||
public BTRoom Adjacent
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public BTRoom(Rectangle rect)
|
||||
{
|
||||
this.rect = rect;
|
||||
}
|
||||
|
||||
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200)
|
||||
{
|
||||
bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability;
|
||||
if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight)
|
||||
{
|
||||
minDivRatio = 0.5f;
|
||||
}
|
||||
else if (rect.Width * minDivRatio < minWidth)
|
||||
{
|
||||
verticalSplit = false;
|
||||
}
|
||||
else if (rect.Height * minDivRatio < minHeight)
|
||||
{
|
||||
verticalSplit = true;
|
||||
}
|
||||
|
||||
subRooms = new BTRoom[2];
|
||||
if (verticalSplit)
|
||||
{
|
||||
SplitVertical(minDivRatio);
|
||||
}
|
||||
else
|
||||
{
|
||||
SplitHorizontal(minDivRatio);
|
||||
}
|
||||
|
||||
subRooms[0].Parent = this;
|
||||
subRooms[1].Parent = this;
|
||||
|
||||
subRooms[0].Adjacent = subRooms[1];
|
||||
subRooms[1].Adjacent = subRooms[0];
|
||||
}
|
||||
|
||||
private void SplitHorizontal(float minDivRatio)
|
||||
{
|
||||
float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server);
|
||||
subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, rect.Width, (int)(rect.Height * div)));
|
||||
subRooms[1] = new BTRoom(new Rectangle(rect.X, rect.Y + subRooms[0].rect.Height, rect.Width, rect.Height - subRooms[0].rect.Height));
|
||||
|
||||
}
|
||||
|
||||
private void SplitVertical(float minDivRatio)
|
||||
{
|
||||
float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server);
|
||||
subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, (int)(rect.Width * div), rect.Height));
|
||||
subRooms[1] = new BTRoom(new Rectangle(rect.X + subRooms[0].rect.Width, rect.Y, rect.Width - subRooms[0].rect.Width, rect.Height));
|
||||
}
|
||||
|
||||
public override void CreateWalls()
|
||||
{
|
||||
Walls = new List<Line>
|
||||
{
|
||||
new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)),
|
||||
new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)),
|
||||
new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)),
|
||||
new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom))
|
||||
};
|
||||
}
|
||||
|
||||
public void Scale(Vector2 scale)
|
||||
{
|
||||
rect.Inflate((scale.X - 1.0f) * 0.5f * rect.Width, (scale.Y - 1.0f) * 0.5f * rect.Height);
|
||||
}
|
||||
|
||||
public List<BTRoom> GetLeaves()
|
||||
{
|
||||
return GetLeaves(new List<BTRoom>());
|
||||
}
|
||||
|
||||
private List<BTRoom> GetLeaves(List<BTRoom> leaves)
|
||||
{
|
||||
if (subRooms == null)
|
||||
{
|
||||
leaves.Add(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
subRooms[0].GetLeaves(leaves);
|
||||
subRooms[1].GetLeaves(leaves);
|
||||
}
|
||||
|
||||
return leaves;
|
||||
}
|
||||
|
||||
public void GenerateCorridors(int minWidth, int maxWidth, List<Corridor> corridors)
|
||||
{
|
||||
if (Adjacent != null && Corridor == null)
|
||||
{
|
||||
Corridor = new Corridor(this, Rand.Range(minWidth, maxWidth, Rand.RandSync.Server), corridors);
|
||||
}
|
||||
|
||||
if (subRooms != null)
|
||||
{
|
||||
subRooms[0].GenerateCorridors(minWidth, maxWidth, corridors);
|
||||
subRooms[1].GenerateCorridors(minWidth, maxWidth, corridors);
|
||||
}
|
||||
}
|
||||
|
||||
public static void CalculateDistancesFromEntrance(BTRoom entrance, List<BTRoom> rooms, List<Corridor> corridors)
|
||||
{
|
||||
entrance.CalculateDistanceFromEntrance(0, rooms, new List<Corridor>(corridors));
|
||||
}
|
||||
|
||||
private void CalculateDistanceFromEntrance(int currentDist, List<BTRoom> rooms, List<Corridor> corridors)
|
||||
{
|
||||
DistanceFromEntrance = DistanceFromEntrance == 0 ? currentDist : Math.Min(currentDist, DistanceFromEntrance);
|
||||
|
||||
currentDist++;
|
||||
|
||||
var roomRect = Rect;
|
||||
roomRect.Inflate(5, 5);
|
||||
foreach (var corridor in corridors)
|
||||
{
|
||||
var corridorRect = corridor.Rect;
|
||||
corridorRect.Inflate(5, 5);
|
||||
if (!corridorRect.Intersects(roomRect)) continue;
|
||||
|
||||
corridor.DistanceFromEntrance = corridor.DistanceFromEntrance == 0 ?
|
||||
DistanceFromEntrance + 1 :
|
||||
Math.Min(corridor.DistanceFromEntrance, DistanceFromEntrance + 1);
|
||||
|
||||
|
||||
List<BTRoom> connectedRooms = new List<BTRoom>();
|
||||
foreach (var otherRoom in rooms)
|
||||
{
|
||||
if (otherRoom == this) continue;
|
||||
if (otherRoom.DistanceFromEntrance > 0 && otherRoom.DistanceFromEntrance < currentDist) continue;
|
||||
|
||||
var otherRoomRect = otherRoom.Rect;
|
||||
otherRoomRect.Inflate(5, 5);
|
||||
if (corridorRect.Intersects(otherRoomRect)) { connectedRooms.Add(otherRoom); }
|
||||
}
|
||||
|
||||
connectedRooms.Sort((r1, r2) =>
|
||||
{
|
||||
return
|
||||
(Math.Abs(r1.Rect.Center.X - Rect.Center.X) + Math.Abs(r1.Rect.Center.Y - Rect.Center.Y)) -
|
||||
(Math.Abs(r2.Rect.Center.X - Rect.Center.X) + Math.Abs(r2.Rect.Center.Y - Rect.Center.Y));
|
||||
});
|
||||
|
||||
for (int i = 0; i < connectedRooms.Count; i++)
|
||||
{
|
||||
connectedRooms[i].CalculateDistanceFromEntrance(currentDist + 1 + i, rooms, corridors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.RuinGeneration
|
||||
{
|
||||
|
||||
class Corridor : RuinShape
|
||||
{
|
||||
private readonly bool isHorizontal;
|
||||
|
||||
public bool IsHorizontal
|
||||
{
|
||||
get { return isHorizontal; }
|
||||
}
|
||||
|
||||
public BTRoom[] ConnectedRooms
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Corridor(Rectangle rect)
|
||||
{
|
||||
this.rect = rect;
|
||||
|
||||
isHorizontal = rect.Width > rect.Height;
|
||||
}
|
||||
|
||||
public Corridor(BTRoom room, int width, List<Corridor> corridors)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(room.Adjacent != null);
|
||||
|
||||
ConnectedRooms = new BTRoom[2];
|
||||
ConnectedRooms[0] = room;
|
||||
ConnectedRooms[1] = room.Adjacent;
|
||||
|
||||
Rectangle room1, room2;
|
||||
|
||||
room1 = room.Rect;
|
||||
room2 = room.Adjacent.Rect;
|
||||
|
||||
isHorizontal = (room1.Right <= room2.X || room2.Right <= room1.X);
|
||||
|
||||
//use the leaves as starting points for the corridor
|
||||
if (room.SubRooms != null)
|
||||
{
|
||||
var leaves1 = room.GetLeaves();
|
||||
var leaves2 = room.Adjacent.GetLeaves();
|
||||
|
||||
var suitableLeaves = GetSuitableLeafRooms(leaves1, leaves2, width, isHorizontal);
|
||||
if (suitableLeaves == null || suitableLeaves.Length < 2)
|
||||
{
|
||||
// No suitable leaves found due to intersections
|
||||
//DebugConsole.ThrowError("Error while generating ruins. Could not find a suitable position for a corridor. The width of the corridors may be too large compared to the sizes of the rooms.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ConnectedRooms[0] = suitableLeaves[0];
|
||||
ConnectedRooms[1] = suitableLeaves[1];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rect = CalculateRectangle(room1, room2, width, isHorizontal);
|
||||
if (rect.Width <= 0 || rect.Height <= 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while generating ruins. Attempted to create a corridor with a width or height of <= 0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
room.Corridor = this;
|
||||
room.Adjacent.Corridor = this;
|
||||
|
||||
for (int i = corridors.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var corridor = corridors[i];
|
||||
|
||||
if (corridor.rect.Intersects(this.rect))
|
||||
{
|
||||
if (isHorizontal && corridor.isHorizontal)
|
||||
{
|
||||
if (this.rect.Width < corridor.rect.Width)
|
||||
return;
|
||||
else
|
||||
corridors.RemoveAt(i);
|
||||
}
|
||||
else if (!isHorizontal && !corridor.isHorizontal)
|
||||
{
|
||||
if (this.rect.Height < corridor.rect.Height)
|
||||
return;
|
||||
else
|
||||
corridors.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
corridors.Add(this);
|
||||
}
|
||||
|
||||
public override void CreateWalls()
|
||||
{
|
||||
Walls = new List<Line>();
|
||||
if (IsHorizontal)
|
||||
{
|
||||
Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)));
|
||||
Walls.Add(new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)));
|
||||
Walls.Add(new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find two rooms which have two face-two-face walls that we can place a corridor in between
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private BTRoom[] GetSuitableLeafRooms(List<BTRoom> leaves1, List<BTRoom> leaves2, int width, bool isHorizontal)
|
||||
{
|
||||
int iOffset = Rand.Int(leaves1.Count, Rand.RandSync.Server);
|
||||
int jOffset = Rand.Int(leaves2.Count, Rand.RandSync.Server);
|
||||
|
||||
for (int iCount = 0; iCount < leaves1.Count; iCount++)
|
||||
{
|
||||
int i = (iCount + iOffset) % leaves1.Count;
|
||||
|
||||
for (int jCount = 0; jCount < leaves2.Count; jCount++)
|
||||
{
|
||||
int j = (jCount + jOffset) % leaves2.Count;
|
||||
|
||||
if (isHorizontal)
|
||||
{
|
||||
if (leaves1[i].Rect.Y > leaves2[j].Rect.Bottom - width) continue;
|
||||
if (leaves1[i].Rect.Bottom < leaves2[j].Rect.Y + width) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (leaves1[i].Rect.X > leaves2[j].Rect.Right - width) continue;
|
||||
if (leaves1[i].Rect.Right < leaves2[j].Rect.X + width) continue;
|
||||
}
|
||||
|
||||
// Check if the given corridor rect would intersect over a third room
|
||||
if (CheckForIntersection(leaves1[i], leaves2[j], leaves1, leaves2, width, isHorizontal)) continue;
|
||||
|
||||
return new BTRoom[] { leaves1[i], leaves2[j] };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool CheckForIntersection(BTRoom potential1, BTRoom potential2, List<BTRoom> leaves1, List<BTRoom> leaves2, int width, bool isHorizontal)
|
||||
{
|
||||
Rectangle potentialCorridorRectangle = CalculateRectangle(potential1.Rect, potential2.Rect, width, isHorizontal);
|
||||
|
||||
if (potentialCorridorRectangle.Width <= 0 || potentialCorridorRectangle.Height <= 0) return true; // Invalid rectangle
|
||||
|
||||
for (int i = 0; i < leaves1.Count; i++)
|
||||
{
|
||||
if (leaves1[i] == potential1) continue;
|
||||
if (potentialCorridorRectangle.Intersects(leaves1[i].Rect)) return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < leaves2.Count; i++)
|
||||
{
|
||||
if (leaves2[i] == potential2) continue;
|
||||
if (potentialCorridorRectangle.Intersects(leaves2[i].Rect)) return true;
|
||||
}
|
||||
|
||||
rect = potentialCorridorRectangle; // Save the rectangle that passes the test
|
||||
return false;
|
||||
}
|
||||
|
||||
private Rectangle CalculateRectangle(Rectangle rect1, Rectangle rect2, int width, bool isHorizontal)
|
||||
{
|
||||
if (isHorizontal)
|
||||
{
|
||||
int left = Math.Min(rect1.Right, rect2.Right);
|
||||
int right = Math.Max(rect1.X, rect2.X);
|
||||
|
||||
int top = Math.Max(rect1.Y, rect2.Y);
|
||||
//int bottom = Math.Min(room1.Bottom, room2.Bottom);
|
||||
int yPos = top;//Rand.Range(top, bottom - width, Rand.RandSync.Server);
|
||||
|
||||
return new Rectangle(left, yPos, right - left, width);
|
||||
}
|
||||
else if (rect1.Y > rect2.Bottom || rect2.Y > rect1.Bottom)
|
||||
{
|
||||
int left = Math.Max(rect1.X, rect2.X);
|
||||
int right = Math.Min(rect1.Right, rect2.Right);
|
||||
|
||||
int top = Math.Min(rect1.Bottom, rect2.Bottom);
|
||||
int bottom = Math.Max(rect1.Y, rect2.Y);
|
||||
|
||||
int xPos = Rand.Range(left, right - width, Rand.RandSync.Server);
|
||||
|
||||
return new Rectangle(xPos, top, width, bottom - top);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("wat");
|
||||
return new Rectangle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.RuinGeneration
|
||||
{
|
||||
[Flags]
|
||||
enum RuinEntityType
|
||||
{
|
||||
Wall, Back, Door, Hatch, Prop
|
||||
}
|
||||
|
||||
class RuinGenerationParams : ISerializableEntity
|
||||
{
|
||||
public static List<RuinGenerationParams> List
|
||||
{
|
||||
get
|
||||
{
|
||||
if (paramsList == null)
|
||||
{
|
||||
LoadAll();
|
||||
}
|
||||
return paramsList;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<RuinGenerationParams> paramsList;
|
||||
|
||||
private string filePath;
|
||||
|
||||
private List<RuinRoom> roomTypeList;
|
||||
|
||||
public string Name => "RuinGenerationParams";
|
||||
|
||||
[Serialize("5000,5000", false), Editable]
|
||||
public Point SizeMin
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Serialize("8000,8000", false), Editable]
|
||||
public Point SizeMax
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(3, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)]
|
||||
public int RoomDivisionIterationsMin
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(4, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)]
|
||||
public int RoomDivisionIterationsMax
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.5f, false, description: "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f)]
|
||||
public float VerticalSplitProbability
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
|
||||
public int MinSplitWidth
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
|
||||
public int MinSplitHeight
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable]
|
||||
public Vector2 RoomWidthRange
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Serialize("0.5,0.9", false, description: "The minimum and maximum height of a room relative to the areas created by the split algorithm."), Editable]
|
||||
public Vector2 RoomHeightRange
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("200,256", false, description: "The minimum and maximum width of the corridors between rooms."), Editable]
|
||||
public Point CorridorWidthRange
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new Dictionary<string, SerializableProperty>();
|
||||
|
||||
public IEnumerable<RuinRoom> RoomTypeList
|
||||
{
|
||||
get { return roomTypeList; }
|
||||
}
|
||||
|
||||
private RuinGenerationParams(XElement element)
|
||||
{
|
||||
roomTypeList = new List<RuinRoom>();
|
||||
|
||||
if (element != null)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
roomTypeList.Add(new RuinRoom(subElement));
|
||||
}
|
||||
}
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
|
||||
public static RuinGenerationParams GetRandom()
|
||||
{
|
||||
if (paramsList == null) { LoadAll(); }
|
||||
|
||||
if (paramsList.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("No ruin configuration files found in any content package.");
|
||||
return new RuinGenerationParams(null);
|
||||
}
|
||||
|
||||
return paramsList[Rand.Int(paramsList.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
|
||||
private static void LoadAll()
|
||||
{
|
||||
paramsList = new List<RuinGenerationParams>();
|
||||
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
|
||||
if (doc == null) { continue; }
|
||||
var mainElement = doc.Root;
|
||||
if (doc.Root.IsOverride())
|
||||
{
|
||||
mainElement = doc.Root.FirstElement();
|
||||
paramsList.Clear();
|
||||
DebugConsole.NewMessage($"Overriding all ruin configuration parameters using the file {configFile.Path}.", Color.Yellow);
|
||||
}
|
||||
else if (paramsList.Any())
|
||||
{
|
||||
DebugConsole.NewMessage($"Adding additional ruin configuration parameters from file '{configFile.Path}'");
|
||||
}
|
||||
var newParams = new RuinGenerationParams(mainElement)
|
||||
{
|
||||
filePath = configFile.Path
|
||||
};
|
||||
paramsList.Add(newParams);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearAll()
|
||||
{
|
||||
paramsList?.Clear();
|
||||
paramsList = null;
|
||||
}
|
||||
|
||||
public static void SaveAll()
|
||||
{
|
||||
XmlWriterSettings settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
NewLineOnAttributes = true
|
||||
};
|
||||
|
||||
foreach (RuinGenerationParams generationParams in List)
|
||||
{
|
||||
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
|
||||
{
|
||||
if (configFile.Path != generationParams.filePath) continue;
|
||||
|
||||
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
|
||||
if (doc == null) { continue; }
|
||||
|
||||
SerializableProperty.SerializeProperties(generationParams, doc.Root);
|
||||
|
||||
using (var writer = XmlWriter.Create(configFile.Path, settings))
|
||||
{
|
||||
doc.WriteTo(writer);
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RuinRoom : ISerializableEntity
|
||||
{
|
||||
public enum RoomPlacement
|
||||
{
|
||||
Any,
|
||||
First,
|
||||
Last
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
|
||||
public float Commonness { get; private set; }
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new Dictionary<string, SerializableProperty>();
|
||||
|
||||
[Serialize(RoomPlacement.Any, false), Editable]
|
||||
public RoomPlacement Placement
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0, false), Editable]
|
||||
public int PlacementOffset
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false), Editable]
|
||||
public bool IsCorridor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1.0f, false), Editable]
|
||||
public float MinWaterAmount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Serialize(1.0f, false), Editable]
|
||||
public float MaxWaterAmount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private List<RuinEntityConfig> entityList = new List<RuinEntityConfig>();
|
||||
|
||||
public RuinRoom(XElement element)
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
Name = element.GetAttributeString("name", "");
|
||||
|
||||
if (element != null)
|
||||
{
|
||||
int groupIndex = 0;
|
||||
LoadEntities(element, ref groupIndex);
|
||||
}
|
||||
|
||||
void LoadEntities(XElement element2, ref int groupIndex)
|
||||
{
|
||||
foreach (XElement subElement in element2.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
groupIndex++;
|
||||
LoadEntities(subElement, ref groupIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
entityList.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RuinEntityConfig GetRandomEntity(RuinEntityType type, Alignment alignment)
|
||||
{
|
||||
var matchingEntities = entityList.FindAll(rs =>
|
||||
rs.Type == type &&
|
||||
rs.Alignment.HasFlag(alignment));
|
||||
|
||||
if (!matchingEntities.Any()) return null;
|
||||
|
||||
return ToolBox.SelectWeightedRandom(
|
||||
matchingEntities,
|
||||
matchingEntities.Select(s => s.Commonness).ToList(),
|
||||
Rand.RandSync.Server);
|
||||
}
|
||||
|
||||
public List<RuinEntityConfig> GetPropList(RuinShape room, Rand.RandSync randSync)
|
||||
{
|
||||
Dictionary<int, List<RuinEntityConfig>> propGroups = new Dictionary<int, List<RuinEntityConfig>>();
|
||||
foreach (RuinEntityConfig entityConfig in entityList)
|
||||
{
|
||||
if (entityConfig.Type != RuinEntityType.Prop) { continue; }
|
||||
if (room.Rect.Width < entityConfig.MinRoomSize.X || room.Rect.Height < entityConfig.MinRoomSize.Y) { continue; }
|
||||
if (room.Rect.Width > entityConfig.MaxRoomSize.X || room.Rect.Height > entityConfig.MaxRoomSize.Y) { continue; }
|
||||
if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex))
|
||||
{
|
||||
propGroups[entityConfig.SingleGroupIndex] = new List<RuinEntityConfig>();
|
||||
}
|
||||
propGroups[entityConfig.SingleGroupIndex].Add(entityConfig);
|
||||
}
|
||||
|
||||
List<RuinEntityConfig> props = new List<RuinEntityConfig>();
|
||||
foreach (KeyValuePair<int, List<RuinEntityConfig>> propGroup in propGroups)
|
||||
{
|
||||
if (propGroup.Key == 0)
|
||||
{
|
||||
props.AddRange(propGroup.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]);
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
class RuinEntityConfig : ISerializableEntity
|
||||
{
|
||||
public readonly MapEntityPrefab Prefab;
|
||||
|
||||
public enum RelativePlacement
|
||||
{
|
||||
SameRoom,
|
||||
NextRoom,
|
||||
NextCorridor,
|
||||
PreviousRoom,
|
||||
PreviousCorridor,
|
||||
FirstRoom,
|
||||
FirstCorridor,
|
||||
LastRoom,
|
||||
LastCorridor
|
||||
}
|
||||
|
||||
public class EntityConnection
|
||||
{
|
||||
//which type of room to search for the item to connect to
|
||||
//sameroom, nextroom, previousroom, firstroom and lastroom are also valid
|
||||
public string RoomName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string TargetEntityIdentifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//Identifier of the item to run the wire from. Only needed in item assemblies to determine which item in the assembly to use.
|
||||
public string SourceEntityIdentifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//if set, the connection is done by running a wire from
|
||||
//(Pair.First = the name of the connection in this item) to (Pair.Second = the name of the connection in the target item)
|
||||
public Pair<string, string> WireConnection
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public EntityConnection(XElement element)
|
||||
{
|
||||
RoomName = element.GetAttributeString("roomname", "");
|
||||
TargetEntityIdentifier = element.GetAttributeString("targetentity", "");
|
||||
SourceEntityIdentifier = element.GetAttributeString("sourceentity", "");
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WireConnection = new Pair<string, string>(
|
||||
subElement.GetAttributeString("from", ""),
|
||||
subElement.GetAttributeString("to", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(Alignment.Bottom, false), Editable]
|
||||
public Alignment Alignment { get; private set; }
|
||||
|
||||
[Serialize("0,0", false, description: "Minimum offset from the anchor position, relative to the size of the room." +
|
||||
" For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center."), Editable]
|
||||
public Vector2 MinOffset { get; private set; }
|
||||
[Serialize("0,0", false, description: "Maximum offset from the anchor position, relative to the size of the room." +
|
||||
" For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center."), Editable]
|
||||
public Vector2 MaxOffset { get; private set; }
|
||||
|
||||
[Serialize(RuinEntityType.Prop, false), Editable]
|
||||
public RuinEntityType Type { get; private set; }
|
||||
|
||||
[Serialize(false, false), Editable]
|
||||
public bool Expand { get; private set; }
|
||||
|
||||
[Serialize(RelativePlacement.SameRoom, false), Editable]
|
||||
public RelativePlacement PlacementRelativeToParent { get; private set; }
|
||||
|
||||
[Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
|
||||
public float Commonness { get; private set; }
|
||||
|
||||
[Serialize(1, false)]
|
||||
public int MinAmount { get; private set; }
|
||||
[Serialize(1, false)]
|
||||
public int MaxAmount { get; private set; }
|
||||
|
||||
[Serialize("0,0", false)]
|
||||
public Point MinRoomSize { get; private set; }
|
||||
|
||||
[Serialize("100000,100000", false)]
|
||||
public Point MaxRoomSize { get; private set; }
|
||||
|
||||
[Serialize("", false)]
|
||||
public string TargetContainer { get; private set; }
|
||||
|
||||
public List<EntityConnection> EntityConnections { get; private set; } = new List<EntityConnection>();
|
||||
|
||||
|
||||
public int SingleGroupIndex;
|
||||
|
||||
private readonly List<RuinEntityConfig> childEntities = new List<RuinEntityConfig>();
|
||||
|
||||
public IEnumerable<RuinEntityConfig> ChildEntities
|
||||
{
|
||||
get { return childEntities; }
|
||||
}
|
||||
|
||||
public string Name => Prefab == null ? "null" : Prefab.Name;
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new Dictionary<string, SerializableProperty>();
|
||||
|
||||
public RuinEntityConfig(XElement element)
|
||||
{
|
||||
string name = element.GetAttributeString("prefab", "");
|
||||
Prefab = MapEntityPrefab.Find(name: null, identifier: name);
|
||||
|
||||
if (Prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Loading ruin entity config failed - map entity prefab \"" + name + "\" not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
int gIndex = 0;
|
||||
LoadChildren(element, ref gIndex);
|
||||
|
||||
void LoadChildren(XElement element2, ref int groupIndex)
|
||||
{
|
||||
foreach (XElement subElement in element2.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "connection":
|
||||
case "entityconnection":
|
||||
EntityConnections.Add(new EntityConnection(subElement));
|
||||
break;
|
||||
case "chooseone":
|
||||
groupIndex++;
|
||||
LoadChildren(subElement, ref groupIndex);
|
||||
break;
|
||||
default:
|
||||
childEntities.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,393 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LinkedSubmarinePrefab : MapEntityPrefab
|
||||
{
|
||||
//public static readonly PrefabCollection<LinkedSubmarinePrefab> Prefabs = new PrefabCollection<LinkedSubmarinePrefab>();
|
||||
|
||||
private bool disposed = false;
|
||||
public override void Dispose()
|
||||
{
|
||||
if (disposed) { return; }
|
||||
disposed = true;
|
||||
//Prefabs.Remove(this);
|
||||
}
|
||||
|
||||
public readonly Submarine mainSub;
|
||||
|
||||
public LinkedSubmarinePrefab(Submarine submarine)
|
||||
{
|
||||
this.mainSub = submarine;
|
||||
}
|
||||
|
||||
protected override void CreateInstance(Rectangle rect)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Submarine.MainSub != null);
|
||||
|
||||
LinkedSubmarine.CreateDummy(Submarine.MainSub, mainSub.FilePath, rect.Location.ToVector2());
|
||||
}
|
||||
}
|
||||
|
||||
partial class LinkedSubmarine : MapEntity
|
||||
{
|
||||
private List<Vector2> wallVertices;
|
||||
|
||||
private string filePath;
|
||||
|
||||
private bool loadSub;
|
||||
private Submarine sub;
|
||||
|
||||
private ushort originalMyPortID;
|
||||
|
||||
//the ID of the docking port the sub was docked to in the original sub file
|
||||
//(needed when replacing a lost sub)
|
||||
private ushort originalLinkedToID;
|
||||
private DockingPort originalLinkedPort;
|
||||
|
||||
private bool purchasedLostShuttles;
|
||||
|
||||
public Submarine Sub
|
||||
{
|
||||
get
|
||||
{
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
|
||||
private XElement saveElement;
|
||||
|
||||
public override bool Linkable
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public LinkedSubmarine(Submarine submarine)
|
||||
: base(null, submarine)
|
||||
{
|
||||
linkedToID = new List<ushort>();
|
||||
|
||||
InsertToList();
|
||||
|
||||
DebugConsole.Log("Created linked submarine (" + ID + ")");
|
||||
}
|
||||
|
||||
public static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
|
||||
{
|
||||
LinkedSubmarine sl = new LinkedSubmarine(mainSub)
|
||||
{
|
||||
sub = linkedSub
|
||||
};
|
||||
|
||||
return sl;
|
||||
}
|
||||
|
||||
public static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position)
|
||||
{
|
||||
XDocument doc = Submarine.OpenFile(filePath);
|
||||
if (doc == null || doc.Root == null) return null;
|
||||
|
||||
LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position);
|
||||
sl.filePath = filePath;
|
||||
|
||||
return sl;
|
||||
}
|
||||
|
||||
public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position)
|
||||
{
|
||||
LinkedSubmarine sl = new LinkedSubmarine(mainSub);
|
||||
sl.GenerateWallVertices(element);
|
||||
|
||||
sl.Rect = new Rectangle(
|
||||
(int)sl.wallVertices.Min(v => v.X + position.X),
|
||||
(int)sl.wallVertices.Max(v => v.Y + position.Y),
|
||||
(int)sl.wallVertices.Max(v => v.X + position.X),
|
||||
(int)sl.wallVertices.Min(v => v.Y + position.Y));
|
||||
|
||||
sl.rect = new Rectangle((int)position.X, (int)position.Y, 1, 1);
|
||||
|
||||
return sl;
|
||||
}
|
||||
|
||||
public override bool IsMouseOn(Vector2 position)
|
||||
{
|
||||
return Vector2.Distance(position, WorldPosition) < 50.0f;
|
||||
}
|
||||
|
||||
public override MapEntity Clone()
|
||||
{
|
||||
return CreateDummy(Submarine, filePath, Position);
|
||||
}
|
||||
|
||||
private void GenerateWallVertices(XElement rootElement)
|
||||
{
|
||||
List<Vector2> points = new List<Vector2>();
|
||||
|
||||
var wallPrefabs = StructurePrefab.Prefabs.Where(mp => mp.Body);
|
||||
|
||||
foreach (XElement element in rootElement.Elements())
|
||||
{
|
||||
if (element.Name != "Structure") { continue; }
|
||||
|
||||
string name = element.GetAttributeString("name", "");
|
||||
string identifier = element.GetAttributeString("identifier", "");
|
||||
|
||||
StructurePrefab prefab = Structure.FindPrefab(name, identifier);
|
||||
if (prefab == null) { continue; }
|
||||
|
||||
var rect = element.GetAttributeVector4("rect", Vector4.Zero);
|
||||
|
||||
points.Add(new Vector2(rect.X, rect.Y));
|
||||
points.Add(new Vector2(rect.X + rect.Z, rect.Y));
|
||||
points.Add(new Vector2(rect.X, rect.Y - rect.W));
|
||||
points.Add(new Vector2(rect.X + rect.Z, rect.Y - rect.W));
|
||||
}
|
||||
|
||||
wallVertices = MathUtils.GiftWrap(points);
|
||||
}
|
||||
|
||||
public static LinkedSubmarine Load(XElement element, Submarine submarine)
|
||||
{
|
||||
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
|
||||
LinkedSubmarine linkedSub = null;
|
||||
|
||||
if (Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
linkedSub = CreateDummy(submarine, element, pos);
|
||||
linkedSub.saveElement = element;
|
||||
linkedSub.purchasedLostShuttles = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
linkedSub = new LinkedSubmarine(submarine)
|
||||
{
|
||||
saveElement = element
|
||||
};
|
||||
|
||||
linkedSub.purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles;
|
||||
string levelSeed = element.GetAttributeString("location", "");
|
||||
if (!string.IsNullOrWhiteSpace(levelSeed) &&
|
||||
GameMain.GameSession.Level != null &&
|
||||
GameMain.GameSession.Level.Seed != levelSeed &&
|
||||
!linkedSub.purchasedLostShuttles)
|
||||
{
|
||||
linkedSub.loadSub = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
linkedSub.loadSub = true;
|
||||
linkedSub.rect.Location = MathUtils.ToPoint(pos);
|
||||
}
|
||||
}
|
||||
|
||||
linkedSub.filePath = element.GetAttributeString("filepath", "");
|
||||
int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]);
|
||||
for (int i = 0; i < linkedToIds.Length; i++)
|
||||
{
|
||||
linkedSub.linkedToID.Add((ushort)linkedToIds[i]);
|
||||
if (Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
if (FindEntityByID((ushort)linkedToIds[i]) is MapEntity linked)
|
||||
{
|
||||
linkedSub.linkedTo.Add(linked);
|
||||
}
|
||||
}
|
||||
}
|
||||
linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0);
|
||||
linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0);
|
||||
|
||||
|
||||
return linkedSub.loadSub ? linkedSub : null;
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
if (!loadSub) { return; }
|
||||
|
||||
sub = Submarine.Load(saveElement, false);
|
||||
|
||||
Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero);
|
||||
if (worldPos != Vector2.Zero)
|
||||
{
|
||||
sub.SetPosition(worldPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
sub.SetPosition(WorldPosition);
|
||||
}
|
||||
|
||||
DockingPort linkedPort = null;
|
||||
DockingPort myPort = null;
|
||||
|
||||
MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
|
||||
if (linkedItem == null)
|
||||
{
|
||||
linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub);
|
||||
}
|
||||
else
|
||||
{
|
||||
linkedPort = ((Item)linkedItem).GetComponent<DockingPort>();
|
||||
}
|
||||
|
||||
if (linkedPort == null)
|
||||
{
|
||||
if (purchasedLostShuttles)
|
||||
{
|
||||
linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent<DockingPort>();
|
||||
}
|
||||
if (linkedPort == null) { return; }
|
||||
}
|
||||
originalLinkedPort = linkedPort;
|
||||
|
||||
myPort = (FindEntityByID(originalMyPortID) as Item)?.GetComponent<DockingPort>();
|
||||
if (myPort == null)
|
||||
{
|
||||
float closestDistance = 0.0f;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
if (port.Item.Submarine != sub || port.IsHorizontal != linkedPort.IsHorizontal) { continue; }
|
||||
float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition);
|
||||
if (myPort == null || dist < closestDistance)
|
||||
{
|
||||
myPort = port;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (myPort != null)
|
||||
{
|
||||
originalMyPortID = myPort.Item.ID;
|
||||
|
||||
myPort.Undock();
|
||||
|
||||
//something else is already docked to the port this sub should be docked to
|
||||
//may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be,
|
||||
//and the shuttle is then restored in the campaign mode
|
||||
//or if the user connects multiple subs to the same docking ports in the sub editor
|
||||
if (linkedPort.Docked && linkedPort.DockingTarget != null && linkedPort.DockingTarget != myPort)
|
||||
{
|
||||
//just spawn below the main sub
|
||||
sub.SetPosition(
|
||||
linkedPort.Item.Submarine.WorldPosition -
|
||||
new Vector2(0, linkedPort.Item.Submarine.GetDockedBorders().Height / 2 + sub.GetDockedBorders().Height / 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition;
|
||||
Vector2 offset = (myPort.IsHorizontal ?
|
||||
Vector2.UnitX * Math.Sign(linkedPort.Item.WorldPosition.X - myPort.Item.WorldPosition.X) :
|
||||
Vector2.UnitY * Math.Sign(linkedPort.Item.WorldPosition.Y - myPort.Item.WorldPosition.Y));
|
||||
offset *= myPort.DockedDistance;
|
||||
|
||||
sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);
|
||||
|
||||
myPort.Dock(linkedPort);
|
||||
myPort.Lock(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles)
|
||||
{
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
if (wall.Submarine != sub) { continue; }
|
||||
for (int i = 0; i < wall.SectionCount; i++)
|
||||
{
|
||||
wall.AddDamage(i, -wall.Prefab.Health);
|
||||
}
|
||||
}
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine != sub) { continue; }
|
||||
hull.WaterVolume = 0.0f;
|
||||
hull.OxygenPercentage = 100.0f;
|
||||
}
|
||||
}
|
||||
|
||||
sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition);
|
||||
sub.Submarine = Submarine;
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement saveElement = null;
|
||||
|
||||
if (sub == null)
|
||||
{
|
||||
if (this.saveElement == null)
|
||||
{
|
||||
var doc = Submarine.OpenFile(filePath);
|
||||
saveElement = doc.Root;
|
||||
saveElement.Name = "LinkedSubmarine";
|
||||
saveElement.Add(new XAttribute("filepath", filePath));
|
||||
}
|
||||
else
|
||||
{
|
||||
saveElement = this.saveElement;
|
||||
}
|
||||
|
||||
if (saveElement.Attribute("pos") != null) saveElement.Attribute("pos").Remove();
|
||||
saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)));
|
||||
|
||||
var linkedPort = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
|
||||
if (linkedPort != null)
|
||||
{
|
||||
saveElement.Attribute("linkedto")?.Remove();
|
||||
saveElement.Add(new XAttribute("linkedto", linkedPort.ID));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
saveElement = new XElement("LinkedSubmarine");
|
||||
sub.SaveToXElement(saveElement);
|
||||
}
|
||||
|
||||
saveElement.Attribute("originallinkedto")?.Remove();
|
||||
saveElement.Add(new XAttribute("originallinkedto", originalLinkedPort != null ? originalLinkedPort.Item.ID : originalLinkedToID));
|
||||
saveElement.Attribute("originalmyport")?.Remove();
|
||||
saveElement.Add(new XAttribute("originalmyport", originalMyPortID));
|
||||
|
||||
if (sub != null)
|
||||
{
|
||||
bool leaveBehind = false;
|
||||
if (!sub.DockedTo.Contains(Submarine.MainSub))
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndPosition || Submarine.MainSub.AtStartPosition);
|
||||
if (Submarine.MainSub.AtEndPosition)
|
||||
{
|
||||
leaveBehind = sub.AtEndPosition != Submarine.MainSub.AtEndPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
leaveBehind = sub.AtStartPosition != Submarine.MainSub.AtStartPosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (leaveBehind)
|
||||
{
|
||||
saveElement.SetAttributeValue("location", Level.Loaded.Seed);
|
||||
saveElement.SetAttributeValue("worldpos", XMLExtensions.Vector2ToString(sub.SubBody.Position));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (saveElement.Attribute("location") != null) saveElement.Attribute("location").Remove();
|
||||
if (saveElement.Attribute("worldpos") != null) saveElement.Attribute("worldpos").Remove();
|
||||
}
|
||||
saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition));
|
||||
}
|
||||
|
||||
parentElement.Add(saveElement);
|
||||
|
||||
return saveElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Location
|
||||
{
|
||||
public List<LocationConnection> Connections;
|
||||
|
||||
private string baseName;
|
||||
private int nameFormatIndex;
|
||||
|
||||
public bool Discovered;
|
||||
|
||||
public int TypeChangeTimer;
|
||||
|
||||
public string BaseName { get => baseName; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public Vector2 MapPosition { get; private set; }
|
||||
|
||||
public LocationType Type { get; private set; }
|
||||
|
||||
public int PortraitId { get; private set; }
|
||||
|
||||
public int MissionsCompleted;
|
||||
|
||||
private List<Mission> availableMissions = new List<Mission>();
|
||||
public IEnumerable<Mission> AvailableMissions
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckMissionCompleted();
|
||||
|
||||
for (int i = availableMissions.Count; i < Connections.Count * 2; i++)
|
||||
{
|
||||
int seed = (ToolBox.StringToInt(BaseName) + MissionsCompleted * 10 + i) % int.MaxValue;
|
||||
MTRandom rand = new MTRandom(seed);
|
||||
|
||||
LocationConnection connection = Connections[(MissionsCompleted + i) % Connections.Count];
|
||||
Location destination = connection.OtherLocation(this);
|
||||
|
||||
var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.All, true);
|
||||
if (mission == null) { continue; }
|
||||
if (availableMissions.Any(m => m.Prefab == mission.Prefab)) { continue; }
|
||||
if (GameSettings.VerboseLogging && mission != null)
|
||||
{
|
||||
DebugConsole.NewMessage("Generated a new mission for a location (location: " + Name + ", seed: " + seed.ToString("X") + ", missions completed: " + MissionsCompleted + ", type: " + mission.Name + ")", Color.White);
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
}
|
||||
|
||||
return availableMissions;
|
||||
}
|
||||
}
|
||||
|
||||
public Mission SelectedMission
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int SelectedMissionIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SelectedMission == null) { return -1; }
|
||||
return availableMissions.IndexOf(SelectedMission);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value < 0 || value >= AvailableMissions.Count())
|
||||
{
|
||||
SelectedMission = null;
|
||||
return;
|
||||
}
|
||||
SelectedMission = availableMissions[value];
|
||||
}
|
||||
}
|
||||
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand)
|
||||
{
|
||||
this.Type = LocationType.Random(rand, zone);
|
||||
this.Name = RandomName(Type, rand);
|
||||
this.MapPosition = mapPosition;
|
||||
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
|
||||
Connections = new List<LocationConnection>();
|
||||
}
|
||||
|
||||
public static Location CreateRandom(Vector2 position, int? zone , Random rand)
|
||||
{
|
||||
return new Location(position, zone, rand);
|
||||
}
|
||||
|
||||
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
|
||||
return AvailableMissions.Where(m => m.Locations[1] == connection.OtherLocation(this));
|
||||
}
|
||||
|
||||
public void ChangeType(LocationType newType)
|
||||
{
|
||||
if (newType == Type) { return; }
|
||||
|
||||
//clear missions from this and adjacent locations (they may be invalid now)
|
||||
availableMissions.Clear();
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
connection.OtherLocation(this)?.availableMissions.Clear();
|
||||
}
|
||||
|
||||
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
|
||||
|
||||
Type = newType;
|
||||
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
}
|
||||
|
||||
public void CheckMissionCompleted()
|
||||
{
|
||||
foreach (Mission mission in availableMissions)
|
||||
{
|
||||
if (mission.Completed)
|
||||
{
|
||||
DebugConsole.Log("Mission \"" + mission.Name + "\" completed in \"" + Name + "\".");
|
||||
MissionsCompleted++;
|
||||
}
|
||||
}
|
||||
|
||||
availableMissions.RemoveAll(m => m.Completed);
|
||||
}
|
||||
|
||||
private string RandomName(LocationType type, Random rand)
|
||||
{
|
||||
baseName = type.GetRandomName(rand);
|
||||
nameFormatIndex = rand.Next() % type.NameFormats.Count;
|
||||
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LocationConnection
|
||||
{
|
||||
public Biome Biome;
|
||||
|
||||
public float Difficulty;
|
||||
|
||||
public List<Vector2[]> CrackSegments;
|
||||
|
||||
public bool Passed;
|
||||
|
||||
public Level Level { get; set; }
|
||||
|
||||
public Vector2 CenterPos
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Locations[0].MapPosition + Locations[1].MapPosition) / 2.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public Location[] Locations { get; private set; }
|
||||
|
||||
public float Length
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public LocationConnection(Location location1, Location location2)
|
||||
{
|
||||
Locations = new Location[] { location1, location2 };
|
||||
|
||||
Length = Vector2.Distance(location1.MapPosition, location2.MapPosition);
|
||||
}
|
||||
|
||||
public Location OtherLocation(Location location)
|
||||
{
|
||||
if (Locations[0] == location)
|
||||
{
|
||||
return Locations[1];
|
||||
}
|
||||
else if (Locations[1] == location)
|
||||
{
|
||||
return Locations[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LocationType
|
||||
{
|
||||
public static readonly List<LocationType> List = new List<LocationType>();
|
||||
|
||||
private List<string> nameFormats;
|
||||
private List<string> names;
|
||||
|
||||
private Sprite symbolSprite;
|
||||
|
||||
private readonly List<Sprite> portraits = new List<Sprite>();
|
||||
|
||||
//<name, commonness>
|
||||
private List<Tuple<JobPrefab, float>> hireableJobs;
|
||||
private float totalHireableWeight;
|
||||
|
||||
public Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
|
||||
|
||||
public readonly string Identifier;
|
||||
public readonly string Name;
|
||||
|
||||
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
|
||||
|
||||
public bool UseInMainMenu
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<string> NameFormats
|
||||
{
|
||||
get { return nameFormats; }
|
||||
}
|
||||
|
||||
public bool HasHireableCharacters
|
||||
{
|
||||
get { return hireableJobs.Any(); }
|
||||
}
|
||||
|
||||
public Sprite Sprite
|
||||
{
|
||||
get { return symbolSprite; }
|
||||
}
|
||||
|
||||
public Color SpriteColor
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"LocationType (" + Identifier + ")";
|
||||
}
|
||||
|
||||
private LocationType(XElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeString("identifier", element.Name.ToString());
|
||||
Name = TextManager.Get("LocationName." + Identifier);
|
||||
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
|
||||
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
|
||||
|
||||
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
|
||||
try
|
||||
{
|
||||
names = File.ReadAllLines(nameFile).ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read name file for location type \"" + Identifier + "\"!", e);
|
||||
names = new List<string>() { "Name file not found" };
|
||||
}
|
||||
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", new string[] { "" });
|
||||
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
|
||||
{
|
||||
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
|
||||
if (splitCommonnessPerZone.Length != 2 ||
|
||||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
|
||||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone0index: zone0commonness, zone1index: zone1commonness\"");
|
||||
break;
|
||||
}
|
||||
CommonnessPerZone[zoneIndex] = zoneCommonness;
|
||||
}
|
||||
|
||||
hireableJobs = new List<Tuple<JobPrefab, float>>();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "hireable":
|
||||
string jobIdentifier = subElement.GetAttributeString("identifier", "");
|
||||
JobPrefab jobPrefab = null;
|
||||
if (jobIdentifier == "")
|
||||
{
|
||||
DebugConsole.ThrowError("Error in location type \""+ Identifier + "\" - hireable jobs should be configured using identifiers instead of names.");
|
||||
}
|
||||
else
|
||||
{
|
||||
jobPrefab = JobPrefab.Get(jobIdentifier.ToLowerInvariant());
|
||||
}
|
||||
if (jobPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in in location type " + Identifier + " - could not find a job with the identifier \"" + jobIdentifier + "\".");
|
||||
continue;
|
||||
}
|
||||
float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f);
|
||||
totalHireableWeight += jobCommonness;
|
||||
Tuple<JobPrefab, float> hireableJob = new Tuple<JobPrefab, float>(jobPrefab, jobCommonness);
|
||||
hireableJobs.Add(hireableJob);
|
||||
break;
|
||||
case "symbol":
|
||||
symbolSprite = new Sprite(subElement, lazyLoad: true);
|
||||
SpriteColor = subElement.GetAttributeColor("color", Color.White);
|
||||
break;
|
||||
case "changeto":
|
||||
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement));
|
||||
break;
|
||||
case "portrait":
|
||||
var portrait = new Sprite(subElement, lazyLoad: true);
|
||||
if (portrait != null)
|
||||
{
|
||||
portraits.Add(portrait);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JobPrefab GetRandomHireable()
|
||||
{
|
||||
float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.Server);
|
||||
|
||||
foreach (Tuple<JobPrefab, float> hireable in hireableJobs)
|
||||
{
|
||||
if (randFloat < hireable.Item2) return hireable.Item1;
|
||||
randFloat -= hireable.Item2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Sprite GetPortrait(int portraitId)
|
||||
{
|
||||
if (portraits.Count == 0) { return null; }
|
||||
return portraits[Math.Abs(portraitId) % portraits.Count];
|
||||
}
|
||||
|
||||
public string GetRandomName(Random rand)
|
||||
{
|
||||
return names[rand.Next() % names.Count];
|
||||
}
|
||||
|
||||
public static LocationType Random(Random rand, int? zone = null)
|
||||
{
|
||||
Debug.Assert(List.Count > 0, "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
|
||||
|
||||
List<LocationType> allowedLocationTypes = zone.HasValue ? List.FindAll(lt => lt.CommonnessPerZone.ContainsKey(zone.Value)) : List;
|
||||
|
||||
if (allowedLocationTypes.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
|
||||
}
|
||||
|
||||
if (zone.HasValue)
|
||||
{
|
||||
return ToolBox.SelectWeightedRandom(
|
||||
allowedLocationTypes,
|
||||
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToList(),
|
||||
rand);
|
||||
}
|
||||
else
|
||||
{
|
||||
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Count];
|
||||
}
|
||||
}
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
List.Clear();
|
||||
var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes);
|
||||
foreach (ContentFile file in locationTypeFiles)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
if (doc == null) { continue; }
|
||||
var mainElement = doc.Root;
|
||||
if (doc.Root.IsOverride())
|
||||
{
|
||||
mainElement = doc.Root.FirstElement();
|
||||
DebugConsole.NewMessage($"Overriding all location types with '{file.Path}'", Color.Yellow);
|
||||
List.Clear();
|
||||
}
|
||||
else if (List.Any())
|
||||
{
|
||||
DebugConsole.NewMessage($"Loading additional location types from file '{file.Path}'");
|
||||
}
|
||||
foreach (XElement sourceElement in mainElement.Elements())
|
||||
{
|
||||
var element = sourceElement;
|
||||
bool allowOverriding = false;
|
||||
if (sourceElement.IsOverride())
|
||||
{
|
||||
element = sourceElement.FirstElement();
|
||||
allowOverriding = true;
|
||||
}
|
||||
string identifier = element.GetAttributeString("identifier", null);
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{file.Path}': No identifier defined for {element.Name.ToString()}");
|
||||
continue;
|
||||
}
|
||||
var duplicate = List.FirstOrDefault(l => l.Identifier == identifier);
|
||||
if (duplicate != null)
|
||||
{
|
||||
if (allowOverriding)
|
||||
{
|
||||
List.Remove(duplicate);
|
||||
DebugConsole.NewMessage($"Overriding the location type with the identifier '{identifier}' with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{file.Path}': Duplicate identifier defined with the identifier '{identifier}'");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
LocationType locationType = new LocationType(element);
|
||||
List.Add(locationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LocationTypeChange
|
||||
{
|
||||
public readonly string ChangeToType;
|
||||
public readonly float Probability;
|
||||
public readonly int RequiredDuration;
|
||||
|
||||
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
|
||||
public readonly List<string> DisallowedAdjacentLocations;
|
||||
|
||||
//the change can only happen if there's at least one of the given types of locations next to this one
|
||||
public readonly List<string> RequiredAdjacentLocations;
|
||||
|
||||
public LocationTypeChange(string currentType, XElement element)
|
||||
{
|
||||
ChangeToType = element.GetAttributeString("type", "");
|
||||
Probability = element.GetAttributeFloat("probability", 1.0f);
|
||||
RequiredDuration = element.GetAttributeInt("requiredduration", 0);
|
||||
|
||||
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
|
||||
RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList();
|
||||
|
||||
string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
|
||||
|
||||
Messages = TextManager.GetAll(messageTag);
|
||||
if (Messages == null)
|
||||
{
|
||||
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Voronoi2;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Map
|
||||
{
|
||||
private MapGenerationParams generationParams;
|
||||
|
||||
private readonly int size;
|
||||
|
||||
private List<LocationConnection> connections;
|
||||
public Action<Location, LocationConnection> OnLocationSelected;
|
||||
//from -> to
|
||||
public Action<Location, Location> OnLocationChanged;
|
||||
public Action<LocationConnection, Mission> OnMissionSelected;
|
||||
|
||||
public Location CurrentLocation { get; private set; }
|
||||
|
||||
public int CurrentLocationIndex
|
||||
{
|
||||
get { return Locations.IndexOf(CurrentLocation); }
|
||||
}
|
||||
|
||||
public Location SelectedLocation { get; private set; }
|
||||
|
||||
public int SelectedLocationIndex
|
||||
{
|
||||
get { return Locations.IndexOf(SelectedLocation); }
|
||||
}
|
||||
|
||||
public int SelectedMissionIndex
|
||||
{
|
||||
get { return SelectedConnection == null ? -1 : CurrentLocation.SelectedMissionIndex; }
|
||||
}
|
||||
|
||||
public LocationConnection SelectedConnection { get; private set; }
|
||||
|
||||
public string Seed { get; private set; }
|
||||
|
||||
public List<Location> Locations { get; private set; }
|
||||
|
||||
public Map(string seed)
|
||||
{
|
||||
generationParams = MapGenerationParams.Instance;
|
||||
this.Seed = seed;
|
||||
this.size = generationParams.Size;
|
||||
|
||||
Locations = new List<Location>();
|
||||
|
||||
connections = new List<LocationConnection>();
|
||||
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed));
|
||||
|
||||
Generate();
|
||||
|
||||
//start from the colony furthest away from the center
|
||||
float largestDist = 0.0f;
|
||||
Vector2 center = new Vector2(size, size) / 2;
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.Type.Identifier != "City") continue;
|
||||
float dist = Vector2.DistanceSquared(center, location.MapPosition);
|
||||
if (dist > largestDist)
|
||||
{
|
||||
largestDist = dist;
|
||||
CurrentLocation = location;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentLocation.Discovered = true;
|
||||
|
||||
foreach (LocationConnection connection in connections)
|
||||
{
|
||||
connection.Level = Level.CreateRandom(connection);
|
||||
}
|
||||
|
||||
InitProjectSpecific();
|
||||
}
|
||||
|
||||
partial void InitProjectSpecific();
|
||||
|
||||
public float[,] Noise;
|
||||
|
||||
private void GenerateNoiseMap(int octaves, float persistence)
|
||||
{
|
||||
float z = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
|
||||
Noise = new float[generationParams.NoiseResolution, generationParams.NoiseResolution];
|
||||
|
||||
float min = float.MaxValue, max = 0.0f;
|
||||
for (int x = 0; x < generationParams.NoiseResolution; x++)
|
||||
{
|
||||
for (int y = 0; y < generationParams.NoiseResolution; y++)
|
||||
{
|
||||
Noise[x, y] = (float)PerlinNoise.OctavePerlin(
|
||||
(double)x / generationParams.NoiseResolution,
|
||||
(double)y / generationParams.NoiseResolution,
|
||||
z, generationParams.NoiseFrequency, octaves, persistence);
|
||||
min = Math.Min(Noise[x, y], min);
|
||||
max = Math.Max(Noise[x, y], max);
|
||||
}
|
||||
}
|
||||
|
||||
float radius = generationParams.NoiseResolution / 2;
|
||||
Vector2 center = Vector2.One * radius;
|
||||
float range = max - min;
|
||||
|
||||
float centerDarkenRadius = radius * generationParams.CenterDarkenRadius;
|
||||
float edgeDarkenRadius = radius * generationParams.EdgeDarkenRadius;
|
||||
for (int x = 0; x < generationParams.NoiseResolution; x++)
|
||||
{
|
||||
for (int y = 0; y < generationParams.NoiseResolution; y++)
|
||||
{
|
||||
//normalize the noise to 0-1 range
|
||||
Noise[x, y] = (Noise[x, y] - min) / range;
|
||||
|
||||
float dist = Vector2.Distance(center, new Vector2(x, y));
|
||||
if (dist < centerDarkenRadius)
|
||||
{
|
||||
float angle = (float)Math.Atan2(y - center.Y, x - center.X);
|
||||
float phase = angle * generationParams.CenterDarkenWaveFrequency + Noise[x, y] * generationParams.CenterDarkenWavePhaseNoise;
|
||||
float currDarkenRadius = centerDarkenRadius * (0.6f + (float)Math.Sin(phase) * 0.4f);
|
||||
if (dist < currDarkenRadius)
|
||||
{
|
||||
float darkenAmount = 1.0f - (dist / currDarkenRadius);
|
||||
Noise[x, y] = MathHelper.Lerp(Noise[x, y], Noise[x, y] * (1.0f - generationParams.CenterDarkenStrength), darkenAmount);
|
||||
}
|
||||
}
|
||||
if (dist > edgeDarkenRadius)
|
||||
{
|
||||
float darkenAmount = Math.Min((dist - edgeDarkenRadius) / (radius - edgeDarkenRadius), 1.0f);
|
||||
Noise[x, y] = MathHelper.Lerp(Noise[x, y], 1.0f - generationParams.EdgeDarkenStrength, darkenAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void GenerateNoiseMapProjSpecific();
|
||||
|
||||
private void Generate()
|
||||
{
|
||||
connections.Clear();
|
||||
Locations.Clear();
|
||||
|
||||
GenerateNoiseMap(generationParams.NoiseOctaves, generationParams.NoisePersistence);
|
||||
|
||||
List<Vector2> sites = new List<Vector2>();
|
||||
float mapRadius = size / 2;
|
||||
Vector2 mapCenter = new Vector2(mapRadius, mapRadius);
|
||||
|
||||
float locationRadius = mapRadius * generationParams.LocationRadius;
|
||||
|
||||
for (float x = mapCenter.X - locationRadius; x < mapCenter.X + locationRadius; x += generationParams.VoronoiSiteInterval)
|
||||
{
|
||||
for (float y = mapCenter.Y - locationRadius; y < mapCenter.Y + locationRadius; y += generationParams.VoronoiSiteInterval)
|
||||
{
|
||||
float noiseVal = Noise[(int)(x / size * generationParams.NoiseResolution), (int)(y / size * generationParams.NoiseResolution)];
|
||||
if (Rand.Range(generationParams.VoronoiSitePlacementMinVal, 1.0f, Rand.RandSync.Server) <
|
||||
noiseVal * generationParams.VoronoiSitePlacementProbability)
|
||||
{
|
||||
sites.Add(new Vector2(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Voronoi voronoi = new Voronoi(0.5f);
|
||||
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(sites, size, size);
|
||||
float zoneRadius = size / 2 / generationParams.DifficultyZones;
|
||||
|
||||
sites.Clear();
|
||||
foreach (GraphEdge edge in edges)
|
||||
{
|
||||
if (edge.Point1 == edge.Point2) continue;
|
||||
|
||||
if (Vector2.DistanceSquared(edge.Point1, mapCenter) >= locationRadius * locationRadius ||
|
||||
Vector2.DistanceSquared(edge.Point2, mapCenter) >= locationRadius * locationRadius) continue;
|
||||
|
||||
Location[] newLocations = new Location[2];
|
||||
newLocations[0] = Locations.Find(l => l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2);
|
||||
newLocations[1] = Locations.Find(l => l != newLocations[0] && (l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2));
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (newLocations[i] != null) continue;
|
||||
|
||||
Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
|
||||
|
||||
int positionIndex = Rand.Int(1, Rand.RandSync.Server);
|
||||
|
||||
Vector2 position = points[positionIndex];
|
||||
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) position = points[1 - positionIndex];
|
||||
int zone = MathHelper.Clamp(generationParams.DifficultyZones - (int)Math.Floor(Vector2.Distance(position, mapCenter) / zoneRadius), 1, generationParams.DifficultyZones);
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server));
|
||||
Locations.Add(newLocations[i]);
|
||||
}
|
||||
|
||||
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
|
||||
float centerDist = Vector2.Distance(newConnection.CenterPos, mapCenter);
|
||||
newConnection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 10.0f, Rand.RandSync.Server), 0, 100);
|
||||
|
||||
connections.Add(newConnection);
|
||||
}
|
||||
|
||||
//remove connections that are too short
|
||||
float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
|
||||
for (int i = connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
LocationConnection connection = connections[i];
|
||||
|
||||
if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//locations.Remove(connection.Locations[0]);
|
||||
connections.Remove(connection);
|
||||
|
||||
foreach (LocationConnection connection2 in connections)
|
||||
{
|
||||
if (connection2.Locations[0] == connection.Locations[0]) connection2.Locations[0] = connection.Locations[1];
|
||||
if (connection2.Locations[1] == connection.Locations[0]) connection2.Locations[1] = connection.Locations[1];
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Location> connectedLocations = new HashSet<Location>();
|
||||
foreach (LocationConnection connection in connections)
|
||||
{
|
||||
connection.Locations[0].Connections.Add(connection);
|
||||
connection.Locations[1].Connections.Add(connection);
|
||||
|
||||
connectedLocations.Add(connection.Locations[0]);
|
||||
connectedLocations.Add(connection.Locations[1]);
|
||||
}
|
||||
|
||||
//remove orphans
|
||||
Locations.RemoveAll(c => !connectedLocations.Contains(c));
|
||||
|
||||
//remove locations that are too close to each other
|
||||
float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
|
||||
for (int i = Locations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
for (int j = Locations.Count - 1; j > i; j--)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(Locations[i].MapPosition, Locations[j].MapPosition);
|
||||
if (dist > minLocationDistanceSqr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//move connections from Locations[j] to Locations[i]
|
||||
foreach (LocationConnection connection in Locations[j].Connections)
|
||||
{
|
||||
if (connection.Locations[0] == Locations[j])
|
||||
{
|
||||
connection.Locations[0] = Locations[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
connection.Locations[1] = Locations[i];
|
||||
}
|
||||
Locations[i].Connections.Add(connection);
|
||||
}
|
||||
Locations.RemoveAt(j);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
i = Math.Min(i, connections.Count - 1);
|
||||
LocationConnection connection = connections[i];
|
||||
for (int n = Math.Min(i - 1, connections.Count - 1); n >= 0; n--)
|
||||
{
|
||||
if (connection.Locations.Contains(connections[n].Locations[0])
|
||||
&& connection.Locations.Contains(connections[n].Locations[1]))
|
||||
{
|
||||
connections.RemoveAt(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (LocationConnection connection in connections)
|
||||
{
|
||||
float centerDist = Vector2.Distance(connection.CenterPos, mapCenter);
|
||||
connection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 0, 100);
|
||||
}
|
||||
|
||||
AssignBiomes();
|
||||
|
||||
GenerateNoiseMapProjSpecific();
|
||||
}
|
||||
|
||||
private void AssignBiomes()
|
||||
{
|
||||
float locationRadius = size * 0.5f * generationParams.LocationRadius;
|
||||
|
||||
var biomes = LevelGenerationParams.GetBiomes();
|
||||
Vector2 centerPos = new Vector2(size, size) / 2;
|
||||
for (int i = 0; i < generationParams.DifficultyZones; i++)
|
||||
{
|
||||
List<Biome> allowedBiomes = biomes.FindAll(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i));
|
||||
float zoneRadius = locationRadius * ((i + 1.0f) / generationParams.DifficultyZones);
|
||||
foreach (LocationConnection connection in connections)
|
||||
{
|
||||
if (connection.Biome != null) continue;
|
||||
|
||||
if (i == generationParams.DifficultyZones - 1 ||
|
||||
Vector2.Distance(connection.Locations[0].MapPosition, centerPos) < zoneRadius ||
|
||||
Vector2.Distance(connection.Locations[1].MapPosition, centerPos) < zoneRadius)
|
||||
{
|
||||
connection.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpandBiomes(List<LocationConnection> seeds)
|
||||
{
|
||||
List<LocationConnection> nextSeeds = new List<LocationConnection>();
|
||||
foreach (LocationConnection connection in seeds)
|
||||
{
|
||||
foreach (Location location in connection.Locations)
|
||||
{
|
||||
foreach (LocationConnection otherConnection in location.Connections)
|
||||
{
|
||||
if (otherConnection == connection) continue;
|
||||
if (otherConnection.Biome != null) continue; //already assigned
|
||||
|
||||
otherConnection.Biome = connection.Biome;
|
||||
nextSeeds.Add(otherConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextSeeds.Count > 0)
|
||||
{
|
||||
ExpandBiomes(nextSeeds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void MoveToNextLocation()
|
||||
{
|
||||
Location prevLocation = CurrentLocation;
|
||||
SelectedConnection.Passed = true;
|
||||
|
||||
CurrentLocation = SelectedLocation;
|
||||
CurrentLocation.Discovered = true;
|
||||
SelectedLocation = null;
|
||||
|
||||
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
|
||||
}
|
||||
|
||||
public void SetLocation(int index)
|
||||
{
|
||||
if (index == -1)
|
||||
{
|
||||
CurrentLocation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= Locations.Count)
|
||||
{
|
||||
DebugConsole.ThrowError("Location index out of bounds");
|
||||
return;
|
||||
}
|
||||
|
||||
Location prevLocation = CurrentLocation;
|
||||
CurrentLocation = Locations[index];
|
||||
CurrentLocation.Discovered = true;
|
||||
|
||||
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
|
||||
}
|
||||
|
||||
public void SelectLocation(int index)
|
||||
{
|
||||
if (index == -1)
|
||||
{
|
||||
SelectedLocation = null;
|
||||
SelectedConnection = null;
|
||||
|
||||
OnLocationSelected?.Invoke(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= Locations.Count)
|
||||
{
|
||||
DebugConsole.ThrowError("Location index out of bounds");
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedLocation = Locations[index];
|
||||
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
|
||||
}
|
||||
|
||||
public void SelectLocation(Location location)
|
||||
{
|
||||
if (!Locations.Contains(location))
|
||||
{
|
||||
string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map.";
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedLocation = location;
|
||||
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
|
||||
}
|
||||
|
||||
public void SelectMission(int missionIndex)
|
||||
{
|
||||
if (SelectedConnection == null) { return; }
|
||||
if (CurrentLocation == null)
|
||||
{
|
||||
string errorMsg = "Failed to select a mission (current location not set).";
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
}
|
||||
CurrentLocation.SelectedMissionIndex = missionIndex;
|
||||
|
||||
//the destination must be the same as the destination of the mission
|
||||
if (CurrentLocation.SelectedMission != null &&
|
||||
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
|
||||
{
|
||||
SelectLocation(CurrentLocation.SelectedMission.Locations[1]);
|
||||
}
|
||||
|
||||
OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission);
|
||||
}
|
||||
|
||||
public void SelectRandomLocation(bool preferUndiscovered)
|
||||
{
|
||||
List<Location> nextLocations = CurrentLocation.Connections.Select(c => c.OtherLocation(CurrentLocation)).ToList();
|
||||
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
|
||||
|
||||
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
|
||||
{
|
||||
SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectLocation(nextLocations[Rand.Int(nextLocations.Count, Rand.RandSync.Unsynced)]);
|
||||
}
|
||||
}
|
||||
|
||||
public void ProgressWorld()
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Discovered) continue;
|
||||
|
||||
//find which types of locations this one can change to
|
||||
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
|
||||
List<LocationTypeChange> readyTypeChanges = new List<LocationTypeChange>();
|
||||
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
|
||||
{
|
||||
//check if there are any adjacent locations that would prevent the change
|
||||
bool disallowedFound = false;
|
||||
foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations)
|
||||
{
|
||||
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
disallowedFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (disallowedFound) continue;
|
||||
|
||||
//check that there's a required adjacent location present
|
||||
bool requiredFound = false;
|
||||
foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations)
|
||||
{
|
||||
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
requiredFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) continue;
|
||||
|
||||
allowedTypeChanges.Add(typeChange);
|
||||
|
||||
if (location.TypeChangeTimer >= typeChange.RequiredDuration)
|
||||
{
|
||||
readyTypeChanges.Add(typeChange);
|
||||
}
|
||||
}
|
||||
|
||||
//select a random type change
|
||||
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(t => t.Probability))
|
||||
{
|
||||
var selectedTypeChange =
|
||||
ToolBox.SelectWeightedRandom(readyTypeChanges, readyTypeChanges.Select(t => t.Probability).ToList(), Rand.RandSync.Unsynced);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowedTypeChanges.Count > 0)
|
||||
{
|
||||
location.TypeChangeTimer++;
|
||||
}
|
||||
else
|
||||
{
|
||||
location.TypeChangeTimer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change);
|
||||
partial void ClearAnimQueue();
|
||||
|
||||
public static Map LoadNew(XElement element)
|
||||
{
|
||||
string mapSeed = element.GetAttributeString("seed", "a");
|
||||
Map map = new Map(mapSeed);
|
||||
map.Load(element, false);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public void Load(XElement element, bool showNotifications)
|
||||
{
|
||||
ClearAnimQueue();
|
||||
SetLocation(element.GetAttributeInt("currentlocation", 0));
|
||||
|
||||
if (!Version.TryParse(element.GetAttributeString("version", ""), out _))
|
||||
{
|
||||
DebugConsole.ThrowError("Incompatible map save file, loading the game failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "location":
|
||||
string locationType = subElement.GetAttributeString("type", "");
|
||||
Location location = Locations[subElement.GetAttributeInt("i", 0)];
|
||||
int typeChangeTimer = subElement.GetAttributeInt("changetimer", 0);
|
||||
int missionsCompleted = subElement.GetAttributeInt("missionscompleted", 0);
|
||||
|
||||
string prevLocationName = location.Name;
|
||||
LocationType prevLocationType = location.Type;
|
||||
location.Discovered = true;
|
||||
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)));
|
||||
location.TypeChangeTimer = typeChangeTimer;
|
||||
location.MissionsCompleted = missionsCompleted;
|
||||
if (showNotifications && prevLocationType != location.Type)
|
||||
{
|
||||
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (change != null)
|
||||
{
|
||||
ChangeLocationType(location, prevLocationName, change);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "connection":
|
||||
int connectionIndex = subElement.GetAttributeInt("i", 0);
|
||||
connections[connectionIndex].Passed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(XElement element)
|
||||
{
|
||||
XElement mapElement = new XElement("map");
|
||||
|
||||
mapElement.Add(new XAttribute("version", GameMain.Version.ToString()));
|
||||
mapElement.Add(new XAttribute("currentlocation", CurrentLocationIndex));
|
||||
mapElement.Add(new XAttribute("seed", Seed));
|
||||
|
||||
for (int i = 0; i < Locations.Count; i++)
|
||||
{
|
||||
var location = Locations[i];
|
||||
if (!location.Discovered) continue;
|
||||
|
||||
var locationElement = new XElement("location", new XAttribute("i", i));
|
||||
locationElement.Add(new XAttribute("type", location.Type.Identifier));
|
||||
if (location.TypeChangeTimer > 0)
|
||||
{
|
||||
locationElement.Add(new XAttribute("changetimer", location.TypeChangeTimer));
|
||||
}
|
||||
|
||||
location.CheckMissionCompleted();
|
||||
if (location.MissionsCompleted > 0)
|
||||
{
|
||||
locationElement.Add(new XAttribute("missionscompleted", location.MissionsCompleted));
|
||||
}
|
||||
|
||||
mapElement.Add(locationElement);
|
||||
}
|
||||
|
||||
for (int i = 0; i < connections.Count; i++)
|
||||
{
|
||||
var connection = connections[i];
|
||||
if (!connection.Passed) continue;
|
||||
|
||||
var connectionElement = new XElement("connection",
|
||||
new XAttribute("i", i),
|
||||
new XAttribute("passed", connection.Passed));
|
||||
|
||||
mapElement.Add(connectionElement);
|
||||
}
|
||||
|
||||
element.Add(mapElement);
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.Remove();
|
||||
}
|
||||
RemoveProjSpecific();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class MapGenerationParams : ISerializableEntity
|
||||
{
|
||||
private static MapGenerationParams instance;
|
||||
private static string loadedFile;
|
||||
public static MapGenerationParams Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Serialize(false, true), Editable]
|
||||
public bool ShowNoiseMap { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool ShowLocations { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool ShowLevelTypeNames { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool ShowOverlay { get; set; }
|
||||
#else
|
||||
public readonly bool ShowLocations = true;
|
||||
public readonly bool ShowLevelTypeNames = false;
|
||||
public readonly bool ShowOverlay = true;
|
||||
#endif
|
||||
|
||||
[Serialize(6, true)]
|
||||
public int DifficultyZones { get; set; } //Number of difficulty zones
|
||||
|
||||
[Serialize(2000, true)]
|
||||
public int Size { get; set; }
|
||||
|
||||
[Serialize(20.0f, true, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)]
|
||||
public float SmallLevelConnectionLength { get; set; }
|
||||
|
||||
[Serialize(200.0f, true, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)]
|
||||
public float LargeLevelConnectionLength { get; set; }
|
||||
|
||||
[Serialize(1024, true)]
|
||||
public int NoiseResolution { get; set; } //Resolution of the noisemap overlay
|
||||
|
||||
[Serialize(10.0f, true), Editable(0.0f, 1000.0f)]
|
||||
public float NoiseFrequency { get; set; }
|
||||
|
||||
[Serialize(8, true), Editable(1, 100)]
|
||||
public int NoiseOctaves { get; set; }
|
||||
|
||||
[Serialize(0.5f, true), Editable(0.0f, 1.0f)]
|
||||
public float NoisePersistence { get; set; }
|
||||
|
||||
[Serialize("200,200", true), Editable]
|
||||
public Vector2 TileSpriteSize { get; set; }
|
||||
[Serialize("280,80", true), Editable]
|
||||
public Vector2 TileSpriteSpacing { get; set; }
|
||||
|
||||
[Serialize(1.0f, true, description: "How dark the center of the map is (1.0f = black)."), Editable(0.0f, 1.0f)]
|
||||
public float CenterDarkenStrength { get; set; }
|
||||
|
||||
[Serialize(0.9f, true, description: "How close to the center the darkening starts (0.8f = 20% from the edge)."), Editable(0.0f, 1.0f)]
|
||||
public float CenterDarkenRadius { get; set; }
|
||||
|
||||
[Serialize(5, true, description: "The edge of the dark center area is wave-shaped, and the frequency is determined by this value." +
|
||||
" I.e. how many points does the star-shaped dark area in the center have."), Editable(0, 1000)]
|
||||
public int CenterDarkenWaveFrequency { get; set; }
|
||||
|
||||
[Serialize(15.0f, true, description: "How heavily the noise map affects the phase of the edge wave (higher value = more irregular shape)."), Editable(0, 1000.0f)]
|
||||
public float CenterDarkenWavePhaseNoise { get; set; }
|
||||
|
||||
[Serialize(0.8f, true, description: "How dark the edges of the map are (1.0f = black)."), Editable(0.0f, 1.0f)]
|
||||
public float EdgeDarkenStrength { get; set; }
|
||||
|
||||
[Serialize(0.9f, true, description: "How far from the center the darkening starts (0.95f = 5% from the edge)."), Editable(0.0f, 1.0f)]
|
||||
public float EdgeDarkenRadius { get; set; }
|
||||
|
||||
[Serialize(0.9f, true, description: "How far from the center locations can be placed."), Editable(0.0f, 1.0f)]
|
||||
public float LocationRadius { get; set; }
|
||||
|
||||
[Serialize(20.0f, true, description: "How far from each other voronoi sites are placed. " +
|
||||
"Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " +
|
||||
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable(1.0f, 100.0f)]
|
||||
public float VoronoiSiteInterval { get; set; }
|
||||
|
||||
[Serialize(0.3f, true, description: "How likely it is for a site to be placed at a given spot (e.g. 20% probability for a site to be placed every 5 units of the map). " +
|
||||
"Multiplied with the noise value in the spot, meaning that sites are less likely to appear in dark spots."), Editable(0.01f, 1.0f)]
|
||||
public float VoronoiSitePlacementProbability { get; set; }
|
||||
|
||||
[Serialize(0.1f, true, description: "Probability * noise ^ 2 must be higher than this for a site to be placed. " +
|
||||
"= How bright the noise map must be at a given spot for a location to be placed there"), Editable(0.01f, 1.0f)]
|
||||
public float VoronoiSitePlacementMinVal { get; set; }
|
||||
|
||||
[Serialize(10.0f, true, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)]
|
||||
public float MinConnectionDistance { get; set; }
|
||||
|
||||
[Serialize(5.0f, true, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)]
|
||||
public float MinLocationDistance { get; set; }
|
||||
|
||||
[Serialize(0.2f, true, description: "Affects how many iterations are done when generating the jagged shape of the connections (iterations = Sqrt(connectionLength * multiplier))."), Editable(0.0f, 10.0f)]
|
||||
public float ConnectionIterationMultiplier { get; set; }
|
||||
|
||||
[Serialize(0.5f, true, description: "How large the \"bends\" in the connections are (displacement = connectionLength * multiplier)."), Editable(0.0f, 10.0f)]
|
||||
public float ConnectionDisplacementMultiplier { get; set; }
|
||||
|
||||
[Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)]
|
||||
public float ConnectionIndicatorIterationMultiplier { get; set; }
|
||||
|
||||
[Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)]
|
||||
public float ConnectionIndicatorDisplacementMultiplier { get; set; }
|
||||
|
||||
public Sprite ConnectionSprite { get; private set; }
|
||||
|
||||
#if CLIENT
|
||||
|
||||
[Serialize(15.0f, true, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
|
||||
public float LocationIconSize { get; set; }
|
||||
|
||||
[Serialize("150,150,150,255", true, description: "The color used to display the low-difficulty connections on the map."), Editable()]
|
||||
public Color LowDifficultyColor { get; set; }
|
||||
[Serialize("210,143,83,255", true, description: "The color used to display the medium-difficulty connections on the map."), Editable()]
|
||||
public Color MediumDifficultyColor { get; set; }
|
||||
[Serialize("216,154,138", true, description: "The color used to display the high-difficulty connections on the map."), Editable()]
|
||||
public Color HighDifficultyColor { get; set; }
|
||||
|
||||
public SpriteSheet DecorativeMapSprite { get; private set; }
|
||||
public SpriteSheet DecorativeGraphSprite { get; private set; }
|
||||
public SpriteSheet DecorativeLineTop { get; private set; }
|
||||
public SpriteSheet DecorativeLineBottom { get; private set; }
|
||||
public SpriteSheet DecorativeLineCorner { get; private set; }
|
||||
|
||||
public SpriteSheet ReticleLarge { get; private set; }
|
||||
public SpriteSheet ReticleMedium { get; private set; }
|
||||
public SpriteSheet ReticleSmall { get; private set; }
|
||||
|
||||
public Sprite MapCircle { get; private set; }
|
||||
public Sprite LocationIndicator { get; private set; }
|
||||
#endif
|
||||
|
||||
public List<Sprite> BackgroundTileSprites { get; private set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return GetType().ToString(); }
|
||||
}
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
|
||||
var files = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.MapGenerationParameters);
|
||||
if (!files.Any())
|
||||
{
|
||||
DebugConsole.ThrowError("No map generation parameters found in the selected content packages!");
|
||||
return;
|
||||
}
|
||||
// Let's not actually load the parameters until we have solved which file is the last, because loading the parameters takes some resources that would also need to be released.
|
||||
XElement selectedElement = null;
|
||||
string selectedFile = null;
|
||||
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();
|
||||
if (selectedElement != null)
|
||||
{
|
||||
DebugConsole.NewMessage($"Overriding the map generation parameters with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
}
|
||||
else if (selectedElement != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {file.Path}: Another map generation parameter file already loaded! Use <override></override> tags to override it.");
|
||||
break;
|
||||
}
|
||||
selectedElement = mainElement;
|
||||
selectedFile = file.Path;
|
||||
}
|
||||
|
||||
if (selectedFile == loadedFile) { return; }
|
||||
|
||||
instance?.ConnectionSprite?.Remove();
|
||||
instance?.BackgroundTileSprites.ForEach(s => s.Remove());
|
||||
#if CLIENT
|
||||
instance?.MapCircle?.Remove();
|
||||
instance?.LocationIndicator?.Remove();
|
||||
instance?.DecorativeMapSprite?.Remove();
|
||||
instance?.DecorativeGraphSprite?.Remove();
|
||||
instance?.DecorativeLineTop?.Remove();
|
||||
instance?.DecorativeLineBottom?.Remove();
|
||||
instance?.DecorativeLineCorner?.Remove();
|
||||
instance?.ReticleLarge?.Remove();
|
||||
instance?.ReticleMedium?.Remove();
|
||||
instance?.ReticleSmall?.Remove();
|
||||
#endif
|
||||
instance = null;
|
||||
|
||||
if (selectedElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find a valid element in the map generation parameter files!");
|
||||
}
|
||||
else
|
||||
{
|
||||
instance = new MapGenerationParams(selectedElement);
|
||||
loadedFile = selectedFile;
|
||||
}
|
||||
}
|
||||
|
||||
private MapGenerationParams(XElement element)
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
BackgroundTileSprites = new List<Sprite>();
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "connectionsprite":
|
||||
ConnectionSprite = new Sprite(subElement);
|
||||
break;
|
||||
case "backgroundtile":
|
||||
BackgroundTileSprites.Add(new Sprite(subElement));
|
||||
break;
|
||||
#if CLIENT
|
||||
case "mapcircle":
|
||||
MapCircle = new Sprite(subElement);
|
||||
break;
|
||||
case "locationindicator":
|
||||
LocationIndicator = new Sprite(subElement);
|
||||
break;
|
||||
case "decorativemapsprite":
|
||||
DecorativeMapSprite = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "decorativegraphsprite":
|
||||
DecorativeGraphSprite = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "decorativelinetop":
|
||||
DecorativeLineTop = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "decorativelinebottom":
|
||||
DecorativeLineBottom = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "decorativelinecorner":
|
||||
DecorativeLineCorner = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "reticlelarge":
|
||||
ReticleLarge = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "reticlemedium":
|
||||
ReticleMedium = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "reticlesmall":
|
||||
ReticleSmall = new SpriteSheet(subElement);
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract partial class MapEntity : Entity
|
||||
{
|
||||
public static List<MapEntity> mapEntityList = new List<MapEntity>();
|
||||
|
||||
public readonly MapEntityPrefab prefab;
|
||||
|
||||
protected List<ushort> linkedToID;
|
||||
|
||||
//observable collection because some entities may need to be notified when the collection is modified
|
||||
public readonly ObservableCollection<MapEntity> linkedTo = new ObservableCollection<MapEntity>();
|
||||
|
||||
private bool flippedX, flippedY;
|
||||
public bool FlippedX { get { return flippedX; } }
|
||||
public bool FlippedY { get { return flippedY; } }
|
||||
|
||||
public bool ShouldBeSaved = true;
|
||||
|
||||
//the position and dimensions of the entity
|
||||
protected Rectangle rect;
|
||||
|
||||
public bool ExternalHighlight = false;
|
||||
|
||||
//is the mouse inside the rect
|
||||
private bool isHighlighted;
|
||||
|
||||
public event Action<Rectangle> Resized;
|
||||
|
||||
public bool IsHighlighted
|
||||
{
|
||||
get { return isHighlighted || ExternalHighlight; }
|
||||
set { isHighlighted = value; }
|
||||
}
|
||||
|
||||
public virtual Rectangle Rect
|
||||
{
|
||||
get { return rect; }
|
||||
set { rect = value; }
|
||||
}
|
||||
|
||||
public Rectangle WorldRect
|
||||
{
|
||||
get { return Submarine == null ? rect : new Rectangle((int)(Submarine.Position.X + rect.X), (int)(Submarine.Position.Y + rect.Y), rect.Width, rect.Height); }
|
||||
}
|
||||
|
||||
public virtual Sprite Sprite
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public virtual bool DrawBelowWater
|
||||
{
|
||||
get
|
||||
{
|
||||
return Sprite != null && SpriteDepth > 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool DrawOverWater
|
||||
{
|
||||
get
|
||||
{
|
||||
return !DrawBelowWater;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool Linkable
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public List<string> AllowedLinks => prefab == null ? new List<string>() : prefab.AllowedLinks;
|
||||
|
||||
public bool ResizeHorizontal
|
||||
{
|
||||
get { return prefab != null && prefab.ResizeHorizontal; }
|
||||
}
|
||||
public bool ResizeVertical
|
||||
{
|
||||
get { return prefab != null && prefab.ResizeVertical; }
|
||||
}
|
||||
|
||||
//for upgrading the dimensions of the entity from xml
|
||||
[Serialize(0, false)]
|
||||
public int RectWidth
|
||||
{
|
||||
get { return rect.Width; }
|
||||
set
|
||||
{
|
||||
if (value <= 0) { return; }
|
||||
Rect = new Rectangle(rect.X, rect.Y, value, rect.Height);
|
||||
}
|
||||
}
|
||||
//for upgrading the dimensions of the entity from xml
|
||||
[Serialize(0, false)]
|
||||
public int RectHeight
|
||||
{
|
||||
get { return rect.Height; }
|
||||
set
|
||||
{
|
||||
if (value <= 0) { return; }
|
||||
Rect = new Rectangle(rect.X, rect.Y, rect.Width, value);
|
||||
}
|
||||
}
|
||||
|
||||
public override Vector2 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector2 rectPos = new Vector2(
|
||||
rect.X + rect.Width / 2.0f,
|
||||
rect.Y - rect.Height / 2.0f);
|
||||
|
||||
//if (MoveWithLevel) rectPos += Level.Loaded.Position;
|
||||
return rectPos;
|
||||
}
|
||||
}
|
||||
|
||||
public override Vector2 SimPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return ConvertUnits.ToSimUnits(Position);
|
||||
}
|
||||
}
|
||||
|
||||
public float SoundRange
|
||||
{
|
||||
get
|
||||
{
|
||||
if (aiTarget == null) return 0.0f;
|
||||
return aiTarget.SoundRange;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (aiTarget == null) return;
|
||||
aiTarget.SoundRange = value;
|
||||
}
|
||||
}
|
||||
|
||||
public float SightRange
|
||||
{
|
||||
get
|
||||
{
|
||||
if (aiTarget == null) return 0.0f;
|
||||
return aiTarget.SightRange;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (aiTarget == null) return;
|
||||
aiTarget.SightRange = value;
|
||||
}
|
||||
}
|
||||
|
||||
public RuinGeneration.Ruin ParentRuin
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get { return ""; }
|
||||
}
|
||||
|
||||
// Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation.
|
||||
private Memento<Rectangle> rectMemento;
|
||||
|
||||
public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine)
|
||||
{
|
||||
this.prefab = prefab;
|
||||
Scale = prefab != null ? prefab.Scale : 1;
|
||||
}
|
||||
|
||||
public virtual void Move(Vector2 amount)
|
||||
{
|
||||
rect.X += (int)amount.X;
|
||||
rect.Y += (int)amount.Y;
|
||||
}
|
||||
|
||||
public virtual bool IsMouseOn(Vector2 position)
|
||||
{
|
||||
return (Submarine.RectContains(WorldRect, position));
|
||||
}
|
||||
|
||||
public abstract MapEntity Clone();
|
||||
|
||||
public static List<MapEntity> Clone(List<MapEntity> entitiesToClone)
|
||||
{
|
||||
List<MapEntity> clones = new List<MapEntity>();
|
||||
foreach (MapEntity e in entitiesToClone)
|
||||
{
|
||||
Debug.Assert(e != null);
|
||||
try
|
||||
{
|
||||
clones.Add(e.Clone());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugConsole.ThrowError("Cloning entity \"" + e.Name + "\" failed.", ex);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"MapEntity.Clone:" + e.Name,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace);
|
||||
return clones;
|
||||
}
|
||||
Debug.Assert(clones.Last() != null);
|
||||
}
|
||||
|
||||
Debug.Assert(clones.Count == entitiesToClone.Count);
|
||||
|
||||
//clone links between the entities
|
||||
for (int i = 0; i < clones.Count; i++)
|
||||
{
|
||||
if (entitiesToClone[i].linkedTo == null) { continue; }
|
||||
foreach (MapEntity linked in entitiesToClone[i].linkedTo)
|
||||
{
|
||||
if (!entitiesToClone.Contains(linked)) { continue; }
|
||||
clones[i].linkedTo.Add(clones[entitiesToClone.IndexOf(linked)]);
|
||||
}
|
||||
}
|
||||
|
||||
//connect clone wires to the clone items and refresh links between doors and gaps
|
||||
for (int i = 0; i < clones.Count; i++)
|
||||
{
|
||||
var cloneItem = clones[i] as Item;
|
||||
if (cloneItem == null) { continue; }
|
||||
|
||||
var door = cloneItem.GetComponent<Door>();
|
||||
if (door != null) { door.RefreshLinkedGap(); }
|
||||
|
||||
var cloneWire = cloneItem.GetComponent<Wire>();
|
||||
if (cloneWire == null) continue;
|
||||
|
||||
var originalWire = ((Item)entitiesToClone[i]).GetComponent<Wire>();
|
||||
|
||||
cloneWire.SetNodes(originalWire.GetNodes());
|
||||
|
||||
for (int n = 0; n < 2; n++)
|
||||
{
|
||||
if (originalWire.Connections[n] == null) { continue; }
|
||||
|
||||
var connectedItem = originalWire.Connections[n].Item;
|
||||
if (connectedItem == null) continue;
|
||||
|
||||
//index of the item the wire is connected to
|
||||
int itemIndex = entitiesToClone.IndexOf(connectedItem);
|
||||
if (itemIndex < 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone.");
|
||||
GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectedNotFound" + connectedItem.ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone.");
|
||||
continue;
|
||||
}
|
||||
|
||||
//index of the connection in the connectionpanel of the target item
|
||||
int connectionIndex = connectedItem.Connections.IndexOf(originalWire.Connections[n]);
|
||||
if (connectionIndex < 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\".");
|
||||
GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectionNotFound" + connectedItem.ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\".");
|
||||
continue;
|
||||
}
|
||||
|
||||
(clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire);
|
||||
cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false);
|
||||
}
|
||||
}
|
||||
|
||||
return clones;
|
||||
}
|
||||
|
||||
protected void InsertToList()
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
if (Sprite == null)
|
||||
{
|
||||
mapEntityList.Add(this);
|
||||
return;
|
||||
}
|
||||
|
||||
while (i < mapEntityList.Count)
|
||||
{
|
||||
i++;
|
||||
|
||||
Sprite existingSprite = mapEntityList[i - 1].Sprite;
|
||||
if (existingSprite == null) continue;
|
||||
#if CLIENT
|
||||
if (existingSprite.Texture == this.Sprite.Texture) break;
|
||||
#endif
|
||||
}
|
||||
|
||||
mapEntityList.Insert(i, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the entity from the entity list without removing links to other entities
|
||||
/// </summary>
|
||||
public virtual void ShallowRemove()
|
||||
{
|
||||
base.Remove();
|
||||
|
||||
mapEntityList.Remove(this);
|
||||
|
||||
if (aiTarget != null) aiTarget.Remove();
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
base.Remove();
|
||||
|
||||
mapEntityList.Remove(this);
|
||||
|
||||
#if CLIENT
|
||||
if (selectedList.Contains(this))
|
||||
{
|
||||
selectedList = selectedList.FindAll(e => e != this);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (aiTarget != null) aiTarget.Remove();
|
||||
|
||||
if (linkedTo != null)
|
||||
{
|
||||
for (int i = linkedTo.Count - 1; i >= 0; i--)
|
||||
{
|
||||
linkedTo[i].RemoveLinked(this);
|
||||
}
|
||||
linkedTo.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call Update() on every object in Entity.list
|
||||
/// </summary>
|
||||
public static void UpdateAll(float deltaTime, Camera cam)
|
||||
{
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
hull.Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
foreach (Structure structure in Structure.WallList)
|
||||
{
|
||||
structure.Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
|
||||
//update gaps in random order, because otherwise in rooms with multiple gaps
|
||||
//the water/air will always tend to flow through the first gap in the list,
|
||||
//which may lead to weird behavior like water draining down only through
|
||||
//one gap in a room even if there are several
|
||||
foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue)))
|
||||
{
|
||||
gap.Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
Powered.UpdatePower(deltaTime);
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
item.Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
UpdateAllProjSpecific(deltaTime);
|
||||
|
||||
Spawner?.Update();
|
||||
}
|
||||
|
||||
static partial void UpdateAllProjSpecific(float deltaTime);
|
||||
|
||||
public virtual void Update(float deltaTime, Camera cam) { }
|
||||
|
||||
/// <summary>
|
||||
/// Flip the entity horizontally
|
||||
/// </summary>
|
||||
/// <param name="relativeToSub">Should the entity be flipped across the y-axis of the sub it's inside</param>
|
||||
public virtual void FlipX(bool relativeToSub)
|
||||
{
|
||||
flippedX = !flippedX;
|
||||
if (!relativeToSub || Submarine == null) return;
|
||||
|
||||
Vector2 relative = WorldPosition - Submarine.WorldPosition;
|
||||
relative.Y = 0.0f;
|
||||
Move(-relative * 2.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flip the entity vertically
|
||||
/// </summary>
|
||||
/// <param name="relativeToSub">Should the entity be flipped across the x-axis of the sub it's inside</param>
|
||||
public virtual void FlipY(bool relativeToSub)
|
||||
{
|
||||
flippedY = !flippedY;
|
||||
if (!relativeToSub || Submarine == null) return;
|
||||
|
||||
Vector2 relative = WorldPosition - Submarine.WorldPosition;
|
||||
relative.X = 0.0f;
|
||||
Move(-relative * 2.0f);
|
||||
}
|
||||
|
||||
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath)
|
||||
{
|
||||
List<MapEntity> entities = new List<MapEntity>();
|
||||
foreach (XElement element in parentElement.Elements())
|
||||
{
|
||||
string typeName = element.Name.ToString();
|
||||
|
||||
Type t;
|
||||
try
|
||||
{
|
||||
t = Type.GetType("Barotrauma." + typeName, true, true);
|
||||
if (t == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type \"" + typeName + "\".");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type \"" + typeName + "\".", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine) });
|
||||
if (loadMethod == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + ".");
|
||||
}
|
||||
else if (!loadMethod.ReturnType.IsSubclassOf(typeof(MapEntity)))
|
||||
{
|
||||
DebugConsole.ThrowError("Error loading entity of the type \"" + t.ToString() + "\" - load method does not return a valid map entity.");
|
||||
}
|
||||
else
|
||||
{
|
||||
object newEntity = loadMethod.Invoke(t, new object[] { element, submarine });
|
||||
if (newEntity != null) entities.Add((MapEntity)newEntity);
|
||||
}
|
||||
}
|
||||
catch (TargetInvocationException e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e);
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the linkedTo-lists of the entities based on the linkedToID-lists
|
||||
/// Has to be done after all the entities have been loaded (an entity can't
|
||||
/// be linked to some other entity that hasn't been loaded yet)
|
||||
/// </summary>
|
||||
private bool mapLoadedCalled;
|
||||
public static void MapLoaded(List<MapEntity> entities, bool updateHulls)
|
||||
{
|
||||
foreach (MapEntity e in entities)
|
||||
{
|
||||
if (e.mapLoadedCalled) continue;
|
||||
if (e.linkedToID == null) continue;
|
||||
if (e.linkedToID.Count == 0) continue;
|
||||
|
||||
e.linkedTo.Clear();
|
||||
|
||||
foreach (ushort i in e.linkedToID)
|
||||
{
|
||||
if (FindEntityByID(i) is MapEntity linked) e.linkedTo.Add(linked);
|
||||
}
|
||||
}
|
||||
|
||||
List<LinkedSubmarine> linkedSubs = new List<LinkedSubmarine>();
|
||||
for (int i = 0; i < entities.Count; i++)
|
||||
{
|
||||
if (entities[i].mapLoadedCalled) continue;
|
||||
if (entities[i] is LinkedSubmarine)
|
||||
{
|
||||
linkedSubs.Add((LinkedSubmarine)entities[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
entities[i].OnMapLoaded();
|
||||
}
|
||||
|
||||
if (updateHulls)
|
||||
{
|
||||
Item.UpdateHulls();
|
||||
Gap.UpdateHulls();
|
||||
}
|
||||
|
||||
entities.ForEach(e => e.mapLoadedCalled = true);
|
||||
|
||||
foreach (LinkedSubmarine linkedSub in linkedSubs)
|
||||
{
|
||||
linkedSub.OnMapLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnMapLoaded() { }
|
||||
|
||||
public virtual XElement Save(XElement parentElement)
|
||||
{
|
||||
DebugConsole.ThrowError("Saving entity " + GetType() + " failed.");
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveLinked(MapEntity e)
|
||||
{
|
||||
if (linkedTo == null) return;
|
||||
if (linkedTo.Contains(e)) linkedTo.Remove(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all linked entities of specific type.
|
||||
/// </summary>
|
||||
public HashSet<T> GetLinkedEntities<T>(HashSet<T> list = null, int? maxDepth = null, Func<T, bool> filter = null) where T : MapEntity
|
||||
{
|
||||
list = list ?? new HashSet<T>();
|
||||
int startDepth = 0;
|
||||
GetLinkedEntitiesRecursive<T>(this, list, ref startDepth, maxDepth, filter);
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all linked entities of specific type.
|
||||
/// </summary>
|
||||
private static void GetLinkedEntitiesRecursive<T>(MapEntity mapEntity, HashSet<T> linkedTargets, ref int depth, int? maxDepth = null, Func<T, bool> filter = null)
|
||||
where T : MapEntity
|
||||
{
|
||||
if (depth > maxDepth) { return; }
|
||||
foreach (var linkedEntity in mapEntity.linkedTo)
|
||||
{
|
||||
if (linkedEntity is T linkedTarget)
|
||||
{
|
||||
if (!linkedTargets.Contains(linkedTarget) && (filter == null || filter(linkedTarget)))
|
||||
{
|
||||
linkedTargets.Add(linkedTarget);
|
||||
depth++;
|
||||
GetLinkedEntitiesRecursive(linkedEntity, linkedTargets, ref depth, maxDepth, filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Serialized properties
|
||||
// We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used.
|
||||
// Nullable on the other requires boxing that we don't want to do too often, since it generates garbage.
|
||||
public bool SpriteDepthOverrideIsSet { get; private set; }
|
||||
public float SpriteOverrideDepth => SpriteDepth;
|
||||
private float _spriteOverrideDepth = float.NaN;
|
||||
[Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)]
|
||||
public float SpriteDepth
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; }
|
||||
return Sprite != null ? Sprite.Depth : 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!float.IsNaN(value))
|
||||
{
|
||||
_spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f);
|
||||
if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); }
|
||||
SpriteDepthOverrideIsSet = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)]
|
||||
public virtual float Scale { get; set; } = 1;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
[Flags]
|
||||
enum MapEntityCategory
|
||||
{
|
||||
Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, ItemAssembly = 256, Legacy = 512
|
||||
}
|
||||
|
||||
abstract partial class MapEntityPrefab : IPrefab, IDisposable
|
||||
{
|
||||
public static IEnumerable<MapEntityPrefab> List
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var ep in CoreEntityPrefab.Prefabs)
|
||||
{
|
||||
yield return ep;
|
||||
}
|
||||
|
||||
foreach (var ep in StructurePrefab.Prefabs)
|
||||
{
|
||||
yield return ep;
|
||||
}
|
||||
|
||||
foreach (var ep in ItemPrefab.Prefabs)
|
||||
{
|
||||
yield return ep;
|
||||
}
|
||||
|
||||
foreach (var ep in ItemAssemblyPrefab.Prefabs)
|
||||
{
|
||||
yield return ep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected string originalName;
|
||||
protected string identifier;
|
||||
|
||||
public Sprite sprite;
|
||||
|
||||
//the position where the structure is being placed (needed when stretching the structure)
|
||||
protected static Vector2 placePosition;
|
||||
|
||||
protected ConstructorInfo constructor;
|
||||
|
||||
//is it possible to stretch the entity horizontally/vertically
|
||||
[Serialize(false, false)]
|
||||
public bool ResizeHorizontal { get; protected set; }
|
||||
[Serialize(false, false)]
|
||||
public bool ResizeVertical { get; protected set; }
|
||||
|
||||
//which prefab has been selected for placing
|
||||
protected static MapEntityPrefab selected;
|
||||
|
||||
public string OriginalName
|
||||
{
|
||||
get { return originalName; }
|
||||
}
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get { return originalName; }
|
||||
}
|
||||
|
||||
public string GetItemNameTextId()
|
||||
{
|
||||
var textId = $"entityname.{Identifier}";
|
||||
return TextManager.ContainsTag(textId) ? textId : null;
|
||||
}
|
||||
|
||||
public string GetHullNameTextId()
|
||||
{
|
||||
var textId = $"roomname.{Identifier}";
|
||||
return TextManager.ContainsTag(textId) ? textId : null;
|
||||
}
|
||||
|
||||
//Used to differentiate between items when saving/loading
|
||||
//Allows changing the name of an item without breaking existing subs or having multiple items with the same name
|
||||
public string Identifier
|
||||
{
|
||||
get { return identifier; }
|
||||
}
|
||||
|
||||
public string FilePath { get; protected set; }
|
||||
|
||||
public ContentPackage ContentPackage { get; protected set; }
|
||||
|
||||
public HashSet<string> Tags
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
} = new HashSet<string>();
|
||||
|
||||
public static MapEntityPrefab Selected
|
||||
{
|
||||
get { return selected; }
|
||||
set { selected = value; }
|
||||
}
|
||||
|
||||
[Serialize("", false)]
|
||||
public string Description
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool HideInMenus { get; set; }
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool Linkable
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Links defined to identifiers.
|
||||
/// </summary>
|
||||
public List<string> AllowedLinks { get; protected set; } = new List<string>();
|
||||
|
||||
public MapEntityCategory Category
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
[Serialize("1.0,1.0,1.0,1.0", false)]
|
||||
public Color SpriteColor
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
[Serialize(1f, true), Editable(0.1f, 10f, DecimalCount = 3)]
|
||||
public float Scale { get; protected set; }
|
||||
|
||||
//If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias.
|
||||
//(allows changing names while keeping backwards compatibility with older sub files)
|
||||
public HashSet<string> Aliases
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
CoreEntityPrefab ep = new CoreEntityPrefab
|
||||
{
|
||||
identifier = "hull",
|
||||
originalName = TextManager.Get("EntityName.hull"),
|
||||
Description = TextManager.Get("EntityDescription.hull"),
|
||||
constructor = typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
|
||||
ResizeHorizontal = true,
|
||||
ResizeVertical = true,
|
||||
Linkable = true
|
||||
};
|
||||
ep.AllowedLinks.Add("hull");
|
||||
ep.Aliases = new HashSet<string> { "hull" };
|
||||
CoreEntityPrefab.Prefabs.Add(ep, false);
|
||||
|
||||
ep = new CoreEntityPrefab
|
||||
{
|
||||
identifier = "gap",
|
||||
originalName = TextManager.Get("EntityName.gap"),
|
||||
Description = TextManager.Get("EntityDescription.gap"),
|
||||
constructor = typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }),
|
||||
ResizeHorizontal = true,
|
||||
ResizeVertical = true
|
||||
};
|
||||
CoreEntityPrefab.Prefabs.Add(ep, false);
|
||||
ep.Aliases = new HashSet<string> { "gap" };
|
||||
|
||||
ep = new CoreEntityPrefab
|
||||
{
|
||||
identifier = "waypoint",
|
||||
originalName = TextManager.Get("EntityName.waypoint"),
|
||||
Description = TextManager.Get("EntityDescription.waypoint"),
|
||||
constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })
|
||||
};
|
||||
CoreEntityPrefab.Prefabs.Add(ep, false);
|
||||
ep.Aliases = new HashSet<string> { "waypoint" };
|
||||
|
||||
ep = new CoreEntityPrefab
|
||||
{
|
||||
identifier = "spawnpoint",
|
||||
originalName = TextManager.Get("EntityName.spawnpoint"),
|
||||
Description = TextManager.Get("EntityDescription.spawnpoint"),
|
||||
constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })
|
||||
};
|
||||
CoreEntityPrefab.Prefabs.Add(ep, false);
|
||||
ep.Aliases = new HashSet<string> { "spawnpoint" };
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public MapEntityPrefab()
|
||||
{
|
||||
Category = MapEntityCategory.Structure;
|
||||
}
|
||||
|
||||
protected virtual void CreateInstance(Rectangle rect)
|
||||
{
|
||||
if (constructor == null) return;
|
||||
object[] lobject = new object[] { this, rect };
|
||||
constructor.Invoke(lobject);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public void DebugCreateInstance()
|
||||
{
|
||||
Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y));
|
||||
CreateInstance(rect);
|
||||
}
|
||||
#endif
|
||||
|
||||
public static bool SelectPrefab(object selection)
|
||||
{
|
||||
if ((selected = selection as MapEntityPrefab) != null)
|
||||
{
|
||||
placePosition = Vector2.Zero;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find a matching map entity prefab
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the item (can be omitted when searching based on identifier)</param>
|
||||
/// <param name="identifier">The identifier of the item (if null, the identifier is ignored and the search is done only based on the name)</param>
|
||||
public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
}
|
||||
foreach (MapEntityPrefab prefab in List)
|
||||
{
|
||||
if (identifier != null)
|
||||
{
|
||||
if (prefab.identifier != identifier)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) { return prefab; }
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) ||
|
||||
prefab.originalName.Equals(name, StringComparison.OrdinalIgnoreCase) ||
|
||||
(prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
return prefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showErrorMessages)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to find a matching MapEntityPrefab (name: \"" + name + "\", identifier: \"" + identifier + "\").\n" + Environment.StackTrace);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find a matching map entity prefab
|
||||
/// </summary>
|
||||
/// <param name="predicate">A predicate that returns true on the desired prefab.</param>
|
||||
public static MapEntityPrefab Find(Predicate<MapEntityPrefab> predicate)
|
||||
{
|
||||
return List.FirstOrDefault(p => predicate(p));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the name or any of the aliases of this prefab match the given name.
|
||||
/// </summary>
|
||||
public bool NameMatches(string name, StringComparison comparisonType) => originalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType)));
|
||||
|
||||
public bool NameMatches(IEnumerable<string> allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType));
|
||||
|
||||
public bool IsLinkAllowed(MapEntityPrefab target)
|
||||
{
|
||||
if (target == null) { return false; }
|
||||
return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier)
|
||||
|| target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t));
|
||||
}
|
||||
|
||||
//a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected
|
||||
public static object GetSelected()
|
||||
{
|
||||
return (object)selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public class Md5Hash
|
||||
{
|
||||
private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private const string cachePath = "Data/hashcache.txt";
|
||||
private static readonly Dictionary<string, Tuple<Md5Hash, long>> cache = new Dictionary<string, Tuple<Md5Hash, long>>();
|
||||
|
||||
public static void LoadCache()
|
||||
{
|
||||
if (!File.Exists(cachePath)) { return; }
|
||||
string[] lines = File.ReadAllLines(cachePath);
|
||||
if (lines.Length <= 0 || lines[0] != GameMain.Version.ToString()) { return; }
|
||||
foreach (string line in lines.Skip(1))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) { continue; }
|
||||
string[] parts = line.Split('|');
|
||||
if (parts.Length < 3) { continue; }
|
||||
|
||||
string path = parts[0].CleanUpPath();
|
||||
string hashStr = parts[1];
|
||||
long timeLong = long.Parse(parts[2]);
|
||||
|
||||
Md5Hash hash = new Md5Hash(hashStr);
|
||||
DateTime time = DateTime.FromBinary(timeLong);
|
||||
|
||||
if (File.GetLastWriteTime(path) == time && !cache.ContainsKey(path))
|
||||
{
|
||||
cache.Add(path, new Tuple<Md5Hash, long>(hash, timeLong));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveCache()
|
||||
{
|
||||
string[] lines = new string[cache.Count+1];
|
||||
lines[0] = GameMain.Version.ToString();
|
||||
int i = 1;
|
||||
foreach (KeyValuePair<string, Tuple<Md5Hash, long>> kpv in cache)
|
||||
{
|
||||
lines[i] = kpv.Key + "|" + kpv.Value.Item1 + "|" + kpv.Value.Item2;
|
||||
i++;
|
||||
}
|
||||
File.WriteAllLines(cachePath, lines);
|
||||
}
|
||||
|
||||
private bool LoadFromCache(string filename)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(filename))
|
||||
{
|
||||
filename = filename.CleanUpPath();
|
||||
lock (cache)
|
||||
{
|
||||
if (cache.ContainsKey(filename))
|
||||
{
|
||||
Hash = cache[filename].Item1.Hash;
|
||||
ShortHash = cache[filename].Item1.ShortHash;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SaveToCache(string filename, long? time = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filename)) { return; }
|
||||
|
||||
lock (cache)
|
||||
{
|
||||
filename = filename.CleanUpPath();
|
||||
Tuple<Md5Hash, long> cacheVal = new Tuple<Md5Hash, long>(this, time ?? File.GetLastWriteTime(filename).ToBinary());
|
||||
if (cache.ContainsKey(filename))
|
||||
{
|
||||
cache[filename] = cacheVal;
|
||||
}
|
||||
else
|
||||
{
|
||||
cache.Add(filename, cacheVal);
|
||||
}
|
||||
SaveCache();
|
||||
}
|
||||
}
|
||||
|
||||
public static Md5Hash FetchFromCache(string filename)
|
||||
{
|
||||
Md5Hash newHash = new Md5Hash();
|
||||
if (newHash.LoadFromCache(filename)) { return newHash; }
|
||||
return null;
|
||||
}
|
||||
|
||||
public string Hash { get; private set; }
|
||||
|
||||
public string ShortHash { get; private set; }
|
||||
|
||||
private Md5Hash()
|
||||
{
|
||||
this.Hash = null;
|
||||
ShortHash = null;
|
||||
}
|
||||
|
||||
public Md5Hash(string md5Hash)
|
||||
{
|
||||
this.Hash = md5Hash;
|
||||
ShortHash = GetShortHash(md5Hash);
|
||||
}
|
||||
|
||||
public Md5Hash(byte[] bytes)
|
||||
{
|
||||
Hash = CalculateHash(bytes);
|
||||
|
||||
ShortHash = GetShortHash(Hash);
|
||||
}
|
||||
|
||||
public Md5Hash(FileStream fileStream, string filename = null, bool tryLoadFromCache = true)
|
||||
{
|
||||
if (tryLoadFromCache)
|
||||
{
|
||||
if (LoadFromCache(filename)) { return; }
|
||||
}
|
||||
|
||||
Hash = CalculateHash(fileStream);
|
||||
|
||||
ShortHash = GetShortHash(Hash);
|
||||
|
||||
SaveToCache(filename);
|
||||
}
|
||||
|
||||
public Md5Hash(XDocument doc, string filename = null, bool tryLoadFromCache = true)
|
||||
{
|
||||
if (tryLoadFromCache)
|
||||
{
|
||||
if (LoadFromCache(filename)) { return; }
|
||||
}
|
||||
|
||||
if (doc == null) { return; }
|
||||
|
||||
string docString = removeWhitespaceRegex.Replace(doc.ToString(), "");
|
||||
|
||||
byte[] inputBytes = Encoding.ASCII.GetBytes(docString);
|
||||
|
||||
Hash = CalculateHash(inputBytes);
|
||||
ShortHash = GetShortHash(Hash);
|
||||
|
||||
SaveToCache(filename);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Hash;
|
||||
}
|
||||
|
||||
private string CalculateHash(FileStream stream)
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
byte[] byteHash = md5.ComputeHash(stream);
|
||||
// step 2, convert byte array to hex string
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < byteHash.Length; i++)
|
||||
{
|
||||
sb.Append(byteHash[i].ToString("X2"));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private string CalculateHash(byte[] bytes)
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
byte[] byteHash = md5.ComputeHash(bytes);
|
||||
// step 2, convert byte array to hex string
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < byteHash.Length; i++)
|
||||
{
|
||||
sb.Append(byteHash[i].ToString("X2"));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetShortHash(string fullHash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullHash)) { return ""; }
|
||||
return fullHash.Length < 7 ? fullHash : fullHash.Substring(0, 7);
|
||||
}
|
||||
|
||||
public static bool RemoveFromCache(string filename)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(filename))
|
||||
{
|
||||
filename = filename.CleanUpPath();
|
||||
lock (cache)
|
||||
{
|
||||
if (cache.ContainsKey(filename))
|
||||
{
|
||||
cache.Remove(filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class PriceInfo
|
||||
{
|
||||
public readonly int BuyPrice;
|
||||
|
||||
//minimum number of items available at a given store
|
||||
public readonly int MinAvailableAmount;
|
||||
//maximum number of items available at a given store
|
||||
public readonly int MaxAvailableAmount;
|
||||
|
||||
public PriceInfo (XElement element)
|
||||
{
|
||||
BuyPrice = element.GetAttributeInt("buyprice", 0);
|
||||
MinAvailableAmount = element.GetAttributeInt("minamount", 0);
|
||||
MaxAvailableAmount = element.GetAttributeInt("maxamount", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class RoundEndCinematic
|
||||
{
|
||||
public bool Running
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Camera AssignedCamera;
|
||||
|
||||
private float duration;
|
||||
|
||||
private CoroutineHandle updateCoroutine;
|
||||
|
||||
public RoundEndCinematic(Submarine submarine, Camera cam, float duration = 10.0f)
|
||||
: this(new List<Submarine>() { submarine }, cam, duration)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public RoundEndCinematic(List<Submarine> submarines, Camera cam, float duration)
|
||||
{
|
||||
if (!submarines.Any(s => s != null)) return;
|
||||
|
||||
this.duration = duration;
|
||||
AssignedCamera = cam;
|
||||
|
||||
Running = true;
|
||||
updateCoroutine = CoroutineManager.StartCoroutine(Update(submarines, cam));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
CoroutineManager.StopCoroutines(updateCoroutine);
|
||||
Running = false;
|
||||
#if CLIENT
|
||||
GUI.ScreenOverlayColor = Color.TransparentBlack;
|
||||
#endif
|
||||
}
|
||||
|
||||
private IEnumerable<object> Update(List<Submarine> subs, Camera cam)
|
||||
{
|
||||
if (!subs.Any()) yield return CoroutineStatus.Success;
|
||||
|
||||
Character.Controlled = null;
|
||||
cam.TargetPos = Vector2.Zero;
|
||||
#if CLIENT
|
||||
GameMain.LightManager.LosEnabled = false;
|
||||
#endif
|
||||
|
||||
Level.Loaded.TopBarrier.Enabled = false;
|
||||
|
||||
cam.TargetPos = Vector2.Zero;
|
||||
float timer = 0.0f;
|
||||
float initialZoom = cam.Zoom;
|
||||
Vector2 initialCameraPos = cam.Position;
|
||||
|
||||
while (timer < duration)
|
||||
{
|
||||
if (Screen.Selected != GameMain.GameScreen)
|
||||
{
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
#if CLIENT
|
||||
GUI.ScreenOverlayColor = Color.TransparentBlack;
|
||||
#endif
|
||||
|
||||
Running = false;
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
Vector2 minPos = new Vector2(
|
||||
subs.Min(s => s.WorldPosition.X - s.Borders.Width / 2),
|
||||
subs.Min(s => s.WorldPosition.Y - s.Borders.Height / 2));
|
||||
Vector2 maxPos = new Vector2(
|
||||
subs.Min(s => s.WorldPosition.X + s.Borders.Width / 2),
|
||||
subs.Min(s => s.WorldPosition.Y + s.Borders.Height / 2));
|
||||
Vector2 cameraPos = new Vector2(
|
||||
MathHelper.SmoothStep(minPos.X, maxPos.X, timer / duration),
|
||||
(minPos.Y + maxPos.Y) / 2.0f);
|
||||
cam.Translate(cameraPos - cam.Position);
|
||||
|
||||
foreach (Submarine sub in subs)
|
||||
{
|
||||
sub.PhysicsBody?.ResetDynamics();
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
cam.Zoom = MathHelper.SmoothStep(initialZoom, 0.5f, timer / duration);
|
||||
if (timer / duration > 0.9f)
|
||||
{
|
||||
GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((timer / duration) - 0.9f) * 10.0f);
|
||||
}
|
||||
#endif
|
||||
timer += CoroutineManager.UnscaledDeltaTime;
|
||||
|
||||
yield return CoroutineStatus.Running;
|
||||
}
|
||||
|
||||
Running = false;
|
||||
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
#if CLIENT
|
||||
GUI.ScreenOverlayColor = Color.TransparentBlack;
|
||||
#endif
|
||||
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,354 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
#endif
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class StructurePrefab : MapEntityPrefab
|
||||
{
|
||||
public static readonly PrefabCollection<StructurePrefab> Prefabs = new PrefabCollection<StructurePrefab>();
|
||||
|
||||
private bool disposed = false;
|
||||
public override void Dispose()
|
||||
{
|
||||
if (disposed) { return; }
|
||||
disposed = true;
|
||||
Prefabs.Remove(this);
|
||||
}
|
||||
|
||||
private string name;
|
||||
public override string Name
|
||||
{
|
||||
get { return name; }
|
||||
}
|
||||
|
||||
public XElement ConfigElement { get; private set; }
|
||||
|
||||
private bool canSpriteFlipX, canSpriteFlipY;
|
||||
|
||||
private float health;
|
||||
|
||||
//default size
|
||||
private Vector2 size;
|
||||
|
||||
//does the structure have a physics body
|
||||
[Serialize(false, false)]
|
||||
public bool Body
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//rotation of the physics body in degrees
|
||||
[Serialize(0.0f, false)]
|
||||
public float BodyRotation
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//in display units
|
||||
[Serialize(0.0f, false)]
|
||||
public float BodyWidth
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//in display units
|
||||
[Serialize(0.0f, false)]
|
||||
public float BodyHeight
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//in display units
|
||||
[Serialize("0.0,0.0", false)]
|
||||
public Vector2 BodyOffset
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool Platform
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool AllowAttachItems
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(100.0f, false)]
|
||||
public float Health
|
||||
{
|
||||
get { return health; }
|
||||
set { health = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool CastShadow
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If null, the orientation is determined automatically based on the dimensions of the structure instances
|
||||
/// </summary>
|
||||
public bool? IsHorizontal
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(Direction.None, false)]
|
||||
public Direction StairDirection
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(45.0f, false)]
|
||||
public float StairAngle
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool NoAITarget
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool CanSpriteFlipX
|
||||
{
|
||||
get { return canSpriteFlipX; }
|
||||
}
|
||||
|
||||
public bool CanSpriteFlipY
|
||||
{
|
||||
get { return canSpriteFlipY; }
|
||||
}
|
||||
|
||||
[Serialize("0,0", true)]
|
||||
public Vector2 Size
|
||||
{
|
||||
get { return size; }
|
||||
private set { size = value; }
|
||||
}
|
||||
|
||||
public Vector2 ScaledSize => size * Scale;
|
||||
|
||||
protected Vector2 textureScale = Vector2.One;
|
||||
[Editable(DecimalCount = 3), Serialize("1.0, 1.0", true)]
|
||||
public Vector2 TextureScale
|
||||
{
|
||||
get { return textureScale; }
|
||||
set
|
||||
{
|
||||
textureScale = new Vector2(
|
||||
MathHelper.Clamp(value.X, 0.01f, 10),
|
||||
MathHelper.Clamp(value.Y, 0.01f, 10));
|
||||
}
|
||||
}
|
||||
|
||||
public Sprite BackgroundSprite
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public static void LoadAll(IEnumerable<ContentFile> files)
|
||||
{
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
LoadFromFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LoadFromFile(ContentFile file)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
if (doc == null) { return; }
|
||||
var rootElement = doc.Root;
|
||||
if (rootElement.IsOverride())
|
||||
{
|
||||
foreach (var element in rootElement.Elements())
|
||||
{
|
||||
foreach (var childElement in element.Elements())
|
||||
{
|
||||
Load(childElement, true, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var element in rootElement.Elements())
|
||||
{
|
||||
if (element.IsOverride())
|
||||
{
|
||||
foreach (var childElement in element.Elements())
|
||||
{
|
||||
Load(childElement, true, file);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Load(element, false, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveByFile(string filePath)
|
||||
{
|
||||
Prefabs.RemoveByFile(filePath);
|
||||
}
|
||||
|
||||
private static StructurePrefab Load(XElement element, bool allowOverride, ContentFile file)
|
||||
{
|
||||
StructurePrefab sp = new StructurePrefab
|
||||
{
|
||||
originalName = element.GetAttributeString("name", ""),
|
||||
FilePath = file.Path,
|
||||
ContentPackage = file.ContentPackage
|
||||
};
|
||||
sp.name = sp.originalName;
|
||||
sp.ConfigElement = element;
|
||||
sp.identifier = element.GetAttributeString("identifier", "");
|
||||
if (string.IsNullOrEmpty(sp.name))
|
||||
{
|
||||
sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})";
|
||||
}
|
||||
sp.Tags = new HashSet<string>();
|
||||
string joinedTags = element.GetAttributeString("tags", "");
|
||||
if (string.IsNullOrEmpty(joinedTags)) joinedTags = element.GetAttributeString("Tags", "");
|
||||
foreach (string tag in joinedTags.Split(','))
|
||||
{
|
||||
sp.Tags.Add(tag.Trim().ToLowerInvariant());
|
||||
}
|
||||
|
||||
if (element.Attribute("ishorizontal") != null)
|
||||
{
|
||||
sp.IsHorizontal = element.GetAttributeBool("ishorizontal", false);
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString())
|
||||
{
|
||||
case "sprite":
|
||||
sp.sprite = new Sprite(subElement, lazyLoad: true);
|
||||
if (subElement.Attribute("sourcerect") == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!");
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (subElement.GetAttributeBool("fliphorizontal", false))
|
||||
sp.sprite.effects = SpriteEffects.FlipHorizontally;
|
||||
if (subElement.GetAttributeBool("flipvertical", false))
|
||||
sp.sprite.effects = SpriteEffects.FlipVertically;
|
||||
#endif
|
||||
|
||||
sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
|
||||
sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
|
||||
|
||||
if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name))
|
||||
{
|
||||
sp.sprite.Name = sp.Name;
|
||||
}
|
||||
sp.sprite.EntityID = sp.identifier;
|
||||
break;
|
||||
case "backgroundsprite":
|
||||
sp.BackgroundSprite = new Sprite(subElement, lazyLoad: true);
|
||||
if (subElement.Attribute("sourcerect") == null && sp.sprite != null)
|
||||
{
|
||||
sp.BackgroundSprite.SourceRect = sp.sprite.SourceRect;
|
||||
sp.BackgroundSprite.size = sp.sprite.size;
|
||||
sp.BackgroundSprite.size.X *= sp.sprite.SourceRect.Width;
|
||||
sp.BackgroundSprite.size.Y *= sp.sprite.SourceRect.Height;
|
||||
sp.BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
#if CLIENT
|
||||
if (subElement.GetAttributeBool("fliphorizontal", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; }
|
||||
if (subElement.GetAttributeBool("flipvertical", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; }
|
||||
sp.BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category))
|
||||
{
|
||||
category = MapEntityCategory.Structure;
|
||||
}
|
||||
sp.Category = category;
|
||||
|
||||
if (category.HasFlag(MapEntityCategory.Legacy))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sp.identifier))
|
||||
{
|
||||
sp.identifier = "legacystructure_" + sp.name.ToLowerInvariant().Replace(" ", "");
|
||||
}
|
||||
}
|
||||
|
||||
sp.Aliases =
|
||||
(element.GetAttributeStringArray("aliases", null) ??
|
||||
element.GetAttributeStringArray("Aliases", new string[0])).ToHashSet();
|
||||
|
||||
string nonTranslatedName = element.GetAttributeString("name", null) ?? element.Name.ToString();
|
||||
sp.Aliases.Add(nonTranslatedName.ToLowerInvariant());
|
||||
|
||||
SerializableProperty.DeserializeProperties(sp, element);
|
||||
if (sp.Body)
|
||||
{
|
||||
sp.Tags.Add("wall");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sp.Description))
|
||||
{
|
||||
sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty;
|
||||
}
|
||||
|
||||
//backwards compatibility
|
||||
if (element.Attribute("size") == null)
|
||||
{
|
||||
sp.size = Vector2.Zero;
|
||||
if (element.Attribute("width") == null && element.Attribute("height") == null)
|
||||
{
|
||||
sp.size.X = sp.sprite.SourceRect.Width;
|
||||
sp.size.Y = sp.sprite.SourceRect.Height;
|
||||
}
|
||||
else
|
||||
{
|
||||
sp.size.X = element.GetAttributeFloat("width", 0.0f);
|
||||
sp.size.Y = element.GetAttributeFloat("height", 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sp.identifier))
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
"Structure prefab \"" + sp.name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading.");
|
||||
}
|
||||
Prefabs.Add(sp, allowOverride);
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,847 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Collision;
|
||||
using FarseerPhysics.Common;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Voronoi2;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class SubmarineBody
|
||||
{
|
||||
public const float NeutralBallastPercentage = 0.07f;
|
||||
|
||||
const float HorizontalDrag = 0.01f;
|
||||
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
|
||||
private const float MinImpactLimbMass = 10.0f;
|
||||
//impacts smaller than this are ignored
|
||||
private const float MinCollisionImpact = 3.0f;
|
||||
//impacts are clamped below this value
|
||||
private const float MaxCollisionImpact = 5.0f;
|
||||
private const float Friction = 0.2f, Restitution = 0.0f;
|
||||
|
||||
public List<Vector2> HullVertices
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private float depthDamageTimer;
|
||||
|
||||
private readonly Submarine submarine;
|
||||
|
||||
public readonly PhysicsBody Body;
|
||||
|
||||
private readonly List<PosInfo> positionBuffer = new List<PosInfo>();
|
||||
|
||||
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
||||
|
||||
struct Impact
|
||||
{
|
||||
public Fixture Target;
|
||||
public Vector2 Velocity;
|
||||
public Vector2 ImpactPos;
|
||||
public Vector2 Normal;
|
||||
|
||||
public Impact(Fixture f1, Fixture f2, Contact contact)
|
||||
{
|
||||
Target = f2;
|
||||
contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
|
||||
if (contact.FixtureA.Body == f1.Body) { contactNormal = -contactNormal; }
|
||||
ImpactPos = points[0];
|
||||
Normal = contactNormal;
|
||||
Velocity = f1.Body.LinearVelocity - f2.Body.LinearVelocity;
|
||||
}
|
||||
}
|
||||
|
||||
public Rectangle Borders
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Vector2 Velocity
|
||||
{
|
||||
get { return Body.LinearVelocity; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
Body.LinearVelocity = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 Position
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(Body.SimPosition); }
|
||||
}
|
||||
|
||||
public List<PosInfo> PositionBuffer
|
||||
{
|
||||
get { return positionBuffer; }
|
||||
}
|
||||
|
||||
public bool AtDamageDepth
|
||||
{
|
||||
get { return Position.Y < DamageDepth; }
|
||||
}
|
||||
|
||||
public Submarine Submarine
|
||||
{
|
||||
get { return submarine; }
|
||||
}
|
||||
|
||||
public SubmarineBody(Submarine sub, bool showWarningMessages = true)
|
||||
{
|
||||
this.submarine = sub;
|
||||
|
||||
Body farseerBody = null;
|
||||
if (!Hull.hullList.Any())
|
||||
{
|
||||
farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
|
||||
if (showWarningMessages)
|
||||
{
|
||||
DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
List<Vector2> convexHull = GenerateConvexHull();
|
||||
for (int i = 0; i < convexHull.Count; i++)
|
||||
{
|
||||
convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
|
||||
}
|
||||
HullVertices = convexHull;
|
||||
|
||||
Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero;
|
||||
|
||||
farseerBody = GameMain.World.CreateBody();
|
||||
farseerBody.UserData = this;
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
if (wall.Submarine != submarine) continue;
|
||||
|
||||
Rectangle rect = wall.Rect;
|
||||
|
||||
farseerBody.CreateRectangle(
|
||||
ConvertUnits.ToSimUnits(wall.BodyWidth),
|
||||
ConvertUnits.ToSimUnits(wall.BodyHeight),
|
||||
50.0f,
|
||||
-wall.BodyRotation,
|
||||
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall;
|
||||
|
||||
minExtents.X = Math.Min(rect.X, minExtents.X);
|
||||
minExtents.Y = Math.Min(rect.Y - rect.Height, minExtents.Y);
|
||||
maxExtents.X = Math.Max(rect.Right, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(rect.Y, maxExtents.Y);
|
||||
}
|
||||
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine != submarine) continue;
|
||||
|
||||
Rectangle rect = hull.Rect;
|
||||
farseerBody.CreateRectangle(
|
||||
ConvertUnits.ToSimUnits(rect.Width),
|
||||
ConvertUnits.ToSimUnits(rect.Height),
|
||||
100.0f,
|
||||
ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull;
|
||||
|
||||
minExtents.X = Math.Min(rect.X, minExtents.X);
|
||||
minExtents.Y = Math.Min(rect.Y - rect.Height, minExtents.Y);
|
||||
maxExtents.X = Math.Max(rect.Right, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(rect.Y, maxExtents.Y);
|
||||
}
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.StaticBodyConfig == null || item.Submarine != submarine) continue;
|
||||
|
||||
float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale;
|
||||
float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale;
|
||||
float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale;
|
||||
|
||||
Vector2 simPos = ConvertUnits.ToSimUnits(item.Position);
|
||||
float simRadius = ConvertUnits.ToSimUnits(radius);
|
||||
float simWidth = ConvertUnits.ToSimUnits(width);
|
||||
float simHeight = ConvertUnits.ToSimUnits(height);
|
||||
|
||||
if (width > 0.0f && height > 0.0f)
|
||||
{
|
||||
farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos).UserData = item;
|
||||
|
||||
minExtents.X = Math.Min(item.Position.X - width / 2, minExtents.X);
|
||||
minExtents.Y = Math.Min(item.Position.Y - height / 2, minExtents.Y);
|
||||
maxExtents.X = Math.Max(item.Position.X + width / 2, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(item.Position.Y + height / 2, maxExtents.Y);
|
||||
}
|
||||
else if (radius > 0.0f && width > 0.0f)
|
||||
{
|
||||
farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos).UserData = item;
|
||||
farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2).UserData = item;
|
||||
farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2).UserData = item;
|
||||
minExtents.X = Math.Min(item.Position.X - width / 2 - radius, minExtents.X);
|
||||
minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y);
|
||||
maxExtents.X = Math.Max(item.Position.X + width / 2 + radius, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y);
|
||||
}
|
||||
else if (radius > 0.0f && height > 0.0f)
|
||||
{
|
||||
farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos).UserData = item;
|
||||
farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2).UserData = item;
|
||||
farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2).UserData = item;
|
||||
minExtents.X = Math.Min(item.Position.X - radius, minExtents.X);
|
||||
minExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, minExtents.Y);
|
||||
maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(item.Position.Y + height / 2 + radius, maxExtents.Y);
|
||||
}
|
||||
else if (radius > 0.0f)
|
||||
{
|
||||
farseerBody.CreateCircle(simRadius, 5.0f, simPos).UserData = item;
|
||||
minExtents.X = Math.Min(item.Position.X - radius, minExtents.X);
|
||||
minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y);
|
||||
maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y);
|
||||
}
|
||||
}
|
||||
|
||||
Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y));
|
||||
}
|
||||
|
||||
farseerBody.BodyType = BodyType.Dynamic;
|
||||
farseerBody.CollisionCategories = Physics.CollisionWall;
|
||||
farseerBody.CollidesWith =
|
||||
Physics.CollisionItem |
|
||||
Physics.CollisionLevel |
|
||||
Physics.CollisionCharacter |
|
||||
Physics.CollisionProjectile |
|
||||
Physics.CollisionWall;
|
||||
|
||||
farseerBody.Restitution = Restitution;
|
||||
farseerBody.Friction = Friction;
|
||||
farseerBody.FixedRotation = true;
|
||||
farseerBody.Awake = true;
|
||||
farseerBody.SleepingAllowed = false;
|
||||
farseerBody.IgnoreGravity = true;
|
||||
farseerBody.OnCollision += OnCollision;
|
||||
farseerBody.UserData = submarine;
|
||||
|
||||
Body = new PhysicsBody(farseerBody);
|
||||
}
|
||||
|
||||
private List<Vector2> GenerateConvexHull()
|
||||
{
|
||||
List<Structure> subWalls = Structure.WallList.FindAll(wall => wall.Submarine == submarine);
|
||||
|
||||
if (subWalls.Count == 0)
|
||||
{
|
||||
return new List<Vector2> { new Vector2(-1.0f, 1.0f), new Vector2(1.0f, 1.0f), new Vector2(0.0f, -1.0f) };
|
||||
}
|
||||
|
||||
List<Vector2> points = new List<Vector2>();
|
||||
|
||||
foreach (Structure wall in subWalls)
|
||||
{
|
||||
points.Add(new Vector2(wall.Rect.X, wall.Rect.Y));
|
||||
points.Add(new Vector2(wall.Rect.X + wall.Rect.Width, wall.Rect.Y));
|
||||
points.Add(new Vector2(wall.Rect.X, wall.Rect.Y - wall.Rect.Height));
|
||||
points.Add(new Vector2(wall.Rect.X + wall.Rect.Width, wall.Rect.Y - wall.Rect.Height));
|
||||
}
|
||||
|
||||
List<Vector2> hullPoints = MathUtils.GiftWrap(points);
|
||||
|
||||
return hullPoints;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
while (impactQueue.Count > 0)
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
|
||||
if (impact.Target.UserData is VoronoiCell cell)
|
||||
{
|
||||
HandleLevelCollision(impact);
|
||||
}
|
||||
else if (impact.Target.Body.UserData is Structure)
|
||||
{
|
||||
HandleLevelCollision(impact);
|
||||
}
|
||||
else if (impact.Target.Body.UserData is Submarine otherSub)
|
||||
{
|
||||
HandleSubCollision(impact, otherSub);
|
||||
}
|
||||
else if (impact.Target.Body.UserData is Limb limb)
|
||||
{
|
||||
HandleLimbCollision(impact, limb);
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------
|
||||
|
||||
if (Body.FarseerBody.BodyType == BodyType.Static) { return; }
|
||||
|
||||
ClientUpdatePosition(deltaTime);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
Vector2 totalForce = CalculateBuoyancy();
|
||||
|
||||
//-------------------------
|
||||
|
||||
//if outside left or right edge of the level
|
||||
if (Position.X < 0 || Position.X > Level.Loaded.Size.X)
|
||||
{
|
||||
Rectangle worldBorders = Borders;
|
||||
worldBorders.Location += MathUtils.ToPoint(Position);
|
||||
|
||||
//push the sub back below the upper "barrier" of the level
|
||||
if (worldBorders.Y > Level.Loaded.Size.Y)
|
||||
{
|
||||
Body.LinearVelocity = new Vector2(
|
||||
Body.LinearVelocity.X,
|
||||
Math.Min(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.Size.Y - worldBorders.Y)));
|
||||
}
|
||||
else if (worldBorders.Y - worldBorders.Height < Level.Loaded.BottomPos)
|
||||
{
|
||||
Body.LinearVelocity = new Vector2(
|
||||
Body.LinearVelocity.X,
|
||||
Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height))));
|
||||
}
|
||||
|
||||
if (Position.X < 0)
|
||||
{
|
||||
float force = Math.Abs(Position.X * 0.5f);
|
||||
totalForce += Vector2.UnitX * force;
|
||||
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float force = (Position.X - Level.Loaded.Size.X) * 0.5f;
|
||||
totalForce -= Vector2.UnitX * force;
|
||||
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------
|
||||
|
||||
if (Body.LinearVelocity.LengthSquared() > 0.0001f)
|
||||
{
|
||||
//TODO: sync current drag with clients?
|
||||
float attachedMass = 0.0f;
|
||||
JointEdge jointEdge = Body.FarseerBody.JointList;
|
||||
while (jointEdge != null)
|
||||
{
|
||||
Body otherBody = jointEdge.Joint.BodyA == Body.FarseerBody ? jointEdge.Joint.BodyB : jointEdge.Joint.BodyA;
|
||||
Character character = (otherBody.UserData as Limb)?.character;
|
||||
if (character != null) attachedMass += character.Mass;
|
||||
|
||||
jointEdge = jointEdge.Next;
|
||||
}
|
||||
|
||||
float horizontalDragCoefficient = MathHelper.Clamp(HorizontalDrag + attachedMass / 5000.0f, 0.0f, MaxDrag);
|
||||
totalForce.X -= Math.Sign(Body.LinearVelocity.X) * Body.LinearVelocity.X * Body.LinearVelocity.X * horizontalDragCoefficient * Body.Mass;
|
||||
|
||||
float verticalDragCoefficient = MathHelper.Clamp(VerticalDrag + attachedMass / 5000.0f, 0.0f, MaxDrag);
|
||||
totalForce.Y -= Math.Sign(Body.LinearVelocity.Y) * Body.LinearVelocity.Y * Body.LinearVelocity.Y * verticalDragCoefficient * Body.Mass;
|
||||
}
|
||||
|
||||
ApplyForce(totalForce);
|
||||
|
||||
UpdateDepthDamage(deltaTime);
|
||||
}
|
||||
|
||||
partial void ClientUpdatePosition(float deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Moves away any character that is inside the bounding box of the sub (but not inside the sub)
|
||||
/// </summary>
|
||||
/// <param name="subTranslation">The translation that was applied to the sub before doing the displacement
|
||||
/// (used for determining where to push the characters)</param>
|
||||
private void DisplaceCharacters(Vector2 subTranslation)
|
||||
{
|
||||
Rectangle worldBorders = Borders;
|
||||
worldBorders.Location += MathUtils.ToPoint(ConvertUnits.ToDisplayUnits(Body.SimPosition));
|
||||
|
||||
Vector2 translateDir = Vector2.Normalize(subTranslation);
|
||||
if (!MathUtils.IsValid(translateDir)) translateDir = Vector2.UnitY;
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) continue;
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
//if the character isn't inside the bounding box, continue
|
||||
if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) continue;
|
||||
|
||||
//cast a line from the position of the character to the same direction as the translation of the sub
|
||||
//and see where it intersects with the bounding box
|
||||
if (!MathUtils.GetLineRectangleIntersection(limb.WorldPosition,
|
||||
limb.WorldPosition + translateDir * 100000.0f, worldBorders, out Vector2 intersection))
|
||||
{
|
||||
//should never happen when casting a line out from inside the bounding box
|
||||
Debug.Assert(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//"+ translatedir" in order to move the character slightly away from the wall
|
||||
c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 CalculateBuoyancy()
|
||||
{
|
||||
float waterVolume = 0.0f;
|
||||
float volume = 0.0f;
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine != submarine) continue;
|
||||
|
||||
waterVolume += hull.WaterVolume;
|
||||
volume += hull.Volume;
|
||||
}
|
||||
|
||||
float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume;
|
||||
|
||||
float buoyancy = NeutralBallastPercentage - waterPercentage;
|
||||
|
||||
if (buoyancy > 0.0f)
|
||||
buoyancy *= 2.0f;
|
||||
else
|
||||
buoyancy = Math.Max(buoyancy, -0.5f);
|
||||
|
||||
return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f);
|
||||
}
|
||||
|
||||
public void ApplyForce(Vector2 force)
|
||||
{
|
||||
Body.ApplyForce(force, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
|
||||
public void SetPosition(Vector2 position)
|
||||
{
|
||||
Body.SetTransform(ConvertUnits.ToSimUnits(position), 0.0f);
|
||||
}
|
||||
|
||||
private void UpdateDepthDamage(float deltaTime)
|
||||
{
|
||||
if (Position.Y > DamageDepth) { return; }
|
||||
|
||||
float depth = DamageDepth - Position.Y;
|
||||
|
||||
depthDamageTimer -= deltaTime;
|
||||
|
||||
if (depthDamageTimer > 0.0f) return;
|
||||
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
if (wall.Submarine != submarine) continue;
|
||||
|
||||
if (wall.Health < depth * 0.01f)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depthDamageTimer = 10.0f;
|
||||
}
|
||||
|
||||
public void FlipX()
|
||||
{
|
||||
List<Vector2> convexHull = GenerateConvexHull();
|
||||
for (int i = 0; i < convexHull.Count; i++)
|
||||
{
|
||||
convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
|
||||
}
|
||||
HullVertices = convexHull;
|
||||
}
|
||||
|
||||
public bool OnCollision(Fixture f1, Fixture f2, Contact contact)
|
||||
{
|
||||
if (f2.Body.UserData is Limb limb)
|
||||
{
|
||||
bool collision = CheckCharacterCollision(contact, limb.character);
|
||||
if (collision)
|
||||
{
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact));
|
||||
}
|
||||
}
|
||||
return collision;
|
||||
}
|
||||
if (f2.Body.UserData is Character character)
|
||||
{
|
||||
return CheckCharacterCollision(contact, character);
|
||||
}
|
||||
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CheckCharacterCollision(Contact contact, Character character)
|
||||
{
|
||||
//characters that can't enter the sub always collide regardless of gaps
|
||||
if (!character.AnimController.CanEnterSubmarine) { return true; }
|
||||
if (character.Submarine != null) { return false; }
|
||||
|
||||
contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
|
||||
|
||||
Vector2 normalizedVel = character.AnimController.Collider.LinearVelocity == Vector2.Zero ?
|
||||
Vector2.Zero : Vector2.Normalize(character.AnimController.Collider.LinearVelocity);
|
||||
|
||||
Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal);
|
||||
Hull newHull = Hull.FindHull(targetPos, null);
|
||||
|
||||
if (newHull == null)
|
||||
{
|
||||
targetPos = ConvertUnits.ToDisplayUnits(points[0] + normalizedVel);
|
||||
newHull = Hull.FindHull(targetPos, null);
|
||||
}
|
||||
|
||||
var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine);
|
||||
targetPos = character.WorldPosition;
|
||||
Gap adjacentGap = Gap.FindAdjacent(gaps, targetPos, 500.0f);
|
||||
if (adjacentGap == null) { return true; }
|
||||
|
||||
if (newHull != null)
|
||||
{
|
||||
CoroutineManager.Invoke(() =>
|
||||
character.AnimController.FindHull(newHull.WorldPosition, true));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleLimbCollision(Impact collision, Limb limb)
|
||||
{
|
||||
if (limb?.body?.FarseerBody == null || limb.character == null) { return; }
|
||||
|
||||
if (limb.Mass > MinImpactLimbMass)
|
||||
{
|
||||
Vector2 normal =
|
||||
Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
|
||||
Vector2.UnitY :
|
||||
Vector2.Normalize(Body.SimPosition - limb.SimPosition);
|
||||
|
||||
float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1);
|
||||
|
||||
ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
|
||||
foreach (Submarine dockedSub in submarine.DockedTo)
|
||||
{
|
||||
dockedSub.SubBody.ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
|
||||
}
|
||||
}
|
||||
|
||||
//find all contacts between the limb and level walls
|
||||
List<Contact> levelContacts = new List<Contact>();
|
||||
ContactEdge contactEdge = limb.body.FarseerBody.ContactList;
|
||||
while (contactEdge?.Contact != null)
|
||||
{
|
||||
if (contactEdge.Contact.Enabled &&
|
||||
contactEdge.Contact.IsTouching &&
|
||||
contactEdge.Other?.UserData is VoronoiCell)
|
||||
{
|
||||
levelContacts.Add(contactEdge.Contact);
|
||||
}
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
|
||||
if (levelContacts.Count == 0) { return; }
|
||||
|
||||
//if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it
|
||||
//not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete),
|
||||
//but more realistic than bouncing and prevents using characters as "bumpers" that prevent all collision damage
|
||||
Vector2 avgContactNormal = Vector2.Zero;
|
||||
foreach (Contact levelContact in levelContacts)
|
||||
{
|
||||
levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
|
||||
|
||||
//if the contact normal is pointing from the limb towards the level cell it's touching, flip the normal
|
||||
VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
|
||||
((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
|
||||
|
||||
var cellDiff = ConvertUnits.ToDisplayUnits(limb.body.SimPosition) - cell.Center;
|
||||
if (Vector2.Dot(contactNormal, cellDiff) < 0)
|
||||
{
|
||||
contactNormal = -contactNormal;
|
||||
}
|
||||
|
||||
avgContactNormal += contactNormal;
|
||||
|
||||
//apply impacts at the positions where this sub is touching the limb
|
||||
ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContacts.Count, contactNormal, collision.ImpactPos, applyDamage: false);
|
||||
}
|
||||
avgContactNormal /= levelContacts.Count;
|
||||
|
||||
float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal);
|
||||
if (contactDot > 0.001f)
|
||||
{
|
||||
Vector2 velChange = Vector2.Normalize(Body.LinearVelocity) * contactDot;
|
||||
if (!MathUtils.IsValid(velChange))
|
||||
{
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"SubmarineBody.HandleLimbCollision:" + submarine.ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Invalid velocity change in SubmarineBody.HandleLimbCollision (submarine velocity: " + Body.LinearVelocity
|
||||
+ ", avgContactNormal: " + avgContactNormal
|
||||
+ ", contactDot: " + contactDot
|
||||
+ ", velChange: " + velChange + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
Body.LinearVelocity -= velChange;
|
||||
|
||||
float damageAmount = contactDot * Body.Mass / limb.character.Mass;
|
||||
limb.character.LastDamageSource = submarine;
|
||||
limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb,
|
||||
new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageAmount) }, 0.0f, true, 0.0f);
|
||||
|
||||
if (limb.character.IsDead)
|
||||
{
|
||||
foreach (LimbJoint limbJoint in limb.character.AnimController.LimbJoints)
|
||||
{
|
||||
if (limbJoint.IsSevered || (limbJoint.LimbA != limb && limbJoint.LimbB != limb)) continue;
|
||||
limb.character.AnimController.SeverLimbJoint(limbJoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLevelCollision(Impact impact)
|
||||
{
|
||||
float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal);
|
||||
|
||||
ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
|
||||
foreach (Submarine dockedSub in submarine.DockedTo)
|
||||
{
|
||||
dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50);
|
||||
for (int i = 0; i < particleAmount; i++)
|
||||
{
|
||||
GameMain.ParticleManager.CreateParticle("iceshards",
|
||||
ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)),
|
||||
Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void HandleSubCollision(Impact impact, Submarine otherSub)
|
||||
{
|
||||
Debug.Assert(otherSub != submarine);
|
||||
|
||||
Vector2 normal = impact.Normal;
|
||||
if (impact.Target.Body == otherSub.SubBody.Body.FarseerBody)
|
||||
{
|
||||
normal = -normal;
|
||||
}
|
||||
|
||||
float thisMass = Body.Mass + submarine.DockedTo.Sum(s => s.PhysicsBody.Mass);
|
||||
float otherMass = otherSub.PhysicsBody.Mass + otherSub.DockedTo.Sum(s => s.PhysicsBody.Mass);
|
||||
float massRatio = otherMass / (thisMass + otherMass);
|
||||
|
||||
float impulse = (Vector2.Dot(impact.Velocity, normal) / 2.0f) * massRatio;
|
||||
|
||||
//apply impact to this sub (the other sub takes care of this in its own collision callback)
|
||||
ApplyImpact(impulse, normal, impact.ImpactPos);
|
||||
foreach (Submarine dockedSub in submarine.DockedTo)
|
||||
{
|
||||
dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
|
||||
}
|
||||
|
||||
//find all contacts between this sub and level walls
|
||||
List<Contact> levelContacts = new List<Contact>();
|
||||
ContactEdge contactEdge = Body.FarseerBody.ContactList;
|
||||
while (contactEdge.Next != null)
|
||||
{
|
||||
if (contactEdge.Contact.Enabled &&
|
||||
contactEdge.Other.UserData is VoronoiCell &&
|
||||
contactEdge.Contact.IsTouching)
|
||||
{
|
||||
levelContacts.Add(contactEdge.Contact);
|
||||
}
|
||||
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
|
||||
if (levelContacts.Count == 0) return;
|
||||
|
||||
//if this sub is in contact with the level, apply artifical impacts
|
||||
//to both subs to prevent the other sub from bouncing on top of this one
|
||||
//and to fake the other sub "crushing" this one against a wall
|
||||
Vector2 avgContactNormal = Vector2.Zero;
|
||||
foreach (Contact levelContact in levelContacts)
|
||||
{
|
||||
levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
|
||||
|
||||
//if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal
|
||||
VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
|
||||
((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
|
||||
|
||||
var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center;
|
||||
if (Vector2.Dot(contactNormal, cellDiff) < 0)
|
||||
{
|
||||
contactNormal = -contactNormal;
|
||||
}
|
||||
|
||||
avgContactNormal += contactNormal;
|
||||
|
||||
//apply impacts at the positions where this sub is touching the level
|
||||
ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContacts.Count, contactNormal, impact.ImpactPos);
|
||||
}
|
||||
avgContactNormal /= levelContacts.Count;
|
||||
|
||||
//apply an impact to the other sub
|
||||
float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal);
|
||||
if (contactDot > 0.0f)
|
||||
{
|
||||
if (otherSub.PhysicsBody.LinearVelocity.LengthSquared() > 0.0001f)
|
||||
{
|
||||
otherSub.PhysicsBody.LinearVelocity -= Vector2.Normalize(otherSub.PhysicsBody.LinearVelocity) * contactDot;
|
||||
}
|
||||
|
||||
impulse = Vector2.Dot(otherSub.Velocity, normal);
|
||||
otherSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
|
||||
foreach (Submarine dockedSub in otherSub.DockedTo)
|
||||
{
|
||||
dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyImpact(float impact, Vector2 direction, Vector2 impactPos, bool applyDamage = true)
|
||||
{
|
||||
if (impact < MinCollisionImpact) { return; }
|
||||
|
||||
Vector2 impulse = direction * impact * 0.5f;
|
||||
impulse = impulse.ClampLength(MaxCollisionImpact);
|
||||
|
||||
if (!MathUtils.IsValid(impulse))
|
||||
{
|
||||
string errorMsg =
|
||||
"Invalid impulse in SubmarineBody.ApplyImpact: " + impulse +
|
||||
". Direction: " + direction + ", body position: " + Body.SimPosition + ", impact: " + impact + ".";
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
|
||||
}
|
||||
if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"SubmarineBody.ApplyImpact:InvalidImpulse",
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = impact * 2.0f;
|
||||
if (!submarine.IsOutpost && !submarine.DockedTo.Any(s => s.IsOutpost))
|
||||
{
|
||||
float angularVelocity =
|
||||
(impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y
|
||||
- (impactPos.Y - Body.SimPosition.Y) / ConvertUnits.ToSimUnits(submarine.Borders.Height / 2) * impulse.X;
|
||||
GameMain.GameScreen.Cam.AngularVelocity = MathHelper.Clamp(angularVelocity * 0.1f, -1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Submarine != submarine) { continue; }
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f);
|
||||
}
|
||||
c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
|
||||
|
||||
bool holdingOntoSomething = false;
|
||||
if (c.SelectedConstruction != null)
|
||||
{
|
||||
var controller = c.SelectedConstruction.GetComponent<Items.Components.Controller>();
|
||||
holdingOntoSomething = controller != null && controller.LimbPositions.Any();
|
||||
}
|
||||
|
||||
//stun for up to 1 second if the impact equal or higher to the maximum impact
|
||||
if (impact >= MaxCollisionImpact && !holdingOntoSomething)
|
||||
{
|
||||
c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != submarine || item.CurrentHull == null ||
|
||||
item.body == null || !item.body.Enabled) continue;
|
||||
|
||||
item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f);
|
||||
}
|
||||
|
||||
var damagedStructures = Explosion.RangedStructureDamage(
|
||||
ConvertUnits.ToDisplayUnits(impactPos),
|
||||
impact * 50.0f,
|
||||
applyDamage ? impact * ImpactDamageMultiplier : 0.0f);
|
||||
|
||||
#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);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum SpawnType { Path, Human, Enemy, Cargo };
|
||||
partial class WayPoint : MapEntity
|
||||
{
|
||||
public static List<WayPoint> WayPointList = new List<WayPoint>();
|
||||
|
||||
public static bool ShowWayPoints = true, ShowSpawnPoints = true;
|
||||
|
||||
protected SpawnType spawnType;
|
||||
|
||||
//characters spawning at the waypoint will be given an ID card with these tags
|
||||
private string idCardDesc;
|
||||
private string[] idCardTags;
|
||||
|
||||
//only characters with this job will be spawned at the waypoint
|
||||
private JobPrefab assignedJob;
|
||||
|
||||
private Hull currentHull;
|
||||
|
||||
private ushort ladderId;
|
||||
public Ladder Ladders;
|
||||
public Structure Stairs;
|
||||
|
||||
public bool isObstructed;
|
||||
|
||||
private ushort gapId;
|
||||
public Gap ConnectedGap
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Door ConnectedDoor
|
||||
{
|
||||
get { return ConnectedGap?.ConnectedDoor; }
|
||||
}
|
||||
|
||||
public Hull CurrentHull
|
||||
{
|
||||
get { return currentHull; }
|
||||
}
|
||||
|
||||
public SpawnType SpawnType
|
||||
{
|
||||
get { return spawnType; }
|
||||
set { spawnType = value; }
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return spawnType == SpawnType.Path ? "WayPoint" : "SpawnPoint";
|
||||
}
|
||||
}
|
||||
|
||||
public string IdCardDesc
|
||||
{
|
||||
get { return idCardDesc; }
|
||||
private set { idCardDesc = value; }
|
||||
}
|
||||
public string[] IdCardTags
|
||||
{
|
||||
get { return idCardTags; }
|
||||
private set
|
||||
{
|
||||
idCardTags = value;
|
||||
for (int i = 0; i < idCardTags.Length; i++)
|
||||
{
|
||||
idCardTags[i] = idCardTags[i].Trim().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JobPrefab AssignedJob
|
||||
{
|
||||
get { return assignedJob; }
|
||||
}
|
||||
|
||||
public WayPoint(Vector2 position, SpawnType spawnType, Submarine submarine, Gap gap = null)
|
||||
: this(new Rectangle((int)position.X - 3, (int)position.Y + 3, 6, 6), submarine)
|
||||
{
|
||||
this.spawnType = spawnType;
|
||||
ConnectedGap = gap;
|
||||
}
|
||||
|
||||
public WayPoint(MapEntityPrefab prefab, Rectangle rectangle)
|
||||
: this (rectangle, Submarine.MainSub)
|
||||
{
|
||||
if (prefab.Identifier.Contains("spawn"))
|
||||
{
|
||||
spawnType = SpawnType.Human;
|
||||
}
|
||||
else
|
||||
{
|
||||
SpawnType = SpawnType.Path;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public WayPoint(Rectangle newRect, Submarine submarine)
|
||||
: this (MapEntityPrefab.Find(null, "waypoint"), newRect, submarine)
|
||||
{
|
||||
}
|
||||
|
||||
public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine)
|
||||
: base (prefab, submarine)
|
||||
{
|
||||
rect = newRect;
|
||||
idCardTags = new string[0];
|
||||
|
||||
#if CLIENT
|
||||
if (iconSprites == null)
|
||||
{
|
||||
iconSprites = new Dictionary<SpawnType, Sprite>()
|
||||
{
|
||||
{ SpawnType.Path, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) },
|
||||
{ SpawnType.Human, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) },
|
||||
{ SpawnType.Enemy, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) },
|
||||
{ SpawnType.Cargo, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
InsertToList();
|
||||
WayPointList.Add(this);
|
||||
|
||||
DebugConsole.Log("Created waypoint (" + ID + ")");
|
||||
|
||||
currentHull = Hull.FindHull(WorldPosition);
|
||||
}
|
||||
|
||||
public override MapEntity Clone()
|
||||
{
|
||||
var clone = new WayPoint(rect, Submarine)
|
||||
{
|
||||
idCardDesc = idCardDesc,
|
||||
idCardTags = idCardTags,
|
||||
spawnType = spawnType,
|
||||
assignedJob = assignedJob
|
||||
};
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public static bool GenerateSubWaypoints(Submarine submarine)
|
||||
{
|
||||
if (!Hull.hullList.Any())
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<WayPoint> existingWaypoints = WayPointList.FindAll(wp => wp.spawnType == SpawnType.Path);
|
||||
foreach (WayPoint wayPoint in existingWaypoints)
|
||||
{
|
||||
wayPoint.Remove();
|
||||
}
|
||||
|
||||
//find all open doors and temporarily activate their bodies to prevent visibility checks
|
||||
//from ignoring the doors and generating waypoint connections that go straight through the door
|
||||
List<Door> openDoors = new List<Door>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
var door = item.GetComponent<Door>();
|
||||
if (door != null && !door.Body.Enabled)
|
||||
{
|
||||
openDoors.Add(door);
|
||||
door.Body.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float minDist = 150.0f;
|
||||
float heightFromFloor = 110.0f;
|
||||
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Rect.Height < 150) continue;
|
||||
|
||||
WayPoint prevWaypoint = null;
|
||||
|
||||
if (hull.Rect.Width < minDist * 3.0f)
|
||||
{
|
||||
new WayPoint(
|
||||
new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (float x = hull.Rect.X + minDist; x <= hull.Rect.Right - minDist; x += minDist)
|
||||
{
|
||||
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
|
||||
|
||||
if (prevWaypoint != null) wayPoint.ConnectTo(prevWaypoint);
|
||||
|
||||
prevWaypoint = wayPoint;
|
||||
}
|
||||
}
|
||||
|
||||
float outSideWaypointInterval = 200.0f;
|
||||
int outsideWaypointDist = 100;
|
||||
|
||||
Rectangle borders = Hull.GetBorders();
|
||||
|
||||
borders.X -= outsideWaypointDist;
|
||||
borders.Y += outsideWaypointDist;
|
||||
|
||||
borders.Width += outsideWaypointDist * 2;
|
||||
borders.Height += outsideWaypointDist * 2;
|
||||
|
||||
borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
|
||||
|
||||
if (borders.Width <= outSideWaypointInterval*2)
|
||||
{
|
||||
borders.Inflate(outSideWaypointInterval*2 - borders.Width, 0);
|
||||
}
|
||||
|
||||
if (borders.Height <= outSideWaypointInterval * 2)
|
||||
{
|
||||
int inflateAmount = (int)(outSideWaypointInterval * 2) - borders.Height;
|
||||
borders.Y += inflateAmount / 2;
|
||||
|
||||
borders.Height += inflateAmount;
|
||||
}
|
||||
|
||||
WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
for (float x = borders.X + outSideWaypointInterval; x < borders.Right - outSideWaypointInterval; x += outSideWaypointInterval)
|
||||
{
|
||||
var wayPoint = new WayPoint(
|
||||
new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition,
|
||||
SpawnType.Path, submarine);
|
||||
|
||||
if (x == borders.X + outSideWaypointInterval)
|
||||
{
|
||||
cornerWaypoint[i, 0] = wayPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
cornerWaypoint[i, 1] = WayPointList[WayPointList.Count - 1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
WayPoint wayPoint = null;
|
||||
for (float y = borders.Y - borders.Height; y < borders.Y; y += outSideWaypointInterval)
|
||||
{
|
||||
wayPoint = new WayPoint(
|
||||
new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition,
|
||||
SpawnType.Path, submarine);
|
||||
|
||||
if (y == borders.Y - borders.Height)
|
||||
{
|
||||
wayPoint.ConnectTo(cornerWaypoint[1, i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
wayPoint.ConnectTo(WayPoint.WayPointList[WayPointList.Count - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
wayPoint.ConnectTo(cornerWaypoint[0, i]);
|
||||
}
|
||||
|
||||
List<Structure> stairList = new List<Structure>();
|
||||
foreach (MapEntity me in mapEntityList)
|
||||
{
|
||||
Structure stairs = me as Structure;
|
||||
if (stairs == null) continue;
|
||||
|
||||
if (stairs.StairDirection != Direction.None) stairList.Add(stairs);
|
||||
}
|
||||
|
||||
foreach (Structure stairs in stairList)
|
||||
{
|
||||
WayPoint[] stairPoints = new WayPoint[3];
|
||||
|
||||
stairPoints[0] = new WayPoint(
|
||||
new Vector2(stairs.Rect.X - 32.0f,
|
||||
stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path, submarine);
|
||||
|
||||
stairPoints[1] = new WayPoint(
|
||||
new Vector2(stairs.Rect.Right + 32.0f,
|
||||
stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine);
|
||||
|
||||
for (int i = 0; i < 2; i++ )
|
||||
{
|
||||
for (int dir = -1; dir <= 1; dir += 2)
|
||||
{
|
||||
WayPoint closest = stairPoints[i].FindClosest(dir, true, new Vector2(-30.0f, 30f));
|
||||
if (closest == null) continue;
|
||||
stairPoints[i].ConnectTo(closest);
|
||||
}
|
||||
}
|
||||
|
||||
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position)/2, SpawnType.Path, submarine);
|
||||
stairPoints[0].ConnectTo(stairPoints[2]);
|
||||
stairPoints[2].ConnectTo(stairPoints[1]);
|
||||
}
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
var ladders = item.GetComponent<Ladder>();
|
||||
if (ladders == null) continue;
|
||||
|
||||
List<WayPoint> ladderPoints = new List<WayPoint>();
|
||||
ladderPoints.Add(new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height + heightFromFloor), SpawnType.Path, submarine));
|
||||
|
||||
WayPoint prevPoint = ladderPoints[0];
|
||||
Vector2 prevPos = prevPoint.SimPosition;
|
||||
List<Body> ignoredBodies = new List<Body>();
|
||||
|
||||
for (float y = ladderPoints[0].Position.Y + 100.0f; y < item.Rect.Y - 1.0f; y += 100.0f)
|
||||
{
|
||||
//first check if there's a door in the way
|
||||
//(we need to create a waypoint linked to the door for NPCs to open it)
|
||||
Body pickedBody = Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)),
|
||||
prevPos, ignoredBodies, Physics.CollisionWall, false,
|
||||
(Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent<Door>() != null);
|
||||
|
||||
Door pickedDoor = null;
|
||||
if (pickedBody != null)
|
||||
{
|
||||
pickedDoor = (pickedBody?.UserData as Item).GetComponent<Door>();
|
||||
}
|
||||
else
|
||||
{
|
||||
//no door, check for walls
|
||||
pickedBody = Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false);
|
||||
}
|
||||
|
||||
if (pickedBody == null)
|
||||
{
|
||||
prevPos = Submarine.LastPickedPosition;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
ignoredBodies.Add(pickedBody);
|
||||
}
|
||||
|
||||
|
||||
if (pickedDoor != null)
|
||||
{
|
||||
WayPoint newPoint = new WayPoint(pickedDoor.Item.Position, SpawnType.Path, submarine);
|
||||
ladderPoints.Add(newPoint);
|
||||
newPoint.ConnectedGap = pickedDoor.LinkedGap;
|
||||
newPoint.ConnectTo(prevPoint);
|
||||
prevPoint = newPoint;
|
||||
prevPos = new Vector2(prevPos.X, ConvertUnits.ToSimUnits(pickedDoor.Item.Position.Y - pickedDoor.Item.Rect.Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
WayPoint newPoint = new WayPoint(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * heightFromFloor, SpawnType.Path, submarine);
|
||||
ladderPoints.Add(newPoint);
|
||||
newPoint.ConnectTo(prevPoint);
|
||||
prevPoint = newPoint;
|
||||
prevPos = ConvertUnits.ToSimUnits(newPoint.Position);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevPoint.rect.Y < item.Rect.Y - 10.0f)
|
||||
{
|
||||
WayPoint newPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
|
||||
ladderPoints.Add(newPoint);
|
||||
newPoint.ConnectTo(prevPoint);
|
||||
}
|
||||
|
||||
//connect ladder waypoints to hull points at the right and left side
|
||||
foreach (WayPoint ladderPoint in ladderPoints)
|
||||
{
|
||||
ladderPoint.Ladders = ladders;
|
||||
//don't connect if the waypoint is at a gap (= at the boundary of hulls and/or at a hatch)
|
||||
if (ladderPoint.ConnectedGap != null) continue;
|
||||
|
||||
for (int dir = -1; dir <= 1; dir += 2)
|
||||
{
|
||||
WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 10f));
|
||||
if (closest == null) continue;
|
||||
ladderPoint.ConnectTo(closest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (!gap.IsHorizontal) continue;
|
||||
|
||||
//too small to walk through
|
||||
if (gap.Rect.Height < 150.0f) continue;
|
||||
|
||||
var wayPoint = new WayPoint(
|
||||
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, submarine, gap);
|
||||
|
||||
for (int dir = -1; dir <= 1; dir += 2)
|
||||
{
|
||||
float tolerance = gap.IsRoomToRoom ? 50.0f : outSideWaypointInterval / 2.0f;
|
||||
|
||||
WayPoint closest = wayPoint.FindClosest(
|
||||
dir, true, new Vector2(-tolerance, tolerance),
|
||||
gap.ConnectedDoor?.Body.FarseerBody);
|
||||
|
||||
if (closest != null)
|
||||
{
|
||||
wayPoint.ConnectTo(closest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (gap.IsHorizontal || gap.IsRoomToRoom || !gap.linkedTo.Any(l => l is Hull)) { continue; }
|
||||
|
||||
//too small to walk through
|
||||
if (gap.Rect.Width < 100.0f) { continue; }
|
||||
|
||||
var wayPoint = new WayPoint(
|
||||
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2), SpawnType.Path, submarine, gap);
|
||||
|
||||
float tolerance = outSideWaypointInterval / 2.0f;
|
||||
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
|
||||
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
|
||||
|
||||
WayPoint closest = wayPoint.FindClosest(
|
||||
dir, false, new Vector2(-tolerance, tolerance),
|
||||
gap.ConnectedDoor?.Body.FarseerBody);
|
||||
|
||||
if (closest != null)
|
||||
{
|
||||
wayPoint.ConnectTo(closest);
|
||||
}
|
||||
}
|
||||
|
||||
var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && !w.linkedTo.Any());
|
||||
|
||||
foreach (WayPoint wp in orphans)
|
||||
{
|
||||
wp.Remove();
|
||||
}
|
||||
|
||||
//re-disable the bodies of the doors that are supposed to be open
|
||||
foreach (Door door in openDoors)
|
||||
{
|
||||
door.Body.Enabled = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null)
|
||||
{
|
||||
if (dir != -1 && dir != 1) return null;
|
||||
|
||||
float closestDist = 0.0f;
|
||||
WayPoint closest = null;
|
||||
|
||||
|
||||
foreach (WayPoint wp in WayPointList)
|
||||
{
|
||||
if (wp.SpawnType != SpawnType.Path || wp == this) continue;
|
||||
|
||||
float diff = 0.0f;
|
||||
if (horizontalSearch)
|
||||
{
|
||||
if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) continue;
|
||||
|
||||
diff = wp.Position.X - Position.X;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) continue;
|
||||
|
||||
diff = wp.Position.Y - Position.Y;
|
||||
}
|
||||
|
||||
if (Math.Sign(diff) != dir) continue;
|
||||
|
||||
float dist = Vector2.Distance(wp.Position, Position);
|
||||
if (closest == null || dist < closestDist)
|
||||
{
|
||||
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, true, true, false);
|
||||
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
|
||||
{
|
||||
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
|
||||
}
|
||||
|
||||
closestDist = dist;
|
||||
closest = wp;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
private void ConnectTo(WayPoint wayPoint2)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(this != wayPoint2);
|
||||
|
||||
if (!linkedTo.Contains(wayPoint2)) linkedTo.Add(wayPoint2);
|
||||
if (!wayPoint2.linkedTo.Contains(this)) wayPoint2.linkedTo.Add(this);
|
||||
}
|
||||
|
||||
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, bool useSyncedRand = false)
|
||||
{
|
||||
List<WayPoint> wayPoints = new List<WayPoint>();
|
||||
|
||||
foreach (WayPoint wp in WayPointList)
|
||||
{
|
||||
if (sub != null && wp.Submarine != sub) continue;
|
||||
if (wp.spawnType != spawnType) continue;
|
||||
if (assignedJob != null && wp.assignedJob != assignedJob.Prefab) continue;
|
||||
|
||||
wayPoints.Add(wp);
|
||||
}
|
||||
|
||||
if (!wayPoints.Any()) return null;
|
||||
|
||||
return wayPoints[Rand.Int(wayPoints.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))];
|
||||
}
|
||||
|
||||
public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
|
||||
{
|
||||
List<WayPoint> subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine);
|
||||
|
||||
List<WayPoint> unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human);
|
||||
|
||||
WayPoint[] assignedWayPoints = new WayPoint[crew.Count];
|
||||
|
||||
for (int i = 0; i < crew.Count; i++ )
|
||||
{
|
||||
//try to give the crew member a spawnpoint that hasn't been assigned to anyone and matches their job
|
||||
for (int n = 0; n < unassignedWayPoints.Count; n++)
|
||||
{
|
||||
if (crew[i].Job.Prefab != unassignedWayPoints[n].assignedJob) continue;
|
||||
assignedWayPoints[i] = unassignedWayPoints[n];
|
||||
unassignedWayPoints.RemoveAt(n);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//go through the crewmembers that don't have a spawnpoint yet (if any)
|
||||
for (int i = 0; i < crew.Count; i++)
|
||||
{
|
||||
if (assignedWayPoints[i] != null) continue;
|
||||
|
||||
//try to assign a spawnpoint that matches the job, even if the spawnpoint is already assigned to someone else
|
||||
foreach (WayPoint wp in subWayPoints)
|
||||
{
|
||||
if (wp.spawnType != SpawnType.Human || wp.assignedJob != crew[i].Job.Prefab) continue;
|
||||
|
||||
assignedWayPoints[i] = wp;
|
||||
break;
|
||||
}
|
||||
if (assignedWayPoints[i] != null) continue;
|
||||
|
||||
//try to assign a spawnpoint that isn't meant for any specific job
|
||||
var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.assignedJob == null);
|
||||
if (nonJobSpecificPoints.Any())
|
||||
{
|
||||
assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
|
||||
if (assignedWayPoints[i] != null) continue;
|
||||
|
||||
//everything else failed -> just give a random spawnpoint inside the sub
|
||||
assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, true);
|
||||
}
|
||||
|
||||
for (int i = 0; i < assignedWayPoints.Length; i++)
|
||||
{
|
||||
if (assignedWayPoints[i] == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't find a waypoint for " + crew[i].Name + "!");
|
||||
assignedWayPoints[i] = WayPointList[0];
|
||||
}
|
||||
}
|
||||
|
||||
return assignedWayPoints;
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
currentHull = Hull.FindHull(WorldPosition, currentHull);
|
||||
|
||||
if (gapId > 0) ConnectedGap = FindEntityByID(gapId) as Gap;
|
||||
|
||||
if (ladderId > 0)
|
||||
{
|
||||
var ladderItem = FindEntityByID(ladderId) as Item;
|
||||
if (ladderItem != null) Ladders = ladderItem.GetComponent<Ladder>();
|
||||
}
|
||||
|
||||
Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs);
|
||||
if (pickedBody != null && pickedBody.UserData is Structure)
|
||||
{
|
||||
Structure structure = (Structure)pickedBody.UserData;
|
||||
if (structure != null && structure.StairDirection != Direction.None)
|
||||
{
|
||||
Stairs = structure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static WayPoint Load(XElement element, Submarine submarine)
|
||||
{
|
||||
Rectangle rect = new Rectangle(
|
||||
int.Parse(element.Attribute("x").Value),
|
||||
int.Parse(element.Attribute("y").Value),
|
||||
(int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
|
||||
|
||||
|
||||
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.spawnType = spawnType;
|
||||
|
||||
string idCardDescString = element.GetAttributeString("idcarddesc", "");
|
||||
if (!string.IsNullOrWhiteSpace(idCardDescString))
|
||||
{
|
||||
w.IdCardDesc = idCardDescString;
|
||||
}
|
||||
string idCardTagString = element.GetAttributeString("idcardtags", "");
|
||||
if (!string.IsNullOrWhiteSpace(idCardTagString))
|
||||
{
|
||||
w.IdCardTags = idCardTagString.Split(',');
|
||||
}
|
||||
|
||||
string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(jobIdentifier))
|
||||
{
|
||||
w.assignedJob =
|
||||
JobPrefab.Get(jobIdentifier) ??
|
||||
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>();
|
||||
int i = 0;
|
||||
while (element.Attribute("linkedto" + i) != null)
|
||||
{
|
||||
w.linkedToID.Add((ushort)int.Parse(element.Attribute("linkedto" + i).Value));
|
||||
i += 1;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
if (!ShouldBeSaved) return null;
|
||||
XElement element = new XElement("WayPoint");
|
||||
|
||||
element.Add(new XAttribute("ID", ID),
|
||||
new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)),
|
||||
new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)),
|
||||
new XAttribute("spawn", spawnType));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(idCardDesc)) element.Add(new XAttribute("idcarddesc", idCardDesc));
|
||||
if (idCardTags.Length > 0)
|
||||
{
|
||||
element.Add(new XAttribute("idcardtags", string.Join(",", idCardTags)));
|
||||
}
|
||||
|
||||
if (assignedJob != null) element.Add(new XAttribute("job", assignedJob.Identifier));
|
||||
if (ConnectedGap != null) element.Add(new XAttribute("gap", ConnectedGap.ID));
|
||||
if (Ladders != null) element.Add(new XAttribute("ladders", Ladders.Item.ID));
|
||||
|
||||
parentElement.Add(element);
|
||||
|
||||
if (linkedTo != null)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (MapEntity e in linkedTo)
|
||||
{
|
||||
if (!e.ShouldBeSaved) continue;
|
||||
element.Add(new XAttribute("linkedto" + i, e.ID));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
public override void ShallowRemove()
|
||||
{
|
||||
base.ShallowRemove();
|
||||
|
||||
WayPointList.Remove(this);
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
base.Remove();
|
||||
|
||||
WayPointList.Remove(this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user