(965c31410a) Unstable v0.10.4.0
This commit is contained in:
@@ -1,228 +0,0 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class CorpsePrefab : IPrefab, IDisposable
|
||||
{
|
||||
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
|
||||
|
||||
private bool disposed = false;
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) { return; }
|
||||
disposed = true;
|
||||
Prefabs.Remove(this);
|
||||
}
|
||||
|
||||
public static CorpsePrefab Get(string identifier)
|
||||
{
|
||||
if (Prefabs == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded.");
|
||||
return null;
|
||||
}
|
||||
if (Prefabs.ContainsKey(identifier))
|
||||
{
|
||||
return Prefabs[identifier];
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize("notfound", false)]
|
||||
public string Identifier { get; private set; }
|
||||
|
||||
[Serialize("any", false)]
|
||||
public string Job { get; private set; }
|
||||
|
||||
[Serialize(1f, false)]
|
||||
public float Commonness { get; private set; }
|
||||
|
||||
[Serialize(Level.PositionType.Wreck, false)]
|
||||
public Level.PositionType SpawnPosition { get; private set; }
|
||||
|
||||
public string OriginalName { get { return Identifier; } }
|
||||
|
||||
public ContentPackage ContentPackage { get; private set; }
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public XElement Element { get; private set; }
|
||||
|
||||
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
|
||||
|
||||
public CorpsePrefab(XElement element, string filePath, bool allowOverriding)
|
||||
{
|
||||
FilePath = filePath;
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
Identifier = Identifier.ToLowerInvariant();
|
||||
Job = Job.ToLowerInvariant();
|
||||
Element = element;
|
||||
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
|
||||
Prefabs.Add(this, allowOverriding);
|
||||
}
|
||||
|
||||
public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync);
|
||||
|
||||
public static void LoadAll(IEnumerable<ContentFile> files)
|
||||
{
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
LoadFromFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LoadFromFile(ContentFile file)
|
||||
{
|
||||
DebugConsole.Log("*** " + file.Path + " ***");
|
||||
RemoveByFile(file.Path);
|
||||
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
if (doc == null) { return; }
|
||||
|
||||
var rootElement = doc.Root;
|
||||
switch (rootElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "corpse":
|
||||
new CorpsePrefab(rootElement, file.Path, false)
|
||||
{
|
||||
ContentPackage = file.ContentPackage
|
||||
};
|
||||
break;
|
||||
case "corpses":
|
||||
foreach (var element in rootElement.Elements())
|
||||
{
|
||||
if (element.IsOverride())
|
||||
{
|
||||
var itemElement = element.GetChildElement("item");
|
||||
if (itemElement != null)
|
||||
{
|
||||
new CorpsePrefab(itemElement, file.Path, true)
|
||||
{
|
||||
ContentPackage = file.ContentPackage
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
new CorpsePrefab(element, file.Path, false)
|
||||
{
|
||||
ContentPackage = file.ContentPackage
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "override":
|
||||
var corpses = rootElement.GetChildElement("corpses");
|
||||
if (corpses != null)
|
||||
{
|
||||
foreach (var element in corpses.Elements())
|
||||
{
|
||||
new CorpsePrefab(element, file.Path, true)
|
||||
{
|
||||
ContentPackage = file.ContentPackage,
|
||||
};
|
||||
}
|
||||
}
|
||||
foreach (var element in rootElement.GetChildElements("corpse"))
|
||||
{
|
||||
new CorpsePrefab(element, file.Path, true)
|
||||
{
|
||||
ContentPackage = file.ContentPackage
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveByFile(string filePath)
|
||||
{
|
||||
Prefabs.RemoveByFile(filePath);
|
||||
}
|
||||
|
||||
public void GiveItems(Character character, Submarine submarine)
|
||||
{
|
||||
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), Rand.RandSync.Unsynced);
|
||||
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
|
||||
{
|
||||
InitializeItems(character, itemElement, submarine);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null)
|
||||
{
|
||||
ItemPrefab itemPrefab;
|
||||
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
|
||||
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
Item item = new Item(itemPrefab, character.Position, null);
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && Entity.Spawner != null)
|
||||
{
|
||||
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
|
||||
{
|
||||
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
|
||||
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
|
||||
}
|
||||
|
||||
Entity.Spawner.CreateNetworkEvent(item, false);
|
||||
}
|
||||
#endif
|
||||
if (itemElement.GetAttributeBool("equip", false))
|
||||
{
|
||||
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
|
||||
allowedSlots.Remove(InvSlotType.Any);
|
||||
|
||||
character.Inventory.TryPutItem(item, null, allowedSlots);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
|
||||
}
|
||||
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
|
||||
{
|
||||
item.AddTag("name:" + character.Name);
|
||||
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
|
||||
var job = character.Info?.Job;
|
||||
if (job != null)
|
||||
{
|
||||
item.AddTag("job:" + job.Name);
|
||||
}
|
||||
}
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
{
|
||||
wifiComponent.TeamID = character.TeamID;
|
||||
}
|
||||
if (parentItem != null)
|
||||
{
|
||||
parentItem.Combine(item, user: null);
|
||||
}
|
||||
foreach (XElement childItemElement in itemElement.Elements())
|
||||
{
|
||||
InitializeItems(character, childItemElement, submarine, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,13 @@ namespace Barotrauma
|
||||
public const ushort EntitySpawnerID = ushort.MaxValue;
|
||||
|
||||
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
|
||||
public static List<Entity> GetEntityList()
|
||||
public static IEnumerable<Entity> GetEntities()
|
||||
{
|
||||
return dictionary.Values.ToList();
|
||||
return dictionary.Values;
|
||||
}
|
||||
|
||||
public static int EntityCount => dictionary.Count;
|
||||
|
||||
public static EntitySpawner Spawner;
|
||||
|
||||
private ushort id;
|
||||
@@ -80,6 +82,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ID the entity had after instantiation/loading. May have been taken up by another entity, causing a new ID to be assigned to this entity.
|
||||
/// </summary>
|
||||
public ushort OriginalID;
|
||||
|
||||
public virtual Vector2 SimPosition
|
||||
{
|
||||
get { return Vector2.Zero; }
|
||||
@@ -124,7 +131,7 @@ namespace Barotrauma
|
||||
spawnTime = Timing.TotalTime;
|
||||
|
||||
//give a unique ID
|
||||
id = this is EntitySpawner ?
|
||||
id = OriginalID = this is EntitySpawner ?
|
||||
EntitySpawnerID :
|
||||
FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset);
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (damages.TryGetValue(limb, out float damage))
|
||||
{
|
||||
c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage);
|
||||
c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,19 @@ namespace Barotrauma
|
||||
|
||||
public float Size => IsHorizontal ? Rect.Height : Rect.Width;
|
||||
|
||||
public Door ConnectedDoor;
|
||||
private Door connectedDoor;
|
||||
public Door ConnectedDoor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connectedDoor != null && connectedDoor.Item.Removed)
|
||||
{
|
||||
connectedDoor = null;
|
||||
}
|
||||
return connectedDoor;
|
||||
}
|
||||
set { connectedDoor = value; }
|
||||
}
|
||||
|
||||
public Structure ConnectedWall;
|
||||
|
||||
@@ -719,9 +731,12 @@ namespace Barotrauma
|
||||
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>();
|
||||
Gap g = new Gap(rect, isHorizontal, submarine)
|
||||
{
|
||||
ID = (ushort)int.Parse(element.Attribute("ID").Value),
|
||||
linkedToID = new List<ushort>(),
|
||||
};
|
||||
g.OriginalID = g.ID;
|
||||
return g;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,16 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private readonly HashSet<string> moduleTags = new HashSet<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Inherited flags from outpost generation.
|
||||
/// </summary>
|
||||
public IEnumerable<string> OutpostModuleTags
|
||||
{
|
||||
get { return moduleTags; }
|
||||
}
|
||||
|
||||
private string roomName;
|
||||
[Editable, Serialize("", true, translationTextTag: "RoomName.")]
|
||||
public string RoomName
|
||||
@@ -83,6 +93,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Color? OriginalAmbientLight = null;
|
||||
|
||||
private Color ambientLight;
|
||||
|
||||
[Editable, Serialize("0,0,0,0", true)]
|
||||
@@ -107,6 +119,16 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
float prevOxygenPercentage = OxygenPercentage;
|
||||
|
||||
if (value.Width != rect.Width)
|
||||
{
|
||||
int arraySize = (int)Math.Ceiling((float)value.Width / WaveWidth + 1);
|
||||
waveY = new float[arraySize];
|
||||
waveVel = new float[arraySize];
|
||||
leftDelta = new float[arraySize];
|
||||
rightDelta = new float[arraySize];
|
||||
}
|
||||
|
||||
base.Rect = value;
|
||||
|
||||
if (Submarine == null || !Submarine.Loading)
|
||||
@@ -238,7 +260,6 @@ namespace Barotrauma
|
||||
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];
|
||||
|
||||
@@ -321,6 +342,15 @@ namespace Barotrauma
|
||||
return newGrid;
|
||||
}
|
||||
|
||||
public void SetModuleTags(IEnumerable<string> tags)
|
||||
{
|
||||
moduleTags.Clear();
|
||||
foreach (string tag in tags)
|
||||
{
|
||||
moduleTags.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
CeilingHeight = Rect.Height;
|
||||
@@ -530,7 +560,8 @@ namespace Barotrauma
|
||||
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];
|
||||
// ReSharper refuses to compile this if it's using "as Hull" since "as" means it can be null and you can't compare null to true or false
|
||||
Hull hull2 = this == gap.linkedTo[0] ? (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; }
|
||||
|
||||
@@ -836,9 +867,23 @@ namespace Barotrauma
|
||||
else if (roomItems.Contains("ballast"))
|
||||
return "RoomName.Ballast";
|
||||
|
||||
if (ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
|
||||
var moduleFlags = Submarine?.Info?.OutpostModuleInfo?.ModuleFlags ?? this.moduleTags;
|
||||
|
||||
if (moduleFlags != null && moduleFlags.Any() &&
|
||||
(Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost))
|
||||
{
|
||||
return "RoomName.Airlock";
|
||||
if (moduleFlags.Contains("airlock") &&
|
||||
ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
|
||||
{
|
||||
return "RoomName.Airlock";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null))
|
||||
{
|
||||
return "RoomName.Airlock";
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle subRect = Submarine.CalculateDimensions();
|
||||
@@ -883,8 +928,9 @@ namespace Barotrauma
|
||||
WaterVolume = element.GetAttributeFloat("pressure", 0.0f),
|
||||
ID = (ushort)int.Parse(element.Attribute("ID").Value)
|
||||
};
|
||||
|
||||
hull.OriginalID = hull.ID;
|
||||
hull.linkedToID = new List<ushort>();
|
||||
|
||||
string linkedToString = element.GetAttributeString("linked", "");
|
||||
if (linkedToString != "")
|
||||
{
|
||||
@@ -895,6 +941,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
string originalAmbientLight = element.GetAttributeString("originalambientlight", null);
|
||||
if (!string.IsNullOrWhiteSpace(originalAmbientLight))
|
||||
{
|
||||
hull.OriginalAmbientLight = XMLExtensions.ParseColor(originalAmbientLight, false);
|
||||
}
|
||||
|
||||
SerializableProperty.DeserializeProperties(hull, element);
|
||||
if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; }
|
||||
|
||||
@@ -924,9 +976,15 @@ namespace Barotrauma
|
||||
|
||||
if (linkedTo != null && linkedTo.Count > 0)
|
||||
{
|
||||
var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved).ToList();
|
||||
var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && !l.Removed).ToList();
|
||||
element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
|
||||
}
|
||||
|
||||
if (OriginalAmbientLight != null)
|
||||
{
|
||||
element.Add(new XAttribute("originalambientlight", XMLExtensions.ColorToString(OriginalAmbientLight.Value)));
|
||||
}
|
||||
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
parentElement.Add(element);
|
||||
return element;
|
||||
|
||||
@@ -53,11 +53,24 @@ namespace Barotrauma
|
||||
name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName;
|
||||
Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description;
|
||||
|
||||
List<ushort> containedItemIDs = new List<ushort>();
|
||||
foreach (XElement entityElement in doc.Root.Elements())
|
||||
{
|
||||
var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase));
|
||||
if (containerElement == null) { continue; }
|
||||
|
||||
var itemIds = containerElement.GetAttributeIntArray("contained", new int[0]);
|
||||
containedItemIDs.AddRange(itemIds.Select(id => (ushort)id));
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
ushort id = (ushort)entityElement.GetAttributeInt("ID", 0);
|
||||
if (id > 0 && containedItemIDs.Contains(id)) { continue; }
|
||||
|
||||
string identifier = entityElement.GetAttributeString("identifier", entityElement.Name.ToString().ToLowerInvariant());
|
||||
MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier);
|
||||
if (mapEntity == null)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LevelData
|
||||
{
|
||||
public enum LevelType
|
||||
{
|
||||
LocationConnection,
|
||||
Outpost
|
||||
}
|
||||
|
||||
public readonly LevelType Type;
|
||||
|
||||
public readonly string Seed;
|
||||
|
||||
public float Difficulty;
|
||||
|
||||
public readonly Biome Biome;
|
||||
|
||||
public readonly LevelGenerationParams GenerationParams;
|
||||
|
||||
public OutpostGenerationParams ForceOutpostGenerationParams;
|
||||
|
||||
public readonly Point Size;
|
||||
|
||||
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
|
||||
|
||||
public LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome)
|
||||
{
|
||||
Seed = seed ?? throw new ArgumentException("Seed was null");
|
||||
Biome = biome ?? throw new ArgumentException("Biome was null");
|
||||
GenerationParams = generationParams ?? throw new ArgumentException("Level generation parameters were null");
|
||||
Type = GenerationParams.Type;
|
||||
Difficulty = difficulty;
|
||||
|
||||
sizeFactor = MathHelper.Clamp(sizeFactor, 0.0f, 1.0f);
|
||||
int width = (int)MathHelper.Lerp(generationParams.MinWidth, generationParams.MaxWidth, sizeFactor);
|
||||
|
||||
Size = new Point(
|
||||
(int)MathUtils.Round(width, Level.GridCellSize),
|
||||
(int)MathUtils.Round(generationParams.Height, Level.GridCellSize));
|
||||
}
|
||||
|
||||
public LevelData(XElement element)
|
||||
{
|
||||
Seed = element.GetAttributeString("seed", "");
|
||||
Difficulty = element.GetAttributeFloat("difficulty", 0.0f);
|
||||
Size = element.GetAttributePoint("size", new Point(1000));
|
||||
Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type);
|
||||
|
||||
string generationParamsId = element.GetAttributeString("generationparams", "");
|
||||
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId);
|
||||
if (GenerationParams == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\".");
|
||||
GenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(l => l.Type == Type);
|
||||
if (GenerationParams == null)
|
||||
{
|
||||
GenerationParams = LevelGenerationParams.LevelParams.First();
|
||||
}
|
||||
}
|
||||
|
||||
string biomeIdentifier = element.GetAttributeString("biome", "");
|
||||
Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier);
|
||||
if (Biome == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\".");
|
||||
Biome = LevelGenerationParams.GetBiomes().First();
|
||||
}
|
||||
|
||||
string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { });
|
||||
EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase))));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates level data using the properties of the connection (seed, size, difficulty)
|
||||
/// </summary>
|
||||
public LevelData(LocationConnection locationConnection)
|
||||
{
|
||||
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
|
||||
Biome = locationConnection.Biome;
|
||||
Type = LevelType.LocationConnection;
|
||||
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome);
|
||||
Difficulty = locationConnection.Difficulty;
|
||||
|
||||
float sizeFactor = MathUtils.InverseLerp(
|
||||
MapGenerationParams.Instance.SmallLevelConnectionLength,
|
||||
MapGenerationParams.Instance.LargeLevelConnectionLength,
|
||||
locationConnection.Length);
|
||||
int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, sizeFactor);
|
||||
Size = new Point(
|
||||
(int)MathUtils.Round(width, Level.GridCellSize),
|
||||
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates level data using the properties of the location
|
||||
/// </summary>
|
||||
public LevelData(Location location)
|
||||
{
|
||||
Seed = location.BaseName;
|
||||
Biome = location.Biome;
|
||||
Type = LevelType.Outpost;
|
||||
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome);
|
||||
Difficulty = 0.0f;
|
||||
|
||||
var rand = new MTRandom(ToolBox.StringToInt(Seed));
|
||||
int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble());
|
||||
Size = new Point(
|
||||
(int)MathUtils.Round(width, Level.GridCellSize),
|
||||
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
|
||||
}
|
||||
|
||||
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(seed))
|
||||
{
|
||||
seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString();
|
||||
}
|
||||
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type;
|
||||
|
||||
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
|
||||
var biome =
|
||||
LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ??
|
||||
LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server);
|
||||
|
||||
return new LevelData(
|
||||
seed,
|
||||
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server),
|
||||
Rand.Range(0.0f, 1.0f, Rand.RandSync.Server),
|
||||
generationParams,
|
||||
biome);
|
||||
}
|
||||
|
||||
public void Save(XElement parentElement)
|
||||
{
|
||||
var newElement = new XElement("Level",
|
||||
new XAttribute("seed", Seed),
|
||||
new XAttribute("biome", Biome.Identifier),
|
||||
new XAttribute("type", Type.ToString()),
|
||||
new XAttribute("difficulty", Difficulty.ToString("G", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("size", XMLExtensions.PointToString(Size)),
|
||||
new XAttribute("generationparams", GenerationParams.Identifier));
|
||||
|
||||
if (Type == LevelType.Outpost && EventHistory.Any())
|
||||
{
|
||||
newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier))));
|
||||
}
|
||||
|
||||
parentElement.Add(newElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ namespace Barotrauma
|
||||
public readonly string DisplayName;
|
||||
public readonly string Description;
|
||||
|
||||
public readonly bool IsEndBiome;
|
||||
|
||||
public readonly List<int> AllowedZones = new List<int>();
|
||||
|
||||
public Biome(string name, string description)
|
||||
@@ -40,37 +42,25 @@ namespace Barotrauma
|
||||
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);
|
||||
}
|
||||
IsEndBiome = element.GetAttributeBool("endbiome", false);
|
||||
|
||||
AllowedZones.AddRange(element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }));
|
||||
}
|
||||
}
|
||||
|
||||
class LevelGenerationParams : ISerializableEntity
|
||||
{
|
||||
public static List<LevelGenerationParams> LevelParams
|
||||
{
|
||||
get { return levelParams; }
|
||||
}
|
||||
public static List<LevelGenerationParams> LevelParams { get; private set; }
|
||||
|
||||
private static List<LevelGenerationParams> levelParams;
|
||||
private static List<Biome> biomes;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
get { return Identifier; }
|
||||
}
|
||||
|
||||
public readonly string Identifier;
|
||||
|
||||
private int minWidth, maxWidth, height;
|
||||
|
||||
private Point voronoiSiteInterval;
|
||||
@@ -107,7 +97,7 @@ namespace Barotrauma
|
||||
private float waterParticleScale;
|
||||
|
||||
//which biomes can this type of level appear in
|
||||
private List<Biome> allowedBiomes = new List<Biome>();
|
||||
private readonly List<Biome> allowedBiomes = new List<Biome>();
|
||||
|
||||
public IEnumerable<Biome> AllowedBiomes
|
||||
{
|
||||
@@ -120,6 +110,13 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(LevelData.LevelType.LocationConnection, true), Editable]
|
||||
public LevelData.LevelType Type
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("27,30,36", true), Editable]
|
||||
public Color AmbientLightColor
|
||||
{
|
||||
@@ -148,6 +145,39 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
private Vector2 startPosition;
|
||||
[Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable]
|
||||
public Vector2 StartPosition
|
||||
{
|
||||
get { return startPosition; }
|
||||
set
|
||||
{
|
||||
startPosition = new Vector2(
|
||||
MathHelper.Clamp(value.X, 0.0f, 1.0f),
|
||||
MathHelper.Clamp(value.Y, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 endPosition;
|
||||
[Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable]
|
||||
public Vector2 EndPosition
|
||||
{
|
||||
get { return endPosition; }
|
||||
set
|
||||
{
|
||||
endPosition = new Vector2(
|
||||
MathHelper.Clamp(value.X, 0.0f, 1.0f),
|
||||
MathHelper.Clamp(value.Y, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(true, true, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable]
|
||||
public bool CreateHoleNextToEnd
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -176,6 +206,13 @@ namespace Barotrauma
|
||||
set { height = Math.Max(value, 2000); }
|
||||
}
|
||||
|
||||
[Serialize(6500, true), Editable(MinValueInt = 5000, MaxValueInt = 1000000)]
|
||||
public int MinTunnelRadius
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize("3000, 3000", true, description: "How far from each other voronoi sites are placed. " +
|
||||
"Sites determine shape of the voronoi graph which the level walls are generated from. " +
|
||||
"(Decreasing this value causes the number of sites, and the complexity of the level, to increase exponentially - be careful when adjusting)")]
|
||||
@@ -386,31 +423,43 @@ namespace Barotrauma
|
||||
public Sprite WallEdgeSpriteSpecular { get; private set; }
|
||||
public Sprite WaterParticles { get; private set; }
|
||||
|
||||
public static List<Biome> GetBiomes()
|
||||
public static IEnumerable<Biome> GetBiomes()
|
||||
{
|
||||
return biomes;
|
||||
}
|
||||
|
||||
public static LevelGenerationParams GetRandom(string seed, Biome biome = null)
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Biome biome = null)
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
if (levelParams == null || !levelParams.Any())
|
||||
if (LevelParams == null || !LevelParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError("Level generation presets not found - using default presets");
|
||||
return new LevelGenerationParams(null);
|
||||
}
|
||||
|
||||
var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any());
|
||||
if (biome == null)
|
||||
{
|
||||
return levelParams.GetRandom(lp => lp.allowedBiomes.Count > 0, Rand.RandSync.Server);
|
||||
matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.Any(b => b.IsEndBiome));
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingLevelParams = matchingLevelParams.FindAll(lp => lp.allowedBiomes.Contains(biome));
|
||||
}
|
||||
|
||||
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);
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{(biome?.Identifier ?? "null")}\", type: \"{type}\"!");
|
||||
if (biome != null)
|
||||
{
|
||||
//try to find params that at least have a suitable type
|
||||
matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type);
|
||||
if (matchingLevelParams.Count == 0)
|
||||
{
|
||||
//still not found, give up and choose some params randomly
|
||||
matchingLevelParams = LevelParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchingLevelParams[Rand.Range(0, matchingLevelParams.Count, Rand.RandSync.Server)];
|
||||
@@ -418,11 +467,14 @@ namespace Barotrauma
|
||||
|
||||
private LevelGenerationParams(XElement element)
|
||||
{
|
||||
Name = element == null ? "default" : element.Name.ToString();
|
||||
Identifier = element == null ? "default" :
|
||||
element.GetAttributeString("identifier", null) ?? element.Name.ToString();
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
if (element == null) { return; }
|
||||
|
||||
string biomeStr = element.GetAttributeString("biomes", "");
|
||||
if (string.IsNullOrWhiteSpace(biomeStr))
|
||||
if (string.IsNullOrWhiteSpace(biomeStr) || biomeStr.Equals("any", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
allowedBiomes = new List<Biome>(biomes);
|
||||
}
|
||||
@@ -445,7 +497,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.NewMessage("Please use biome identifiers instead of names in level generation parameter \"" + Name + "\".", Color.Orange);
|
||||
DebugConsole.NewMessage("Please use biome identifiers instead of names in level generation parameter \"" + Identifier + "\".", Color.Orange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +536,7 @@ namespace Barotrauma
|
||||
|
||||
public static void LoadPresets()
|
||||
{
|
||||
levelParams = new List<LevelGenerationParams>();
|
||||
LevelParams = new List<LevelGenerationParams>();
|
||||
biomes = new List<Biome>();
|
||||
|
||||
var files = GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters);
|
||||
@@ -549,7 +601,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (XElement levelParamElement in levelParamElements)
|
||||
{
|
||||
levelParams.Add(new LevelGenerationParams(levelParamElement));
|
||||
LevelParams.Add(new LevelGenerationParams(levelParamElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LevelObject
|
||||
partial class LevelObject : ISpatialEntity
|
||||
{
|
||||
public readonly LevelObjectPrefab Prefab;
|
||||
public Vector3 Position;
|
||||
@@ -50,6 +50,14 @@ namespace Barotrauma
|
||||
get { return spriteIndex < 0 || Prefab.SpecularSprites.Count == 0 ? null : Prefab.SpecularSprites[spriteIndex % Prefab.SpecularSprites.Count]; }
|
||||
}
|
||||
|
||||
Vector2 ISpatialEntity.Position => new Vector2(Position.X, Position.Y);
|
||||
|
||||
public Vector2 WorldPosition => new Vector2(Position.X, Position.Y);
|
||||
|
||||
public Vector2 SimPosition => ConvertUnits.ToSimUnits(WorldPosition);
|
||||
|
||||
public Submarine Submarine => null;
|
||||
|
||||
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
|
||||
{
|
||||
Triggers = new List<LevelTrigger>();
|
||||
|
||||
+45
-25
@@ -31,6 +31,8 @@ namespace Barotrauma
|
||||
public readonly Alignment Alignment;
|
||||
public readonly float Length;
|
||||
|
||||
private readonly float noiseVal;
|
||||
|
||||
public SpawnPosition(GraphEdge graphEdge, Vector2 normal, LevelObjectPrefab.SpawnPosType spawnPosType, Alignment alignment)
|
||||
{
|
||||
GraphEdge = graphEdge;
|
||||
@@ -39,16 +41,16 @@ namespace Barotrauma
|
||||
Alignment = alignment;
|
||||
|
||||
Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2);
|
||||
|
||||
noiseVal =
|
||||
(float)(PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 10000.0f, GraphEdge.Point1.Y / 10000.0f, 0.5f) +
|
||||
PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 20000.0f, GraphEdge.Point1.Y / 20000.0f, 0.5f));
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if (prefab.ClusteringAmount <= 0.0f) { return Length; }
|
||||
float noise = (noiseVal + PerlinNoise.GetPerlin(prefab.ClusteringGroup, prefab.ClusteringGroup * 0.3f)) % 1.0f;
|
||||
return Length * (float)Math.Pow(noise, prefab.ClusteringAmount);
|
||||
}
|
||||
}
|
||||
@@ -90,15 +92,33 @@ namespace Barotrauma
|
||||
Alignment.Top));
|
||||
}
|
||||
|
||||
availableSpawnPositions.Add(new SpawnPosition(
|
||||
new GraphEdge(level.StartPosition - Vector2.UnitX, level.StartPosition + Vector2.UnitX),
|
||||
-Vector2.UnitY, LevelObjectPrefab.SpawnPosType.LevelStart, Alignment.Top));
|
||||
availableSpawnPositions.Add(new SpawnPosition(
|
||||
new GraphEdge(level.EndPosition - Vector2.UnitX, level.EndPosition + Vector2.UnitX),
|
||||
-Vector2.UnitY, LevelObjectPrefab.SpawnPosType.LevelEnd, Alignment.Top));
|
||||
|
||||
var availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List);
|
||||
objects = new List<LevelObject>();
|
||||
|
||||
Dictionary<LevelObjectPrefab, List<SpawnPosition>> suitableSpawnPositions = new Dictionary<LevelObjectPrefab, List<SpawnPosition>>();
|
||||
Dictionary<LevelObjectPrefab, List<float>> spawnPositionWeights = new Dictionary<LevelObjectPrefab, List<float>>();
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
//get a random prefab and find a place to spawn it
|
||||
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams.Name);
|
||||
|
||||
SpawnPosition spawnPosition = FindObjectPosition(availableSpawnPositions, level, prefab);
|
||||
LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams.Identifier, availablePrefabs);
|
||||
if (prefab == null) { continue; }
|
||||
if (!suitableSpawnPositions.ContainsKey(prefab))
|
||||
{
|
||||
suitableSpawnPositions.Add(prefab, availableSpawnPositions.Where(sp =>
|
||||
prefab.SpawnPos.HasFlag(sp.SpawnPosType) && (sp.Length >= prefab.MinSurfaceWidth && prefab.Alignment.HasFlag(sp.Alignment) || sp.SpawnPosType == LevelObjectPrefab.SpawnPosType.LevelEnd || sp.SpawnPosType == LevelObjectPrefab.SpawnPosType.LevelStart)).ToList());
|
||||
spawnPositionWeights.Add(prefab,
|
||||
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
|
||||
}
|
||||
|
||||
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) continue;
|
||||
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
|
||||
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
|
||||
|
||||
float rotation = 0.0f;
|
||||
if (prefab.AlignWithSurface && spawnPosition != null)
|
||||
@@ -124,6 +144,13 @@ namespace Barotrauma
|
||||
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);
|
||||
if (prefab.MaxCount < amount)
|
||||
{
|
||||
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
|
||||
{
|
||||
availablePrefabs.Remove(prefab);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
|
||||
{
|
||||
@@ -145,9 +172,7 @@ namespace Barotrauma
|
||||
AddObject(childObject, level);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void AddObject(LevelObject newObject, Level level)
|
||||
@@ -321,16 +346,6 @@ namespace Barotrauma
|
||||
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)
|
||||
@@ -383,10 +398,15 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private LevelObjectPrefab GetRandomPrefab(string levelType)
|
||||
{
|
||||
return GetRandomPrefab(levelType, LevelObjectPrefab.List);
|
||||
}
|
||||
|
||||
private LevelObjectPrefab GetRandomPrefab(string levelType, IList<LevelObjectPrefab> availablePrefabs)
|
||||
{
|
||||
return ToolBox.SelectWeightedRandom(
|
||||
LevelObjectPrefab.List,
|
||||
LevelObjectPrefab.List.Select(p => p.GetCommonness(levelType)).ToList(), Rand.RandSync.Server);
|
||||
availablePrefabs,
|
||||
availablePrefabs.Select(p => p.GetCommonness(levelType)).ToList(), Rand.RandSync.Server);
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
|
||||
+11
-1
@@ -41,7 +41,9 @@ namespace Barotrauma
|
||||
Wall = 1,
|
||||
RuinWall = 2,
|
||||
SeaFloor = 4,
|
||||
MainPath = 8
|
||||
MainPath = 8,
|
||||
LevelStart = 16,
|
||||
LevelEnd = 32,
|
||||
}
|
||||
|
||||
public List<Sprite> Sprites
|
||||
@@ -117,6 +119,14 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
[Serialize(10000, false, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)]
|
||||
public int MaxCount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("0.0,1.0", true), Editable]
|
||||
public Vector2 DepthRange
|
||||
{
|
||||
|
||||
@@ -152,11 +152,11 @@ namespace Barotrauma.RuinGeneration
|
||||
{
|
||||
mainElement = doc.Root.FirstElement();
|
||||
paramsList.Clear();
|
||||
DebugConsole.NewMessage($"Overriding all ruin configuration parameters using the file {configFile.Path}.", Color.Yellow);
|
||||
DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow);
|
||||
}
|
||||
else if (paramsList.Any())
|
||||
{
|
||||
DebugConsole.NewMessage($"Adding additional ruin configuration parameters from file '{configFile.Path}'");
|
||||
DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'");
|
||||
}
|
||||
var newParams = new RuinGenerationParams(mainElement)
|
||||
{
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace Barotrauma
|
||||
if (wall.Submarine != sub) { continue; }
|
||||
for (int i = 0; i < wall.SectionCount; i++)
|
||||
{
|
||||
wall.AddDamage(i, -wall.Prefab.Health);
|
||||
wall.AddDamage(i, -wall.MaxHealth);
|
||||
}
|
||||
}
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
|
||||
@@ -1,13 +1,61 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Location
|
||||
{
|
||||
public List<LocationConnection> Connections;
|
||||
public class TakenItem
|
||||
{
|
||||
public readonly ushort OriginalID;
|
||||
public readonly ushort OriginalContainerID;
|
||||
public readonly ushort ModuleIndex;
|
||||
public readonly string Identifier;
|
||||
|
||||
public TakenItem(string identifier, UInt16 originalID, UInt16 originalContainerID, ushort moduleIndex)
|
||||
{
|
||||
OriginalID = originalID;
|
||||
OriginalContainerID = originalContainerID;
|
||||
ModuleIndex = moduleIndex;
|
||||
Identifier = identifier;
|
||||
}
|
||||
|
||||
public TakenItem(Item item)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(item.OriginalModuleIndex >= 0, "Trying to add a non-outpost item to a location's taken items");
|
||||
|
||||
if (item.OriginalContainerID != Entity.NullEntityID)
|
||||
{
|
||||
OriginalContainerID = item.OriginalContainerID;
|
||||
}
|
||||
OriginalID = item.OriginalID;
|
||||
ModuleIndex = (ushort)item.OriginalModuleIndex;
|
||||
Identifier = item.prefab.Identifier;
|
||||
}
|
||||
|
||||
public bool IsEqual(TakenItem obj)
|
||||
{
|
||||
return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier;
|
||||
}
|
||||
public bool Matches(Item item)
|
||||
{
|
||||
if (item.OriginalContainerID != Entity.NullEntityID)
|
||||
{
|
||||
return item.OriginalContainerID == OriginalContainerID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier;
|
||||
}
|
||||
else
|
||||
{
|
||||
return item.OriginalID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
|
||||
|
||||
private string baseName;
|
||||
private int nameFormatIndex;
|
||||
@@ -20,43 +68,59 @@ namespace Barotrauma
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public Biome Biome { get; set; }
|
||||
|
||||
public Vector2 MapPosition { get; private set; }
|
||||
|
||||
public LocationType Type { get; private set; }
|
||||
|
||||
public int PortraitId { get; private set; }
|
||||
public LevelData LevelData { get; set; }
|
||||
|
||||
public int MissionsCompleted;
|
||||
|
||||
private List<Mission> availableMissions = new List<Mission>();
|
||||
public IEnumerable<Mission> AvailableMissions
|
||||
private float normalizedDepth;
|
||||
public float NormalizedDepth
|
||||
{
|
||||
get
|
||||
get { return normalizedDepth; }
|
||||
set
|
||||
{
|
||||
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;
|
||||
if (!MathUtils.IsValid(value)) { return; }
|
||||
normalizedDepth = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public int PortraitId { get; private set; }
|
||||
|
||||
public Reputation Reputation { get; set; }
|
||||
|
||||
private const float StoreMaxReputationModifier = 0.1f;
|
||||
private const float StoreSellPriceModifier = 0.8f;
|
||||
private const float MechanicalMaxDiscountPercentage = 50.0f;
|
||||
public const int StoreInitialBalance = 5000;
|
||||
public int StoreCurrentBalance { get; set; }
|
||||
public List<PurchasedItem> StoreStock { get; set; }
|
||||
|
||||
private readonly List<TakenItem> takenItems = new List<TakenItem>();
|
||||
public IEnumerable<TakenItem> TakenItems
|
||||
{
|
||||
get { return takenItems; }
|
||||
}
|
||||
|
||||
private readonly HashSet<int> killedCharacterIdentifiers = new HashSet<int>();
|
||||
public IEnumerable<int> KilledCharacterIdentifiers
|
||||
{
|
||||
get { return killedCharacterIdentifiers; }
|
||||
}
|
||||
|
||||
private readonly List<Mission> availableMissions = new List<Mission>();
|
||||
public IEnumerable<Mission> AvailableMissions
|
||||
{
|
||||
get
|
||||
{
|
||||
availableMissions.RemoveAll(m => m.Completed);
|
||||
return availableMissions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Mission SelectedMission
|
||||
{
|
||||
get;
|
||||
@@ -81,20 +145,273 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand)
|
||||
private float priceMultiplier = 1.0f;
|
||||
public float PriceMultiplier
|
||||
{
|
||||
this.Type = LocationType.Random(rand, zone);
|
||||
this.Name = RandomName(Type, rand);
|
||||
this.MapPosition = mapPosition;
|
||||
get { return priceMultiplier; }
|
||||
set { priceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f); }
|
||||
}
|
||||
|
||||
private float mechanicalpriceMultiplier = 1.0f;
|
||||
public float MechanicalPriceMultiplier
|
||||
{
|
||||
get => mechanicalpriceMultiplier;
|
||||
set => mechanicalpriceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f);
|
||||
}
|
||||
|
||||
public string LastTypeChangeMessage;
|
||||
|
||||
private struct LoadedMission
|
||||
{
|
||||
public MissionPrefab MissionPrefab { get; }
|
||||
public int DestinationIndex { get; }
|
||||
public bool SelectedMission { get; }
|
||||
|
||||
public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission)
|
||||
{
|
||||
MissionPrefab = prefab;
|
||||
DestinationIndex = destinationIndex;
|
||||
SelectedMission = selectedMission;
|
||||
}
|
||||
}
|
||||
|
||||
private List<LoadedMission> loadedMissions;
|
||||
|
||||
public HireManager HireManager;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Location ({Name ?? "null"})";
|
||||
}
|
||||
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
Type = LocationType.Random(rand, zone, requireOutpost);
|
||||
Name = RandomName(Type, rand, existingLocations);
|
||||
MapPosition = mapPosition;
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
|
||||
Connections = new List<LocationConnection>();
|
||||
}
|
||||
|
||||
public static Location CreateRandom(Vector2 position, int? zone , Random rand)
|
||||
public Location(XElement element)
|
||||
{
|
||||
return new Location(position, zone, rand);
|
||||
string locationType = element.GetAttributeString("type", "");
|
||||
Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase));
|
||||
baseName = element.GetAttributeString("basename", "");
|
||||
Name = element.GetAttributeString("name", "");
|
||||
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
|
||||
NormalizedDepth = element.GetAttributeFloat("normalizeddepth", 0.0f);
|
||||
TypeChangeTimer = element.GetAttributeInt("changetimer", 0);
|
||||
Discovered = element.GetAttributeBool("discovered", false);
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
|
||||
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
|
||||
|
||||
string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]);
|
||||
foreach (string takenItem in takenItemStr)
|
||||
{
|
||||
string[] takenItemSplit = takenItem.Split(';');
|
||||
if (takenItemSplit.Length != 4)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in saved location: could not parse taken item data \"{takenItem}\"");
|
||||
continue;
|
||||
}
|
||||
if (!ushort.TryParse(takenItemSplit[1], out ushort id))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\"");
|
||||
continue;
|
||||
}
|
||||
if (!ushort.TryParse(takenItemSplit[2], out ushort containerId))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in saved location: could not parse taken container id \"{takenItemSplit[2]}\"");
|
||||
continue;
|
||||
}
|
||||
if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
|
||||
continue;
|
||||
}
|
||||
takenItems.Add(new TakenItem(takenItemSplit[0], id, containerId, moduleIndex));
|
||||
}
|
||||
|
||||
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", new int[0]).ToHashSet();
|
||||
|
||||
System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationType}\"!");
|
||||
if (Type == null)
|
||||
{
|
||||
Type = LocationType.List.First();
|
||||
}
|
||||
|
||||
LevelData = new LevelData(element.Element("Level"));
|
||||
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
|
||||
if (element.GetChildElement("store") is XElement storeElement)
|
||||
{
|
||||
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
|
||||
StoreStock = LoadStoreStock(storeElement);
|
||||
}
|
||||
|
||||
LoadMissions(element);
|
||||
}
|
||||
|
||||
public void LoadMissions(XElement locationElement)
|
||||
{
|
||||
if (locationElement.GetChildElement("missions") is XElement missionsElement)
|
||||
{
|
||||
loadedMissions = new List<LoadedMission>();
|
||||
foreach (XElement childElement in missionsElement.GetChildElements("mission"))
|
||||
{
|
||||
var id = childElement.GetAttributeString("prefabid", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = MissionPrefab.List.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase));
|
||||
if (prefab == null) { continue; }
|
||||
var destination = childElement.GetAttributeInt("destinationindex", -1);
|
||||
var selected = childElement.GetAttributeBool("selected", false);
|
||||
loadedMissions.Add(new LoadedMission(prefab, destination, selected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
return new Location(position, zone, rand, requireOutpost, existingLocations);
|
||||
}
|
||||
|
||||
public void ChangeType(LocationType newType)
|
||||
{
|
||||
if (newType == Type) { return; }
|
||||
|
||||
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
|
||||
|
||||
Type = newType;
|
||||
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
CreateStore(force: true);
|
||||
}
|
||||
|
||||
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
|
||||
availableMissions.Add(InstantiateMission(missionPrefab, connection));
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
}
|
||||
|
||||
public MissionPrefab UnlockMissionByIdentifier(string identifier)
|
||||
{
|
||||
if (AvailableMissions.Any(m => m.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))) { return null; }
|
||||
|
||||
var missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (missionPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var mission = InstantiateMission(missionPrefab);
|
||||
//don't allow duplicate missions in the same connection
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
return missionPrefab;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public MissionPrefab UnlockMissionByTag(string tag)
|
||||
{
|
||||
var matchingMissions = MissionPrefab.List.FindAll(mp => mp.Tags.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)));
|
||||
if (!matchingMissions.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions not found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m));
|
||||
if (unusedMissions.Any())
|
||||
{
|
||||
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this))));
|
||||
if (!suitableMissions.Any())
|
||||
{
|
||||
suitableMissions = unusedMissions;
|
||||
}
|
||||
MissionPrefab missionPrefab = suitableMissions.GetRandom();
|
||||
var mission = InstantiateMission(missionPrefab);
|
||||
//don't allow duplicate missions in the same connection
|
||||
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
|
||||
#endif
|
||||
return missionPrefab;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection = null)
|
||||
{
|
||||
if (connection == null)
|
||||
{
|
||||
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
|
||||
if (!suitableConnections.Any())
|
||||
{
|
||||
suitableConnections = Connections;
|
||||
}
|
||||
//prefer connections that haven't been passed through, and connections with fewer available missions
|
||||
connection = ToolBox.SelectWeightedRandom(
|
||||
suitableConnections.ToList(),
|
||||
suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(),
|
||||
Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
Location destination = connection.OtherLocation(this);
|
||||
return prefab.Instantiate(new Location[] { this, destination });
|
||||
}
|
||||
|
||||
public void InstantiateLoadedMissions(Map map)
|
||||
{
|
||||
availableMissions.Clear();
|
||||
if (loadedMissions == null || loadedMissions.None()) { return; }
|
||||
foreach (LoadedMission loadedMission in loadedMissions)
|
||||
{
|
||||
Location destination = null;
|
||||
if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
|
||||
{
|
||||
destination = map.Locations[loadedMission.DestinationIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
destination = Connections.First().OtherLocation(this);
|
||||
}
|
||||
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination });
|
||||
availableMissions.Add(mission);
|
||||
if (loadedMission.SelectedMission) { SelectedMission = mission; }
|
||||
}
|
||||
loadedMissions = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all unlocked missions from the location
|
||||
/// </summary>
|
||||
public void ClearMissions()
|
||||
{
|
||||
availableMissions.Clear();
|
||||
SelectedMissionIndex = -1;
|
||||
}
|
||||
|
||||
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
|
||||
@@ -102,50 +419,346 @@ namespace Barotrauma
|
||||
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
|
||||
return AvailableMissions.Where(m => m.Locations[1] == connection.OtherLocation(this));
|
||||
}
|
||||
|
||||
public void ChangeType(LocationType newType)
|
||||
|
||||
public void RemoveHireableCharacter(CharacterInfo character)
|
||||
{
|
||||
if (newType == Type) { return; }
|
||||
|
||||
//clear missions from this and adjacent locations (they may be invalid now)
|
||||
availableMissions.Clear();
|
||||
foreach (LocationConnection connection in Connections)
|
||||
if (!Type.HasHireableCharacters)
|
||||
{
|
||||
connection.OtherLocation(this)?.availableMissions.Clear();
|
||||
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
if (HireManager == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
|
||||
|
||||
Type = newType;
|
||||
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
HireManager.RemoveCharacter(character);
|
||||
}
|
||||
|
||||
public void CheckMissionCompleted()
|
||||
public IEnumerable<CharacterInfo> GetHireableCharacters()
|
||||
{
|
||||
foreach (Mission mission in availableMissions)
|
||||
if (!Type.HasHireableCharacters)
|
||||
{
|
||||
if (mission.Completed)
|
||||
{
|
||||
DebugConsole.Log("Mission \"" + mission.Name + "\" completed in \"" + Name + "\".");
|
||||
MissionsCompleted++;
|
||||
}
|
||||
return Enumerable.Empty<CharacterInfo>();
|
||||
}
|
||||
|
||||
availableMissions.RemoveAll(m => m.Completed);
|
||||
HireManager ??= new HireManager();
|
||||
|
||||
if (!HireManager.AvailableCharacters.Any())
|
||||
{
|
||||
HireManager.GenerateCharacters(location: this, amount: HireManager.MaxAvailableCharacters);
|
||||
}
|
||||
return HireManager.AvailableCharacters;
|
||||
}
|
||||
|
||||
private string RandomName(LocationType type, Random rand)
|
||||
private string RandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
|
||||
{
|
||||
baseName = type.GetRandomName(rand);
|
||||
baseName = type.GetRandomName(rand, existingLocations);
|
||||
if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; }
|
||||
nameFormatIndex = rand.Next() % type.NameFormats.Count;
|
||||
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
|
||||
}
|
||||
|
||||
private List<PurchasedItem> CreateStoreStock()
|
||||
{
|
||||
var stock = new List<PurchasedItem>();
|
||||
foreach (ItemPrefab prefab in ItemPrefab.Prefabs)
|
||||
{
|
||||
if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo))
|
||||
{
|
||||
var quantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount :
|
||||
(priceInfo.MaxAvailableAmount > 0 ? Math.Min(priceInfo.MaxAvailableAmount, 5) : 5);
|
||||
stock.Add(new PurchasedItem(prefab, quantity));
|
||||
}
|
||||
}
|
||||
return stock;
|
||||
}
|
||||
|
||||
public static List<PurchasedItem> LoadStoreStock(XElement storeElement)
|
||||
{
|
||||
var stock = new List<PurchasedItem>();
|
||||
if (storeElement == null) { return stock; }
|
||||
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
|
||||
{
|
||||
var id = stockElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
|
||||
if (prefab == null) { continue; }
|
||||
var qty = stockElement.GetAttributeInt("qty", 0);
|
||||
if (qty < 1) { continue; }
|
||||
stock.Add(new PurchasedItem(prefab, qty));
|
||||
}
|
||||
return stock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
|
||||
/// </summary>
|
||||
public void RegisterTakenItems(IEnumerable<Item> items)
|
||||
{
|
||||
foreach (Item item in items)
|
||||
{
|
||||
if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.OriginalID)) { continue; }
|
||||
if (item.OriginalModuleIndex < 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost.");
|
||||
continue;
|
||||
}
|
||||
takenItems.Add(new TakenItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the characters who have been killed to prevent them from spawning when re-entering the outpost
|
||||
/// </summary>
|
||||
public void RegisterKilledCharacters(IEnumerable<Character> characters)
|
||||
{
|
||||
foreach (Character character in characters)
|
||||
{
|
||||
if (character?.Info == null) { continue; }
|
||||
killedCharacterIdentifiers.Add(character.Info.GetIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveTakenItems()
|
||||
{
|
||||
foreach (TakenItem takenItem in takenItems)
|
||||
{
|
||||
Item item = Item.ItemList.Find(it => takenItem.Matches(it));
|
||||
item?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public int GetAdjustedItemBuyPrice(PriceInfo priceInfo)
|
||||
{
|
||||
// TODO: Check priceInfo.CanBeBought
|
||||
if (priceInfo == null) { return 0; }
|
||||
var price = priceInfo.Price;
|
||||
if (Reputation.Value > 0.0f)
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
|
||||
}
|
||||
else
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
|
||||
}
|
||||
// Item price should never go below 1 mk
|
||||
return Math.Max(price, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If item.GetPriceInfo() returns null, this will return 0
|
||||
/// </summary>
|
||||
public int GetAdjustedItemBuyPrice(ItemPrefab item) => GetAdjustedItemBuyPrice(item?.GetPriceInfo(this));
|
||||
|
||||
public int GetAdjustedItemSellPrice(PriceInfo priceInfo)
|
||||
{
|
||||
if (priceInfo == null) { return 0; }
|
||||
var price = (int)(StoreSellPriceModifier * priceInfo.Price);
|
||||
if (Reputation.Value > 0.0f)
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
|
||||
}
|
||||
else
|
||||
{
|
||||
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
|
||||
}
|
||||
// Item price should never go below 1 mk
|
||||
return Math.Max(price, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If item.GetPriceInfo() returns null, this will return 0
|
||||
/// </summary>
|
||||
public int GetAdjustedItemSellPrice(ItemPrefab item) => GetAdjustedItemSellPrice(item?.GetPriceInfo(this));
|
||||
|
||||
public int GetAdjustedMechanicalCost(int cost)
|
||||
{
|
||||
float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f);
|
||||
return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If 'force' is true, the stock will be recreated even if it has been created previously already.
|
||||
/// This is used when (at least) when the type of the location changes.
|
||||
/// </summary>
|
||||
public void CreateStore(bool force = false)
|
||||
{
|
||||
if (!force && StoreStock != null) { return; }
|
||||
|
||||
if (StoreStock != null)
|
||||
{
|
||||
StoreCurrentBalance = Math.Max(StoreCurrentBalance, StoreInitialBalance);
|
||||
var newStock = CreateStoreStock();
|
||||
foreach (PurchasedItem oldStockItem in StoreStock)
|
||||
{
|
||||
if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is PurchasedItem newStockItem)
|
||||
{
|
||||
if (oldStockItem.Quantity > newStockItem.Quantity)
|
||||
{
|
||||
newStockItem.Quantity = oldStockItem.Quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
StoreStock = newStock;
|
||||
}
|
||||
else
|
||||
{
|
||||
StoreCurrentBalance = StoreInitialBalance;
|
||||
StoreStock = CreateStoreStock();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateStore()
|
||||
{
|
||||
if (StoreStock == null)
|
||||
{
|
||||
CreateStore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (StoreCurrentBalance < StoreInitialBalance)
|
||||
{
|
||||
StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
|
||||
}
|
||||
|
||||
var stock = StoreStock;
|
||||
var stockToRemove = new List<PurchasedItem>();
|
||||
foreach (PurchasedItem item in stock)
|
||||
{
|
||||
if (item.ItemPrefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo))
|
||||
{
|
||||
item.Quantity += 1;
|
||||
if (priceInfo.MaxAvailableAmount > 0)
|
||||
{
|
||||
item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
stockToRemove.Add(item);
|
||||
}
|
||||
}
|
||||
stockToRemove.ForEach(i => stock.Remove(i));
|
||||
StoreStock = stock;
|
||||
}
|
||||
|
||||
public void AddToStock(List<SoldItem> items)
|
||||
{
|
||||
if (StoreStock == null || items == null) { return; }
|
||||
#if DEBUG
|
||||
if (items.Any()) { DebugConsole.NewMessage("Adding items to stock at " + Name, Color.Purple); }
|
||||
#endif
|
||||
foreach (SoldItem item in items)
|
||||
{
|
||||
if (StoreStock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
|
||||
{
|
||||
stockItem.Quantity += 1;
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage("Added 1x " + item.ItemPrefab.Name + ", new total: " + stockItem.Quantity, Color.Cyan);
|
||||
#endif
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
DebugConsole.NewMessage(item.ItemPrefab.Name + " not sold at location, can't add", Color.Cyan);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveFromStock(List<PurchasedItem> items)
|
||||
{
|
||||
if (StoreStock == null || items == null) { return; }
|
||||
#if DEBUG
|
||||
if (items.Any()) { DebugConsole.NewMessage("Removing items from stock at " + Name, Color.Purple); }
|
||||
#endif
|
||||
foreach (PurchasedItem item in items)
|
||||
{
|
||||
if (StoreStock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
|
||||
{
|
||||
stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0);
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage("Removed " + item.Quantity + "x " + item.ItemPrefab.Name + ", new total: " + stockItem.Quantity, Color.Cyan);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public XElement Save(Map map, XElement parentElement)
|
||||
{
|
||||
var locationElement = new XElement("location",
|
||||
new XAttribute("type", Type.Identifier),
|
||||
new XAttribute("basename", BaseName),
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("discovered", Discovered),
|
||||
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
|
||||
new XAttribute("normalizeddepth", NormalizedDepth.ToString("G", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("pricemultiplier", PriceMultiplier),
|
||||
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier));
|
||||
LevelData.Save(locationElement);
|
||||
|
||||
if (TypeChangeTimer > 0)
|
||||
{
|
||||
locationElement.Add(new XAttribute("changetimer", TypeChangeTimer));
|
||||
}
|
||||
if (takenItems.Any())
|
||||
{
|
||||
locationElement.Add(new XAttribute(
|
||||
"takenitems",
|
||||
string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerID + ";" + it.ModuleIndex))));
|
||||
}
|
||||
if (killedCharacterIdentifiers.Any())
|
||||
{
|
||||
locationElement.Add(new XAttribute("killedcharacters", string.Join(',', killedCharacterIdentifiers)));
|
||||
}
|
||||
|
||||
if (StoreStock != null)
|
||||
{
|
||||
var storeElement = new XElement("store", new XAttribute("balance", StoreCurrentBalance));
|
||||
foreach (PurchasedItem item in StoreStock)
|
||||
{
|
||||
if (item?.ItemPrefab == null) { continue; }
|
||||
storeElement.Add(new XElement("stock",
|
||||
new XAttribute("id", item.ItemPrefab.Identifier),
|
||||
new XAttribute("qty", item.Quantity)));
|
||||
}
|
||||
locationElement.Add(storeElement);
|
||||
}
|
||||
|
||||
if (AvailableMissions is List<Mission> missions && missions.Any())
|
||||
{
|
||||
var missionsElement = new XElement("missions");
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
var location = mission.Locations.FirstOrDefault(l => l != this);
|
||||
var i = map.Locations.IndexOf(location);
|
||||
missionsElement.Add(new XElement("mission",
|
||||
new XAttribute("prefabid", mission.Prefab.Identifier),
|
||||
new XAttribute("destinationindex", i),
|
||||
new XAttribute("selected", mission == SelectedMission)));
|
||||
}
|
||||
locationElement.Add(missionsElement);
|
||||
}
|
||||
|
||||
parentElement.Add(locationElement);
|
||||
|
||||
return locationElement;
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
public void RemoveProjSpecific()
|
||||
{
|
||||
HireManager?.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ namespace Barotrauma
|
||||
|
||||
public float Difficulty;
|
||||
|
||||
public List<Vector2[]> CrackSegments;
|
||||
public readonly List<Vector2[]> CrackSegments = new List<Vector2[]>();
|
||||
|
||||
public bool Passed;
|
||||
|
||||
public Level Level { get; set; }
|
||||
public LevelData LevelData { get; set; }
|
||||
|
||||
public Vector2 CenterPos
|
||||
{
|
||||
@@ -33,8 +33,7 @@ namespace Barotrauma
|
||||
|
||||
public LocationConnection(Location location1, Location location2)
|
||||
{
|
||||
Locations = new Location[] { location1, location2 };
|
||||
|
||||
Locations = new Location[] { location1, location2 };
|
||||
Length = Vector2.Distance(location1.MapPosition, location2.MapPosition);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ namespace Barotrauma
|
||||
{
|
||||
public static readonly List<LocationType> List = new List<LocationType>();
|
||||
|
||||
private List<string> nameFormats;
|
||||
private List<string> names;
|
||||
private readonly List<string> nameFormats;
|
||||
private readonly List<string> names;
|
||||
|
||||
private Sprite symbolSprite;
|
||||
private readonly Sprite symbolSprite;
|
||||
|
||||
private readonly List<Sprite> portraits = new List<Sprite>();
|
||||
|
||||
@@ -47,6 +47,12 @@ namespace Barotrauma
|
||||
get { return hireableJobs.Any(); }
|
||||
}
|
||||
|
||||
public bool HasOutpost
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Sprite Sprite
|
||||
{
|
||||
get { return symbolSprite; }
|
||||
@@ -66,9 +72,10 @@ namespace Barotrauma
|
||||
private LocationType(XElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeString("identifier", element.Name.ToString());
|
||||
Name = TextManager.Get("LocationName." + Identifier);
|
||||
Name = TextManager.Get("LocationName." + Identifier, fallBackTag: "unknown");
|
||||
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
|
||||
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
|
||||
HasOutpost = element.GetAttributeBool("hasoutpost", true);
|
||||
|
||||
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
|
||||
try
|
||||
@@ -158,16 +165,25 @@ namespace Barotrauma
|
||||
return portraits[Math.Abs(portraitId) % portraits.Count];
|
||||
}
|
||||
|
||||
public string GetRandomName(Random rand)
|
||||
public string GetRandomName(Random rand, IEnumerable<Location> existingLocations)
|
||||
{
|
||||
if (existingLocations != null)
|
||||
{
|
||||
var unusedNames = names.Where(name => !existingLocations.Any(l => l.BaseName == name)).ToList();
|
||||
if (unusedNames.Count > 0)
|
||||
{
|
||||
return unusedNames[rand.Next() % unusedNames.Count];
|
||||
}
|
||||
}
|
||||
return names[rand.Next() % names.Count];
|
||||
}
|
||||
|
||||
public static LocationType Random(Random rand, int? zone = null)
|
||||
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false)
|
||||
{
|
||||
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;
|
||||
List<LocationType> allowedLocationTypes =
|
||||
List.FindAll(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost));
|
||||
|
||||
if (allowedLocationTypes.Count == 0)
|
||||
{
|
||||
@@ -239,6 +255,11 @@ namespace Barotrauma
|
||||
List.Add(locationType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (EventSet eventSet in EventSet.List)
|
||||
{
|
||||
eventSet.CheckLocationTypeErrors();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,24 @@ namespace Barotrauma
|
||||
{
|
||||
partial class Map
|
||||
{
|
||||
private MapGenerationParams generationParams;
|
||||
|
||||
private readonly int size;
|
||||
private readonly MapGenerationParams generationParams;
|
||||
|
||||
private Location furthestDiscoveredLocation;
|
||||
|
||||
private int Width => generationParams.Width;
|
||||
private int Height => generationParams.Height;
|
||||
|
||||
private List<LocationConnection> connections;
|
||||
public Action<Location, LocationConnection> OnLocationSelected;
|
||||
//from -> to
|
||||
/// <summary>
|
||||
/// From -> To
|
||||
/// </summary>
|
||||
public Action<Location, Location> OnLocationChanged;
|
||||
public Action<LocationConnection, Mission> OnMissionSelected;
|
||||
|
||||
public Location EndLocation { get; private set; }
|
||||
|
||||
public Location StartLocation { get; private set; }
|
||||
|
||||
public Location CurrentLocation { get; private set; }
|
||||
|
||||
public int CurrentLocationIndex
|
||||
@@ -44,140 +52,176 @@ namespace Barotrauma
|
||||
|
||||
public List<Location> Locations { get; private set; }
|
||||
|
||||
public Map(string seed)
|
||||
public List<LocationConnection> Connections { get; private set; }
|
||||
|
||||
public Map()
|
||||
{
|
||||
generationParams = MapGenerationParams.Instance;
|
||||
this.Seed = seed;
|
||||
this.size = generationParams.Size;
|
||||
|
||||
Locations = new List<Location>();
|
||||
Connections = new List<LocationConnection>();
|
||||
}
|
||||
|
||||
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)
|
||||
/// <summary>
|
||||
/// Load a previously saved campaign map from XML
|
||||
/// </summary>
|
||||
private Map(CampaignMode campaign, XElement element) : this()
|
||||
{
|
||||
Seed = element.GetAttributeString("seed", "a");
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (location.Type.Identifier != "City") continue;
|
||||
float dist = Vector2.DistanceSquared(center, location.MapPosition);
|
||||
if (dist > largestDist)
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
largestDist = dist;
|
||||
CurrentLocation = location;
|
||||
case "location":
|
||||
int i = subElement.GetAttributeInt("i", 0);
|
||||
while (Locations.Count <= i)
|
||||
{
|
||||
Locations.Add(null);
|
||||
}
|
||||
Locations[i] = new Location(subElement);
|
||||
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentLocation.Discovered = true;
|
||||
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
|
||||
|
||||
foreach (LocationConnection connection in connections)
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
connection.Level = Level.CreateRandom(connection);
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "connection":
|
||||
Point locationIndices = subElement.GetAttributePoint("locations", new Point(0, 1));
|
||||
var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
|
||||
{
|
||||
Passed = subElement.GetAttributeBool("passed", false),
|
||||
Difficulty = subElement.GetAttributeFloat("difficulty", 0.0f)
|
||||
};
|
||||
Locations[locationIndices.X].Connections.Add(connection);
|
||||
Locations[locationIndices.Y].Connections.Add(connection);
|
||||
connection.LevelData = new LevelData(subElement.Element("Level"));
|
||||
string biomeId = subElement.GetAttributeString("biome", "");
|
||||
connection.Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ?? LevelGenerationParams.GetBiomes().First();
|
||||
Connections.Add(connection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int startLocationindex = element.GetAttributeInt("startlocation", -1);
|
||||
if (startLocationindex > 0 && startLocationindex < Locations.Count)
|
||||
{
|
||||
StartLocation = Locations[startLocationindex];
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Error while loading the map (start location index out of bounds).");
|
||||
}
|
||||
int endLocationindex = element.GetAttributeInt("endlocation", -1);
|
||||
if (endLocationindex > 0 && endLocationindex < Locations.Count)
|
||||
{
|
||||
EndLocation = Locations[endLocationindex];
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Error while loading the map (end location index out of bounds).");
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (EndLocation == null || location.MapPosition.X > EndLocation.MapPosition.X)
|
||||
{
|
||||
EndLocation = location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InitProjectSpecific();
|
||||
}
|
||||
|
||||
partial void InitProjectSpecific();
|
||||
|
||||
public float[,] Noise;
|
||||
|
||||
private void GenerateNoiseMap(int octaves, float persistence)
|
||||
/// <summary>
|
||||
/// Generate a new campaign map from the seed
|
||||
/// </summary>
|
||||
public Map(CampaignMode campaign, string seed) : this()
|
||||
{
|
||||
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++)
|
||||
Seed = seed;
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
|
||||
|
||||
Generate();
|
||||
|
||||
if (Locations.Count == 0)
|
||||
{
|
||||
for (int y = 0; y < generationParams.NoiseResolution; y++)
|
||||
throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < Locations.Count; i++)
|
||||
{
|
||||
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Type.Identifier.Equals("city", StringComparison.OrdinalIgnoreCase) &&
|
||||
!location.Type.Identifier.Equals("outpost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
|
||||
{
|
||||
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);
|
||||
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
CurrentLocation.CreateStore();
|
||||
CurrentLocation.Discovered = true;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
InitProjectSpecific();
|
||||
}
|
||||
|
||||
partial void GenerateNoiseMapProjSpecific();
|
||||
partial void InitProjectSpecific();
|
||||
|
||||
#region Generation
|
||||
|
||||
private void Generate()
|
||||
{
|
||||
connections.Clear();
|
||||
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)
|
||||
List<Vector2> voronoiSites = new List<Vector2>();
|
||||
for (float x = 10.0f; x < Width - 10.0f; x += generationParams.VoronoiSiteInterval.X)
|
||||
{
|
||||
for (float y = mapCenter.Y - locationRadius; y < mapCenter.Y + locationRadius; y += generationParams.VoronoiSiteInterval)
|
||||
for (float y = 10.0f; y < Height - 10.0f; y += generationParams.VoronoiSiteInterval.Y)
|
||||
{
|
||||
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));
|
||||
}
|
||||
voronoiSites.Add(new Vector2(
|
||||
x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server),
|
||||
y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server)));
|
||||
}
|
||||
}
|
||||
|
||||
Voronoi voronoi = new Voronoi(0.5f);
|
||||
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(sites, size, size);
|
||||
float zoneRadius = size / 2 / generationParams.DifficultyZones;
|
||||
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
|
||||
sites.Clear();
|
||||
Vector2 margin = new Vector2(
|
||||
Math.Min(10, Width * 0.1f),
|
||||
Math.Min(10, Height * 0.2f));
|
||||
|
||||
float startX = margin.X, endX = Width - margin.X;
|
||||
float startY = margin.Y, endY = Height - margin.Y;
|
||||
|
||||
if (!edges.Any())
|
||||
{
|
||||
throw new Exception($"Generating a campaign map failed (no edges in the voronoi graph). Width: {Width}, height: {Height}, margin: {margin}");
|
||||
}
|
||||
|
||||
voronoiSites.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;
|
||||
if (edge.Point1 == edge.Point2) { continue; }
|
||||
|
||||
if (edge.Point1.X < margin.X || edge.Point1.X > Width - margin.X || edge.Point1.Y < startY || edge.Point1.Y > endY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (edge.Point2.X < margin.X || edge.Point2.X > Width - margin.X || edge.Point2.Y < startY || edge.Point2.Y > endY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Location[] newLocations = new Location[2];
|
||||
newLocations[0] = Locations.Find(l => l.MapPosition == edge.Point1 || l.MapPosition == edge.Point2);
|
||||
@@ -193,23 +237,20 @@ namespace Barotrauma
|
||||
|
||||
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));
|
||||
int zone = MathHelper.Clamp((int)Math.Floor(position.X / zoneWidth) + 1, 1, generationParams.DifficultyZones);
|
||||
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, Locations);
|
||||
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);
|
||||
Connections.Add(newConnection);
|
||||
}
|
||||
|
||||
//remove connections that are too short
|
||||
float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
|
||||
for (int i = connections.Count - 1; i >= 0; i--)
|
||||
for (int i = Connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
LocationConnection connection = connections[i];
|
||||
LocationConnection connection = Connections[i];
|
||||
|
||||
if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
|
||||
{
|
||||
@@ -217,17 +258,17 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//locations.Remove(connection.Locations[0]);
|
||||
connections.Remove(connection);
|
||||
Connections.Remove(connection);
|
||||
|
||||
foreach (LocationConnection connection2 in connections)
|
||||
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];
|
||||
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)
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
connection.Locations[0].Connections.Add(connection);
|
||||
connection.Locations[1].Connections.Add(connection);
|
||||
@@ -263,57 +304,182 @@ namespace Barotrauma
|
||||
}
|
||||
Locations[i].Connections.Add(connection);
|
||||
}
|
||||
Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]);
|
||||
Locations.RemoveAt(j);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = connections.Count - 1; i >= 0; i--)
|
||||
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--)
|
||||
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]))
|
||||
if (connection.Locations.Contains(Connections[n].Locations[0])
|
||||
&& connection.Locations.Contains(Connections[n].Locations[1]))
|
||||
{
|
||||
connections.RemoveAt(n);
|
||||
Connections.RemoveAt(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (LocationConnection connection in connections)
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
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);
|
||||
for (int i = location.Connections.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!Connections.Contains(location.Connections[i]))
|
||||
{
|
||||
location.Connections.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
connection.Difficulty = MathHelper.Clamp((connection.CenterPos.X / Width * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f);
|
||||
}
|
||||
|
||||
AssignBiomes();
|
||||
CreateEndLocation();
|
||||
|
||||
GenerateNoiseMapProjSpecific();
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location);
|
||||
location.NormalizedDepth = location.MapPosition.X / Width;
|
||||
}
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
connection.LevelData = new LevelData(connection);
|
||||
}
|
||||
}
|
||||
|
||||
partial void GenerateLocationConnectionVisuals();
|
||||
|
||||
public Biome GetBiome(Vector2 mapPos)
|
||||
{
|
||||
return GetBiome(mapPos.X);
|
||||
}
|
||||
|
||||
public Biome GetBiome(float xPos)
|
||||
{
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
|
||||
if (zoneIndex < 1)
|
||||
{
|
||||
return LevelGenerationParams.GetBiomes().First();
|
||||
|
||||
}
|
||||
else if (zoneIndex >= generationParams.DifficultyZones)
|
||||
{
|
||||
return LevelGenerationParams.GetBiomes().Last();
|
||||
}
|
||||
return LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
|
||||
}
|
||||
|
||||
private void AssignBiomes()
|
||||
{
|
||||
float locationRadius = size * 0.5f * generationParams.LocationRadius;
|
||||
|
||||
var biomes = LevelGenerationParams.GetBiomes();
|
||||
Vector2 centerPos = new Vector2(size, size) / 2;
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
|
||||
List<Biome> allowedBiomes = new List<Biome>(10);
|
||||
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;
|
||||
allowedBiomes.Clear();
|
||||
allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
|
||||
float zoneX = Width - zoneWidth * i;
|
||||
|
||||
if (i == generationParams.DifficultyZones - 1 ||
|
||||
Vector2.Distance(connection.Locations[0].MapPosition, centerPos) < zoneRadius ||
|
||||
Vector2.Distance(connection.Locations[1].MapPosition, centerPos) < zoneRadius)
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (location.MapPosition.X < zoneX)
|
||||
{
|
||||
connection.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)];
|
||||
location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
if (connection.Biome != null) { continue; }
|
||||
connection.Biome = connection.Locations[0].Biome;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(Locations.All(l => l.Biome != null));
|
||||
System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
|
||||
}
|
||||
|
||||
private void CreateEndLocation()
|
||||
{
|
||||
float zoneWidth = Width / generationParams.DifficultyZones;
|
||||
Vector2 endPos = new Vector2(Width - zoneWidth / 2, Height / 2);
|
||||
float closestDist = float.MaxValue;
|
||||
EndLocation = Locations.First();
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(endPos, location.MapPosition);
|
||||
if (location.Biome.IsEndBiome && dist < closestDist)
|
||||
{
|
||||
EndLocation = location;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
Location previousToEndLocation = null;
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Biome.IsEndBiome && (previousToEndLocation == null || location.MapPosition.X > previousToEndLocation.MapPosition.X))
|
||||
{
|
||||
previousToEndLocation = location;
|
||||
}
|
||||
}
|
||||
|
||||
if (EndLocation == null || previousToEndLocation == null) { return; }
|
||||
|
||||
//remove all locations from the end biome except the end location
|
||||
for (int i = Locations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Locations[i].Biome.IsEndBiome && Locations[i] != EndLocation)
|
||||
{
|
||||
for (int j = Locations[i].Connections.Count - 1; j >= 0; j--)
|
||||
{
|
||||
if (j >= Locations[i].Connections.Count) { continue; }
|
||||
var connection = Locations[i].Connections[j];
|
||||
var otherLocation = connection.OtherLocation(Locations[i]);
|
||||
Locations[i].Connections.RemoveAt(j);
|
||||
otherLocation?.Connections.Remove(connection);
|
||||
Connections.Remove(connection);
|
||||
}
|
||||
Locations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
//removed all connections from the second-to-last location, need to reconnect it
|
||||
if (!previousToEndLocation.Connections.Any())
|
||||
{
|
||||
Location connectTo = Locations.First();
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Biome.IsEndBiome && location != previousToEndLocation && location.MapPosition.X > connectTo.MapPosition.X)
|
||||
{
|
||||
connectTo = location;
|
||||
}
|
||||
}
|
||||
var newConnection = new LocationConnection(previousToEndLocation, connectTo)
|
||||
{
|
||||
Biome = EndLocation.Biome,
|
||||
Difficulty = 100.0f
|
||||
};
|
||||
Connections.Add(newConnection);
|
||||
previousToEndLocation.Connections.Add(newConnection);
|
||||
connectTo.Connections.Add(newConnection);
|
||||
}
|
||||
|
||||
var endConnection = new LocationConnection(previousToEndLocation, EndLocation)
|
||||
{
|
||||
Biome = EndLocation.Biome,
|
||||
Difficulty = 100.0f
|
||||
};
|
||||
Connections.Add(endConnection);
|
||||
previousToEndLocation.Connections.Add(endConnection);
|
||||
EndLocation.Connections.Add(endConnection);
|
||||
}
|
||||
|
||||
private void ExpandBiomes(List<LocationConnection> seeds)
|
||||
@@ -340,9 +506,22 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion Generation
|
||||
|
||||
public void MoveToNextLocation()
|
||||
{
|
||||
if (SelectedConnection == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
if (SelectedLocation == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
Location prevLocation = CurrentLocation;
|
||||
SelectedConnection.Passed = true;
|
||||
|
||||
@@ -350,6 +529,7 @@ namespace Barotrauma
|
||||
CurrentLocation.Discovered = true;
|
||||
SelectedLocation = null;
|
||||
|
||||
CurrentLocation.CreateStore();
|
||||
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
|
||||
}
|
||||
|
||||
@@ -371,6 +551,16 @@ namespace Barotrauma
|
||||
CurrentLocation = Locations[index];
|
||||
CurrentLocation.Discovered = true;
|
||||
|
||||
if (prevLocation != CurrentLocation)
|
||||
{
|
||||
var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
|
||||
if (connection != null)
|
||||
{
|
||||
connection.Passed = true;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentLocation.CreateStore();
|
||||
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
|
||||
}
|
||||
|
||||
@@ -392,7 +582,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
SelectedLocation = Locations[index];
|
||||
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
SelectedConnection =
|
||||
Connections.Find(c => c.Locations.Contains(GameMain.GameSession?.Campaign?.CurrentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
|
||||
Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
|
||||
}
|
||||
|
||||
@@ -407,7 +599,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
SelectedLocation = location;
|
||||
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
|
||||
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
|
||||
}
|
||||
|
||||
@@ -427,7 +619,7 @@ namespace Barotrauma
|
||||
if (CurrentLocation.SelectedMission != null &&
|
||||
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
|
||||
{
|
||||
SelectLocation(CurrentLocation.SelectedMission.Locations[1]);
|
||||
CurrentLocation.SelectedMissionIndex = -1;
|
||||
}
|
||||
|
||||
OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission);
|
||||
@@ -452,7 +644,12 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
if (!location.Discovered) continue;
|
||||
if (!location.Discovered) { continue; }
|
||||
|
||||
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
|
||||
{
|
||||
furthestDiscoveredLocation = location;
|
||||
}
|
||||
|
||||
//find which types of locations this one can change to
|
||||
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
|
||||
@@ -469,7 +666,7 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (disallowedFound) continue;
|
||||
if (disallowedFound) { continue; }
|
||||
|
||||
//check that there's a required adjacent location present
|
||||
bool requiredFound = false;
|
||||
@@ -481,7 +678,7 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) continue;
|
||||
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) { continue; }
|
||||
|
||||
allowedTypeChanges.Add(typeChange);
|
||||
|
||||
@@ -514,22 +711,76 @@ namespace Barotrauma
|
||||
{
|
||||
location.TypeChangeTimer = 0;
|
||||
}
|
||||
|
||||
location.UpdateStore();
|
||||
}
|
||||
}
|
||||
|
||||
public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation)
|
||||
{
|
||||
if (startingLocation.Type.HasOutpost)
|
||||
{
|
||||
endingLocation = startingLocation;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int iterations = 0;
|
||||
int distance = 0;
|
||||
endingLocation = null;
|
||||
|
||||
List<Location> testedLocations = new List<Location>();
|
||||
List<Location> locationsToTest = new List<Location> { startingLocation };
|
||||
|
||||
while (endingLocation == null && iterations < 100)
|
||||
{
|
||||
List<Location> nextTestingBatch = new List<Location>();
|
||||
for (int i = 0; i < locationsToTest.Count; i++)
|
||||
{
|
||||
Location testLocation = locationsToTest[i];
|
||||
for (int j = 0; j < testLocation.Connections.Count; j++)
|
||||
{
|
||||
Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation);
|
||||
if (potentialOutpost.Type.HasOutpost)
|
||||
{
|
||||
distance = iterations + 1;
|
||||
endingLocation = potentialOutpost;
|
||||
}
|
||||
else if (!testedLocations.Contains(potentialOutpost))
|
||||
{
|
||||
nextTestingBatch.Add(potentialOutpost);
|
||||
}
|
||||
}
|
||||
|
||||
testedLocations.Add(testLocation);
|
||||
}
|
||||
|
||||
locationsToTest = nextTestingBatch;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change);
|
||||
partial void ClearAnimQueue();
|
||||
|
||||
public static Map LoadNew(XElement element)
|
||||
/// <summary>
|
||||
/// Load a previously saved map from an xml element
|
||||
/// </summary>
|
||||
public static Map Load(CampaignMode campaign, XElement element)
|
||||
{
|
||||
string mapSeed = element.GetAttributeString("seed", "a");
|
||||
Map map = new Map(mapSeed);
|
||||
map.Load(element, false);
|
||||
|
||||
Map map = new Map(campaign, element);
|
||||
map.LoadState(element, false);
|
||||
#if CLIENT
|
||||
map.DrawOffset = -map.CurrentLocation.MapPosition;
|
||||
#endif
|
||||
return map;
|
||||
}
|
||||
|
||||
public void Load(XElement element, bool showNotifications)
|
||||
/// <summary>
|
||||
/// Load the state of an existing map from xml (current state of locations, where the crew is now, etc).
|
||||
/// </summary>
|
||||
public void LoadState(XElement element, bool showNotifications)
|
||||
{
|
||||
ClearAnimQueue();
|
||||
SetLocation(element.GetAttributeInt("currentlocation", 0));
|
||||
@@ -545,17 +796,27 @@ namespace Barotrauma
|
||||
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);
|
||||
|
||||
location.TypeChangeTimer = subElement.GetAttributeInt("changetimer", 0);
|
||||
location.Discovered = subElement.GetAttributeBool("discovered", false);
|
||||
if (location.Discovered)
|
||||
{
|
||||
#if CLIENT
|
||||
RemoveFogOfWar(StartLocation);
|
||||
#endif
|
||||
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
|
||||
{
|
||||
furthestDiscoveredLocation = location;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string locationType = subElement.GetAttributeString("type", "");
|
||||
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;
|
||||
LocationType newLocationType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)) ?? LocationType.List.First();
|
||||
location.ChangeType(newLocationType);
|
||||
if (showNotifications && prevLocationType != location.Type)
|
||||
{
|
||||
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -564,13 +825,25 @@ namespace Barotrauma
|
||||
ChangeLocationType(location, prevLocationName, change);
|
||||
}
|
||||
}
|
||||
location.LoadMissions(subElement);
|
||||
break;
|
||||
case "connection":
|
||||
int connectionIndex = subElement.GetAttributeInt("i", 0);
|
||||
connections[connectionIndex].Passed = true;
|
||||
Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location?.InstantiateLoadedMissions(this);
|
||||
}
|
||||
|
||||
int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1);
|
||||
if (currentLocationConnection >= 0)
|
||||
{
|
||||
SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation));
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(XElement element)
|
||||
@@ -579,38 +852,39 @@ namespace Barotrauma
|
||||
|
||||
mapElement.Add(new XAttribute("version", GameMain.Version.ToString()));
|
||||
mapElement.Add(new XAttribute("currentlocation", CurrentLocationIndex));
|
||||
if (GameMain.GameSession.GameMode is CampaignMode campaign)
|
||||
{
|
||||
if (campaign.NextLevel != null && campaign.NextLevel.Type == LevelData.LevelType.LocationConnection)
|
||||
{
|
||||
mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(CurrentLocation.Connections.Find(c => c.LevelData == campaign.NextLevel))));
|
||||
}
|
||||
else if (Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection && !CurrentLocation.Type.HasOutpost)
|
||||
{
|
||||
mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData == Level.Loaded.LevelData))));
|
||||
}
|
||||
}
|
||||
mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex));
|
||||
mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation)));
|
||||
mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation)));
|
||||
mapElement.Add(new XAttribute("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);
|
||||
var locationElement = location.Save(this, mapElement);
|
||||
locationElement.Add(new XAttribute("i", i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < connections.Count; i++)
|
||||
for (int i = 0; i < Connections.Count; i++)
|
||||
{
|
||||
var connection = connections[i];
|
||||
if (!connection.Passed) continue;
|
||||
var connection = Connections[i];
|
||||
|
||||
var connectionElement = new XElement("connection",
|
||||
new XAttribute("i", i),
|
||||
new XAttribute("passed", connection.Passed));
|
||||
|
||||
new XAttribute("passed", connection.Passed),
|
||||
new XAttribute("difficulty", connection.Difficulty),
|
||||
new XAttribute("biome", connection.Biome.Identifier),
|
||||
new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1])));
|
||||
connection.LevelData.Save(connectionElement);
|
||||
mapElement.Add(connectionElement);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -20,9 +18,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Serialize(false, true), Editable]
|
||||
public bool ShowNoiseMap { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool ShowLocations { get; set; }
|
||||
|
||||
@@ -40,8 +35,11 @@ namespace Barotrauma
|
||||
[Serialize(6, true)]
|
||||
public int DifficultyZones { get; set; } //Number of difficulty zones
|
||||
|
||||
[Serialize(2000, true)]
|
||||
public int Size { get; set; }
|
||||
[Serialize(8000, true), Editable]
|
||||
public int Width { get; set; }
|
||||
|
||||
[Serialize(500, true), Editable]
|
||||
public int Height { 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; }
|
||||
@@ -49,57 +47,13 @@ namespace Barotrauma
|
||||
[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. " +
|
||||
[Serialize("20,20", 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; }
|
||||
"(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable]
|
||||
public Point VoronoiSiteInterval { get; set; }
|
||||
|
||||
[Serialize(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("5,5", true), Editable]
|
||||
public Point VoronoiSiteVariance { get; set; }
|
||||
|
||||
[Serialize(10.0f, true, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)]
|
||||
public float MinConnectionDistance { get; set; }
|
||||
@@ -107,48 +61,60 @@ namespace Barotrauma
|
||||
[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)]
|
||||
[Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
|
||||
public float ConnectionIndicatorIterationMultiplier { get; set; }
|
||||
|
||||
[Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)]
|
||||
[Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
|
||||
public float ConnectionIndicatorDisplacementMultiplier { get; set; }
|
||||
|
||||
public Sprite ConnectionSprite { get; private set; }
|
||||
|
||||
#if CLIENT
|
||||
|
||||
[Serialize(0.75f, true), Editable(DecimalCount = 2)]
|
||||
public float MinZoom { get; set; }
|
||||
|
||||
[Serialize(1.5f, true), Editable(DecimalCount = 2)]
|
||||
public float MaxZoom { get; set; }
|
||||
|
||||
[Serialize(1.0f, true), Editable(DecimalCount = 2)]
|
||||
public float MapTileScale { get; set; }
|
||||
|
||||
[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; }
|
||||
[Serialize(5.0f, true, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)]
|
||||
public float LocationConnectionWidth { get; set; }
|
||||
|
||||
[Serialize("220,220,100,255", true, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()]
|
||||
public Color IndicatorColor { get; set; }
|
||||
|
||||
[Serialize("150,150,150,255", true, description: "The color used to display the connections between locations."), Editable()]
|
||||
public Color ConnectionColor { get; set; }
|
||||
|
||||
[Serialize("150,150,150,255", true, description: "The color used to display the connections between locations when they're highlighted."), Editable()]
|
||||
public Color HighlightedConnectionColor { get; set; }
|
||||
|
||||
[Serialize("150,150,150,255", true, description: "The color used to display the connections the player hasn't travelled through."), Editable()]
|
||||
public Color UnvisitedConnectionColor { get; set; }
|
||||
|
||||
public Sprite ConnectionSprite { get; private set; }
|
||||
public Sprite PassedConnectionSprite { get; private 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 MissionIcon { get; private set; }
|
||||
public Sprite TypeChangeIcon { get; private set; }
|
||||
|
||||
public Sprite MapCircle { get; private set; }
|
||||
public Sprite LocationIndicator { get; private set; }
|
||||
public Sprite FogOfWarSprite { get; private set; }
|
||||
public Sprite CurrentLocationIndicator { get; private set; }
|
||||
public Sprite SelectedLocationIndicator { get; private set; }
|
||||
|
||||
private readonly Dictionary<string, List<Sprite>> mapTiles = new Dictionary<string, List<Sprite>>();
|
||||
public Dictionary<string, List<Sprite>> MapTiles
|
||||
{
|
||||
get { return mapTiles; }
|
||||
}
|
||||
#endif
|
||||
|
||||
public List<Sprite> BackgroundTileSprites { get; private set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return GetType().ToString(); }
|
||||
@@ -195,19 +161,26 @@ namespace Barotrauma
|
||||
|
||||
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();
|
||||
if (instance != null)
|
||||
{
|
||||
instance?.ConnectionSprite?.Remove();
|
||||
instance?.PassedConnectionSprite?.Remove();
|
||||
instance?.SelectedLocationIndicator?.Remove();
|
||||
instance?.CurrentLocationIndicator?.Remove();
|
||||
instance?.DecorativeGraphSprite?.Remove();
|
||||
instance?.MissionIcon?.Remove();
|
||||
instance?.TypeChangeIcon?.Remove();
|
||||
instance?.FogOfWarSprite?.Remove();
|
||||
foreach (List<Sprite> spriteList in instance.mapTiles.Values)
|
||||
{
|
||||
foreach (Sprite sprite in spriteList)
|
||||
{
|
||||
sprite.Remove();
|
||||
}
|
||||
}
|
||||
instance.mapTiles.Clear();
|
||||
}
|
||||
#endif
|
||||
instance = null;
|
||||
|
||||
@@ -225,48 +198,44 @@ namespace Barotrauma
|
||||
private MapGenerationParams(XElement element)
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
BackgroundTileSprites = new List<Sprite>();
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
#if CLIENT
|
||||
case "connectionsprite":
|
||||
ConnectionSprite = new Sprite(subElement);
|
||||
break;
|
||||
case "backgroundtile":
|
||||
BackgroundTileSprites.Add(new Sprite(subElement));
|
||||
case "passedconnectionsprite":
|
||||
PassedConnectionSprite = new Sprite(subElement);
|
||||
break;
|
||||
#if CLIENT
|
||||
case "mapcircle":
|
||||
MapCircle = new Sprite(subElement);
|
||||
case "maptile":
|
||||
string biome = subElement.GetAttributeString("biome", "");
|
||||
if (!mapTiles.ContainsKey(biome))
|
||||
{
|
||||
mapTiles[biome] = new List<Sprite>();
|
||||
}
|
||||
mapTiles[biome].Add(new Sprite(subElement));
|
||||
break;
|
||||
case "fogofwarsprite":
|
||||
FogOfWarSprite = new Sprite(subElement);
|
||||
break;
|
||||
case "locationindicator":
|
||||
LocationIndicator = new Sprite(subElement);
|
||||
case "currentlocationindicator":
|
||||
CurrentLocationIndicator = new Sprite(subElement);
|
||||
break;
|
||||
case "decorativemapsprite":
|
||||
DecorativeMapSprite = new SpriteSheet(subElement);
|
||||
case "selectedlocationindicator":
|
||||
SelectedLocationIndicator = new Sprite(subElement);
|
||||
break;
|
||||
case "decorativegraphsprite":
|
||||
DecorativeGraphSprite = new SpriteSheet(subElement);
|
||||
break;
|
||||
case "decorativelinetop":
|
||||
DecorativeLineTop = new SpriteSheet(subElement);
|
||||
case "missionicon":
|
||||
MissionIcon = new Sprite(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);
|
||||
case "typechangeicon":
|
||||
TypeChangeIcon = new Sprite(subElement);
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -18,6 +19,33 @@ namespace Barotrauma
|
||||
public readonly MapEntityPrefab prefab;
|
||||
|
||||
protected List<ushort> linkedToID;
|
||||
|
||||
/// <summary>
|
||||
/// List of upgrades this item has
|
||||
/// </summary>
|
||||
protected readonly List<Upgrade> Upgrades = new List<Upgrade>();
|
||||
|
||||
public HashSet<string> disallowedUpgrades = new HashSet<string>();
|
||||
|
||||
[Editable, Serialize("", true)]
|
||||
public string DisallowedUpgrades
|
||||
{
|
||||
get { return string.Join(",", disallowedUpgrades); }
|
||||
set
|
||||
{
|
||||
disallowedUpgrades.Clear();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
string[] splitTags = value.Split(',');
|
||||
foreach (string tag in splitTags)
|
||||
{
|
||||
string[] splitTag = tag.Trim().Split(':');
|
||||
splitTag[0] = splitTag[0].ToLowerInvariant();
|
||||
disallowedUpgrades.Add(string.Join(":", splitTag));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//observable collection because some entities may need to be notified when the collection is modified
|
||||
public readonly ObservableCollection<MapEntity> linkedTo = new ObservableCollection<MapEntity>();
|
||||
@@ -202,6 +230,20 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, true)]
|
||||
public bool RemoveIfLinkedOutpostDoorInUse
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
} = true;
|
||||
|
||||
/// <summary>
|
||||
/// The index of the outpost module this entity originally spawned in (-1 if not an outpost item)
|
||||
/// </summary>
|
||||
public int OriginalModuleIndex = -1;
|
||||
|
||||
public UInt16 OriginalContainerID;
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get { return ""; }
|
||||
@@ -224,6 +266,76 @@ namespace Barotrauma
|
||||
return (Submarine.RectContains(WorldRect, position));
|
||||
}
|
||||
|
||||
public bool HasUpgrade(string identifier)
|
||||
{
|
||||
return GetUpgrade(identifier) != null;
|
||||
}
|
||||
|
||||
public Upgrade GetUpgrade(string identifier)
|
||||
{
|
||||
return Upgrades.Find(upgrade => upgrade.Identifier == identifier);
|
||||
}
|
||||
|
||||
public List<Upgrade> GetUpgrades()
|
||||
{
|
||||
return Upgrades;
|
||||
}
|
||||
|
||||
public void SetUpgrade(Upgrade upgrade, bool createNetworkEvent = false)
|
||||
{
|
||||
Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier);
|
||||
if (existingUpgrade != null)
|
||||
{
|
||||
existingUpgrade.Level = upgrade.Level;
|
||||
existingUpgrade.ApplyUpgrade();
|
||||
upgrade.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
AddUpgrade(upgrade, createNetworkEvent);
|
||||
}
|
||||
DebugConsole.Log($"Set (ID: {ID} {prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new upgrade to the item
|
||||
/// </summary>
|
||||
public virtual bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false)
|
||||
{
|
||||
if (this is Item item && !upgrade.Prefab.UpgradeCategories.Any(category => category.CanBeApplied(item, upgrade.Prefab)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disallowedUpgrades.Contains(upgrade.Identifier)) { return false; }
|
||||
|
||||
Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier);
|
||||
|
||||
if (existingUpgrade != null)
|
||||
{
|
||||
existingUpgrade.Level += upgrade.Level;
|
||||
existingUpgrade.ApplyUpgrade();
|
||||
upgrade.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
upgrade.ApplyUpgrade();
|
||||
Upgrades.Add(upgrade);
|
||||
}
|
||||
|
||||
// not used anymore
|
||||
#if SERVER
|
||||
// if (createNetworkEvent)
|
||||
// {
|
||||
// if (this is IServerSerializable serializable)
|
||||
// {
|
||||
// GameMain.Server.CreateEntityEvent(serializable, new object[] { NetEntityEvent.Type.Upgrade, upgrade });
|
||||
// }
|
||||
// }
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
public abstract MapEntity Clone();
|
||||
|
||||
public static List<MapEntity> Clone(List<MapEntity> entitiesToClone)
|
||||
@@ -503,24 +615,12 @@ namespace Barotrauma
|
||||
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);
|
||||
}
|
||||
}
|
||||
InitializeLoadedLinks(entities);
|
||||
|
||||
List<LinkedSubmarine> linkedSubs = new List<LinkedSubmarine>();
|
||||
for (int i = 0; i < entities.Count; i++)
|
||||
{
|
||||
if (entities[i].mapLoadedCalled) continue;
|
||||
if (entities[i].mapLoadedCalled || entities[i].Removed) { continue; }
|
||||
if (entities[i] is LinkedSubmarine)
|
||||
{
|
||||
linkedSubs.Add((LinkedSubmarine)entities[i]);
|
||||
@@ -544,6 +644,35 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitializeLoadedLinks(IEnumerable<MapEntity> entities)
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Linking the entity \"{e.Name}\" to another entity failed. Could not find an entity with the ID \"{i}\".");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
e.linkedToID.Clear();
|
||||
|
||||
(e as WayPoint)?.InitializeLinks();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnMapLoaded() { }
|
||||
|
||||
public virtual XElement Save(XElement parentElement)
|
||||
|
||||
@@ -111,6 +111,9 @@ namespace Barotrauma
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
[Serialize("", false)]
|
||||
public string AllowedUpgrades { get; set; }
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool HideInMenus { get; set; }
|
||||
@@ -207,6 +210,11 @@ namespace Barotrauma
|
||||
Category = MapEntityCategory.Structure;
|
||||
}
|
||||
|
||||
public string[] GetAllowedUpgrades()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(",");
|
||||
}
|
||||
|
||||
protected virtual void CreateInstance(Rectangle rect)
|
||||
{
|
||||
if (constructor == null) return;
|
||||
@@ -300,6 +308,8 @@ namespace Barotrauma
|
||||
public bool IsLinkAllowed(MapEntityPrefab target)
|
||||
{
|
||||
if (target == null) { return false; }
|
||||
if (target is StructurePrefab && AllowedLinks.Contains("structure")) { return true; }
|
||||
if (target is ItemPrefab && AllowedLinks.Contains("item")) { return true; }
|
||||
return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier)
|
||||
|| target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t));
|
||||
}
|
||||
|
||||
@@ -18,32 +18,47 @@ namespace Barotrauma
|
||||
|
||||
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))
|
||||
try
|
||||
{
|
||||
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))
|
||||
if (!File.Exists(cachePath)) { return; }
|
||||
string[] lines = File.ReadAllLines(cachePath, Encoding.UTF8);
|
||||
if (lines.Length <= 0 || lines[0] != GameMain.Version.ToString()) { return; }
|
||||
foreach (string line in lines.Skip(1))
|
||||
{
|
||||
cache.Add(path, new Tuple<Md5Hash, long>(hash, timeLong));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace}", Microsoft.Xna.Framework.Color.Orange);
|
||||
cache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveCache()
|
||||
{
|
||||
string[] lines = new string[cache.Count+1];
|
||||
#if SERVER
|
||||
//don't save to the cache if the server is owned by a client,
|
||||
//since this suggests that they're running concurrently and
|
||||
//will interfere with each other here
|
||||
if (GameMain.Server?.OwnerConnection != null) { return; }
|
||||
#endif
|
||||
|
||||
string[] lines = new string[cache.Count + 1];
|
||||
lines[0] = GameMain.Version.ToString();
|
||||
int i = 1;
|
||||
foreach (KeyValuePair<string, Tuple<Md5Hash, long>> kpv in cache)
|
||||
@@ -51,7 +66,7 @@ namespace Barotrauma
|
||||
lines[i] = kpv.Key + "|" + kpv.Value.Item1 + "|" + kpv.Value.Item2;
|
||||
i++;
|
||||
}
|
||||
File.WriteAllLines(cachePath, lines);
|
||||
File.WriteAllLines(cachePath, lines, Encoding.UTF8);
|
||||
}
|
||||
|
||||
private bool LoadFromCache(string filename)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal class NPCSet : IDisposable
|
||||
{
|
||||
private static List<NPCSet>? Sets { get; set; }
|
||||
|
||||
private string Identifier { get; }
|
||||
|
||||
private readonly List<HumanPrefab> Humans = new List<HumanPrefab>();
|
||||
|
||||
private bool Disposed { get; set; }
|
||||
|
||||
private NPCSet(XElement element, string filePath)
|
||||
{
|
||||
Identifier = element.GetAttributeString("identifier", string.Empty);
|
||||
|
||||
foreach (XElement npcElement in element.Elements())
|
||||
{
|
||||
Humans.Add(new HumanPrefab(npcElement, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
public static HumanPrefab? Get(string identifier, string npcidentifier)
|
||||
{
|
||||
HumanPrefab prefab = Sets.Where(set => set.Identifier == identifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault();
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{identifier}\".");
|
||||
return null;
|
||||
}
|
||||
return new HumanPrefab(prefab.Element, prefab.FilePath);
|
||||
}
|
||||
|
||||
public static void LoadSets()
|
||||
{
|
||||
Sets?.ForEach(set => set.Dispose());
|
||||
Sets = new List<NPCSet>();
|
||||
IEnumerable<ContentFile> files = GameMain.Instance.GetFilesOfType(ContentType.NPCSets);
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
XElement? rootElement = doc?.Root;
|
||||
|
||||
if (doc == null || rootElement == null) { continue; }
|
||||
|
||||
if (doc.Root.IsOverride())
|
||||
{
|
||||
Sets.Clear();
|
||||
DebugConsole.NewMessage($"Overriding all NPC sets with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
|
||||
foreach (XElement element in rootElement.Elements())
|
||||
{
|
||||
bool isOverride = element.IsOverride();
|
||||
XElement sourceElement = isOverride ? element.FirstElement() : element;
|
||||
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
|
||||
string identifier = sourceElement.GetAttributeString("identifier", null);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"No identifier defined for the NPC set config '{elementName}' in file '{file.Path}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
var existingParams = Sets.Find(set => set.Identifier == identifier);
|
||||
if (existingParams != null)
|
||||
{
|
||||
if (isOverride)
|
||||
{
|
||||
DebugConsole.NewMessage($"Overriding NPC set config '{identifier}' using the file '{file.Path}'", Color.Yellow);
|
||||
Sets.Remove(existingParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Duplicate NPC set config: '{identifier}' defined in {elementName} of '{file.Path}'");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Sets.Add(new NPCSet(element, file.Path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!Disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Humans.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
Disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class OutpostGenerationParams : ISerializableEntity
|
||||
{
|
||||
public static List<OutpostGenerationParams> Params { get; private set; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public string Identifier { get; private set; }
|
||||
|
||||
private readonly List<string> allowedLocationTypes = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Identifiers of the location types this outpost can appear in. If empty, can appear in all types of locations.
|
||||
/// </summary>
|
||||
public IEnumerable<string> AllowedLocationTypes
|
||||
{
|
||||
get { return allowedLocationTypes; }
|
||||
}
|
||||
|
||||
[Serialize(10, isSaveable: true), Editable(MinValueInt = 1, MaxValueInt = 50)]
|
||||
public int TotalModuleCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(200.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float MinHallwayLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, int> moduleCounts = new Dictionary<string, int>();
|
||||
|
||||
public IEnumerable<KeyValuePair<string, int>> ModuleCounts
|
||||
{
|
||||
get { return moduleCounts; }
|
||||
}
|
||||
|
||||
private readonly List<List<HumanPrefab>> humanPrefabLists = new List<List<HumanPrefab>>();
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
|
||||
|
||||
private OutpostGenerationParams(XElement element, string filePath)
|
||||
{
|
||||
Identifier = element.GetAttributeString("identifier", "");
|
||||
Name = element.GetAttributeString("name", Identifier);
|
||||
allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty<string>()).ToList();
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "modulecount":
|
||||
string moduleFlag = (subElement.GetAttributeString("flag", null) ?? subElement.GetAttributeString("moduletype", "")).ToLowerInvariant();
|
||||
moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0);
|
||||
break;
|
||||
case "npcs":
|
||||
humanPrefabLists.Add(new List<HumanPrefab>());
|
||||
foreach (XElement npcElement in subElement.Elements())
|
||||
{
|
||||
string from = npcElement.GetAttributeString("from", string.Empty);
|
||||
|
||||
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
|
||||
if (!string.IsNullOrWhiteSpace(from))
|
||||
{
|
||||
HumanPrefab prefab = NPCSet.Get(from, npcElement.GetAttributeString("identifier", string.Empty));
|
||||
if (prefab != null)
|
||||
{
|
||||
humanPrefabLists.Last().Add(prefab);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
humanPrefabLists.Last().Add(new HumanPrefab(npcElement, filePath));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetModuleCount(string moduleFlag)
|
||||
{
|
||||
if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return int.MaxValue; }
|
||||
return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0;
|
||||
}
|
||||
|
||||
public void SetModuleCount(string moduleFlag, int count)
|
||||
{
|
||||
if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return; }
|
||||
if (count <= 0)
|
||||
{
|
||||
moduleCounts.Remove(moduleFlag);
|
||||
}
|
||||
else
|
||||
{
|
||||
moduleCounts[moduleFlag] = count;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAllowedLocationTypes(IEnumerable<string> allowedLocationTypes)
|
||||
{
|
||||
this.allowedLocationTypes.Clear();
|
||||
foreach (string locationType in allowedLocationTypes)
|
||||
{
|
||||
if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
this.allowedLocationTypes.Add(locationType);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<HumanPrefab> GetHumanPrefabs(Rand.RandSync randSync)
|
||||
{
|
||||
if (humanPrefabLists == null || !humanPrefabLists.Any()) { return Enumerable.Empty<HumanPrefab>(); }
|
||||
return humanPrefabLists.GetRandom(randSync);
|
||||
}
|
||||
|
||||
public static void LoadPresets()
|
||||
{
|
||||
Params = new List<OutpostGenerationParams>();
|
||||
var files = GameMain.Instance.GetFilesOfType(ContentType.OutpostConfig);
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
if (doc?.Root == null) { continue; }
|
||||
var mainElement = doc.Root;
|
||||
if (doc.Root.IsOverride())
|
||||
{
|
||||
Params.Clear();
|
||||
DebugConsole.NewMessage($"Overriding all outpost generation parameters with '{file.Path}'", Color.Yellow);
|
||||
}
|
||||
|
||||
foreach (XElement element in mainElement.Elements())
|
||||
{
|
||||
bool isOverride = element.IsOverride();
|
||||
XElement sourceElement = isOverride ? element.FirstElement() : element;
|
||||
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
|
||||
string identifier = sourceElement.GetAttributeString("identifier", null);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"No identifier defined for the outpost config '{elementName}' in file '{file.Path}'");
|
||||
continue;
|
||||
}
|
||||
var existingParams = Params.Find(p => p.Identifier == identifier);
|
||||
if (existingParams != null)
|
||||
{
|
||||
if (isOverride)
|
||||
{
|
||||
DebugConsole.NewMessage($"Overriding outpost config '{identifier}' using the file '{file.Path}'", Color.Yellow);
|
||||
Params.Remove(existingParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Duplicate outpost config: '{identifier}' defined in {elementName} of '{file.Path}'");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Params.Add(new OutpostGenerationParams(element, file.Path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class OutpostModuleInfo : ISerializableEntity
|
||||
{
|
||||
[Flags]
|
||||
public enum GapPosition
|
||||
{
|
||||
None = 0,
|
||||
Right = 1,
|
||||
Left = 2,
|
||||
Top = 4,
|
||||
Bottom = 8
|
||||
}
|
||||
|
||||
private readonly HashSet<string> moduleFlags = new HashSet<string>();
|
||||
public IEnumerable<string> ModuleFlags
|
||||
{
|
||||
get { return moduleFlags; }
|
||||
}
|
||||
|
||||
private readonly HashSet<string> allowAttachToModules = new HashSet<string>();
|
||||
public IEnumerable<string> AllowAttachToModules
|
||||
{
|
||||
get { return allowAttachToModules; }
|
||||
}
|
||||
|
||||
private readonly HashSet<string> allowedLocationTypes = new HashSet<string>();
|
||||
public IEnumerable<string> AllowedLocationTypes
|
||||
{
|
||||
get { return allowedLocationTypes; }
|
||||
}
|
||||
|
||||
[Serialize(100, isSaveable: true, description: "How many instances of this module can be used in one outpost."), Editable]
|
||||
public int MaxCount { get; set; }
|
||||
|
||||
[Serialize(10.0f, isSaveable: true, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable]
|
||||
public float Commonness { get; set; }
|
||||
|
||||
[Serialize(GapPosition.None, isSaveable: true, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")]
|
||||
public GapPosition GapPositions { get; set; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
|
||||
|
||||
public OutpostModuleInfo(SubmarineInfo submarineInfo, XElement element)
|
||||
{
|
||||
Name = $"OutpostModuleInfo ({submarineInfo.Name})";
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
SetFlags(
|
||||
element.GetAttributeStringArray("flags", null, convertToLowerInvariant: true) ??
|
||||
element.GetAttributeStringArray("moduletypes", new string[0], convertToLowerInvariant: true));
|
||||
SetAllowAttachTo(element.GetAttributeStringArray("allowattachto", new string[0], convertToLowerInvariant: true));
|
||||
allowedLocationTypes = new HashSet<string>(element.GetAttributeStringArray("allowedlocationtypes", new string[0], convertToLowerInvariant: true));
|
||||
}
|
||||
|
||||
public OutpostModuleInfo(SubmarineInfo submarineInfo)
|
||||
{
|
||||
Name = $"OutpostModuleInfo ({submarineInfo.Name})";
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this);
|
||||
}
|
||||
public OutpostModuleInfo(OutpostModuleInfo original)
|
||||
{
|
||||
Name = original.Name;
|
||||
moduleFlags = new HashSet<string>(original.moduleFlags);
|
||||
allowAttachToModules = new HashSet<string>(original.allowAttachToModules);
|
||||
allowedLocationTypes = new HashSet<string>(original.allowedLocationTypes);
|
||||
SerializableProperties = new Dictionary<string, SerializableProperty>();
|
||||
GapPositions = original.GapPositions;
|
||||
foreach (KeyValuePair<string, SerializableProperty> kvp in original.SerializableProperties)
|
||||
{
|
||||
SerializableProperties.Add(kvp.Key, kvp.Value);
|
||||
if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null)
|
||||
{
|
||||
kvp.Value.TrySetValue(this, kvp.Value.GetValue(original));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFlags(IEnumerable<string> newFlags)
|
||||
{
|
||||
moduleFlags.Clear();
|
||||
if (newFlags.Contains("hallwayhorizontal"))
|
||||
{
|
||||
moduleFlags.Add("hallwayhorizontal");
|
||||
return;
|
||||
}
|
||||
if (newFlags.Contains("hallwayvertical"))
|
||||
{
|
||||
moduleFlags.Add("hallwayvertical");
|
||||
return;
|
||||
}
|
||||
if (!newFlags.Any())
|
||||
{
|
||||
moduleFlags.Add("none");
|
||||
}
|
||||
foreach (string flag in newFlags)
|
||||
{
|
||||
if (flag == "none" && newFlags.Count() > 1) { continue; }
|
||||
moduleFlags.Add(flag.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
public void SetAllowAttachTo(IEnumerable<string> allowAttachTo)
|
||||
{
|
||||
allowAttachToModules.Clear();
|
||||
if (!allowAttachTo.Any())
|
||||
{
|
||||
allowAttachToModules.Add("any");
|
||||
}
|
||||
foreach (string flag in allowAttachTo)
|
||||
{
|
||||
if (flag == "any" && allowAttachTo.Count() > 1) { continue; }
|
||||
allowAttachToModules.Add(flag);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAllowedLocationTypes(IEnumerable<string> allowedLocationTypes)
|
||||
{
|
||||
this.allowedLocationTypes.Clear();
|
||||
foreach (string locationType in allowedLocationTypes)
|
||||
{
|
||||
if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
this.allowedLocationTypes.Add(locationType);
|
||||
}
|
||||
}
|
||||
|
||||
public void DetermineGapPositions(Submarine sub)
|
||||
{
|
||||
GapPositions = GapPosition.None;
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (gap.Submarine != sub || gap.linkedTo.Count != 1) { continue; }
|
||||
if (gap.ConnectedDoor != null && !gap.ConnectedDoor.UseBetweenOutpostModules) { continue; }
|
||||
|
||||
//ignore gaps that are at a docking port
|
||||
bool portFound = false;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
if (Submarine.RectContains(gap.WorldRect, port.Item.WorldPosition))
|
||||
{
|
||||
portFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (portFound) { continue; }
|
||||
|
||||
GapPositions |= gap.IsHorizontal ?
|
||||
gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? GapPosition.Right : GapPosition.Left :
|
||||
gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? GapPosition.Top : GapPosition.Bottom;
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(XElement element)
|
||||
{
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
element.SetAttributeValue("flags", string.Join(",", ModuleFlags));
|
||||
element.SetAttributeValue("allowattachto", string.Join(",", AllowAttachToModules));
|
||||
element.SetAttributeValue("allowedlocationtypes", string.Join(",", AllowedLocationTypes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class PriceInfo
|
||||
{
|
||||
public readonly int BuyPrice;
|
||||
|
||||
public readonly int Price;
|
||||
public readonly bool CanBeBought;
|
||||
//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;
|
||||
|
||||
/// <summary>
|
||||
/// Support for the old style of determining item prices
|
||||
/// when there were individual Price elements for each location type
|
||||
/// where the item was for sale.
|
||||
/// </summary>
|
||||
public PriceInfo (XElement element)
|
||||
{
|
||||
BuyPrice = element.GetAttributeInt("buyprice", 0);
|
||||
MinAvailableAmount = element.GetAttributeInt("minamount", 0);
|
||||
MaxAvailableAmount = element.GetAttributeInt("maxamount", 0);
|
||||
Price = element.GetAttributeInt("buyprice", 0);
|
||||
CanBeBought = true;
|
||||
MinAvailableAmount = GetMinAmount(element);
|
||||
MaxAvailableAmount = GetMaxAmount(element);
|
||||
}
|
||||
|
||||
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0)
|
||||
{
|
||||
Price = price;
|
||||
CanBeBought = canBeBought;
|
||||
MinAvailableAmount = minAmount;
|
||||
MaxAvailableAmount = maxAmount;
|
||||
}
|
||||
|
||||
public static List<Tuple<string, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
|
||||
{
|
||||
defaultPrice = null;
|
||||
var basePrice = element.GetAttributeInt("baseprice", 0);
|
||||
var soldByDefault = element.GetAttributeBool("soldbydefault", true);
|
||||
var minAmount = GetMinAmount(element);
|
||||
var maxAmount = GetMaxAmount(element);
|
||||
var priceInfos = new List<Tuple<string, PriceInfo>>();
|
||||
|
||||
foreach (XElement childElement in element.GetChildElements("price"))
|
||||
{
|
||||
var priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
|
||||
var sold = childElement.GetAttributeBool("sold", soldByDefault);
|
||||
priceInfos.Add(new Tuple<string, PriceInfo>(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(),
|
||||
new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold,
|
||||
minAmount: sold ? GetMinAmount(childElement, minAmount) : 0,
|
||||
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0)));
|
||||
}
|
||||
|
||||
var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
|
||||
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
|
||||
minAmount: canBeBoughtAtOtherLocations ? minAmount : 0,
|
||||
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0);
|
||||
|
||||
return priceInfos;
|
||||
}
|
||||
|
||||
private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ?
|
||||
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue;
|
||||
|
||||
private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ?
|
||||
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
|
||||
//dimensions of the wall sections' physics bodies (only used for debug rendering)
|
||||
private List<Vector2> bodyDebugDimensions = new List<Vector2>();
|
||||
private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>();
|
||||
|
||||
public bool Indestructible;
|
||||
|
||||
@@ -100,10 +100,16 @@ namespace Barotrauma
|
||||
get { return Sections.Length; }
|
||||
}
|
||||
|
||||
public float Health
|
||||
private float? maxHealth;
|
||||
|
||||
[Serialize(100.0f, true)]
|
||||
public float MaxHealth
|
||||
{
|
||||
get { return Prefab.Health; }
|
||||
get => maxHealth ?? Prefab.Health;
|
||||
set => maxHealth = value;
|
||||
}
|
||||
|
||||
public float Health => MaxHealth;
|
||||
|
||||
public override bool DrawBelowWater
|
||||
{
|
||||
@@ -232,6 +238,7 @@ namespace Barotrauma
|
||||
if (Prefab.Body)
|
||||
{
|
||||
CreateSections();
|
||||
UpdateSections();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -336,6 +343,8 @@ namespace Barotrauma
|
||||
if (rectangle.Width == 0 || rectangle.Height == 0) { return; }
|
||||
defaultRect = rectangle;
|
||||
|
||||
maxHealth = sp.Health;
|
||||
|
||||
rect = rectangle;
|
||||
TextureScale = sp.TextureScale;
|
||||
|
||||
@@ -722,7 +731,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false;
|
||||
|
||||
return (Sections[sectionIndex].damage >= Prefab.Health);
|
||||
return (Sections[sectionIndex].damage >= MaxHealth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -732,7 +741,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false;
|
||||
|
||||
return (Sections[sectionIndex].damage >= Prefab.Health * LeakThreshold);
|
||||
return (Sections[sectionIndex].damage >= MaxHealth * LeakThreshold);
|
||||
}
|
||||
|
||||
public int SectionLength(int sectionIndex)
|
||||
@@ -741,6 +750,29 @@ namespace Barotrauma
|
||||
|
||||
return (IsHorizontal ? Sections[sectionIndex].rect.Width : Sections[sectionIndex].rect.Height);
|
||||
}
|
||||
|
||||
public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false)
|
||||
{
|
||||
if (!upgrade.Prefab.IsWallUpgrade) { return false; }
|
||||
|
||||
Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier);
|
||||
|
||||
if (existingUpgrade != null)
|
||||
{
|
||||
existingUpgrade.Level += upgrade.Level;
|
||||
existingUpgrade.ApplyUpgrade();
|
||||
upgrade.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Upgrades.Add(upgrade);
|
||||
upgrade.ApplyUpgrade();
|
||||
}
|
||||
|
||||
UpdateSections();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddDamage(int sectionIndex, float damage, Character attacker = null)
|
||||
{
|
||||
@@ -751,7 +783,7 @@ namespace Barotrauma
|
||||
var section = Sections[sectionIndex];
|
||||
|
||||
#if CLIENT
|
||||
float dmg = Math.Min(Health - section.damage, damage);
|
||||
float dmg = Math.Min(MaxHealth - section.damage, damage);
|
||||
float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f)));
|
||||
// Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle
|
||||
if (particleAmount < 1 && Rand.Value() < 0.10f)
|
||||
@@ -773,15 +805,10 @@ namespace Barotrauma
|
||||
if (particle == null) break;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.Client == null)
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
#endif
|
||||
SetDamage(sectionIndex, section.damage + damage, attacker);
|
||||
#if CLIENT
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false)
|
||||
@@ -901,12 +928,12 @@ namespace Barotrauma
|
||||
|
||||
private void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
|
||||
{
|
||||
if (Submarine != null && Submarine.GodMode || Indestructible) return;
|
||||
if (!Prefab.Body) return;
|
||||
if (!MathUtils.IsValid(damage)) return;
|
||||
if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
|
||||
if (!Prefab.Body) { return; }
|
||||
if (!MathUtils.IsValid(damage)) { return; }
|
||||
|
||||
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
|
||||
|
||||
damage = MathHelper.Clamp(damage, 0.0f, Prefab.Health);
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
|
||||
{
|
||||
@@ -923,8 +950,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
if (damage < Prefab.Health * LeakThreshold)
|
||||
if (damage < MaxHealth * LeakThreshold)
|
||||
{
|
||||
if (Sections[sectionIndex].gap != null)
|
||||
{
|
||||
@@ -952,18 +978,18 @@ namespace Barotrauma
|
||||
if (IsHorizontal)
|
||||
{
|
||||
diffFromCenter = (gapRect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth;
|
||||
if (BodyWidth > 0.0f) gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width));
|
||||
if (BodyHeight > 0.0f) gapRect.Height = (int)BodyHeight;
|
||||
if (BodyWidth > 0.0f) { gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width)); }
|
||||
if (BodyHeight > 0.0f) { gapRect.Height = (int)BodyHeight; }
|
||||
}
|
||||
else
|
||||
{
|
||||
diffFromCenter = ((gapRect.Y - gapRect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight;
|
||||
if (BodyWidth > 0.0f) gapRect.Width = (int)BodyWidth;
|
||||
if (BodyHeight > 0.0f) gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height));
|
||||
if (BodyWidth > 0.0f) { gapRect.Width = (int)BodyWidth; }
|
||||
if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); }
|
||||
}
|
||||
if (FlippedX) diffFromCenter = -diffFromCenter;
|
||||
if (FlippedX) { diffFromCenter = -diffFromCenter; }
|
||||
|
||||
if (BodyRotation != 0.0f)
|
||||
if (Math.Abs(BodyRotation) > 0.01f)
|
||||
{
|
||||
Vector2 structureCenter = Position;
|
||||
Vector2 gapPos = structureCenter + new Vector2(
|
||||
@@ -988,6 +1014,7 @@ namespace Barotrauma
|
||||
if (diagonal)
|
||||
{
|
||||
horizontalGap = gapRect.Y - gapRect.Height / 2 < Position.Y;
|
||||
if (FlippedY) { horizontalGap = !horizontalGap; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,13 +1038,13 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
float gapOpen = (damage / Prefab.Health - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
|
||||
float gapOpen = (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
|
||||
Sections[sectionIndex].gap.Open = gapOpen;
|
||||
}
|
||||
|
||||
float damageDiff = damage - Sections[sectionIndex].damage;
|
||||
bool hadHole = SectionBodyDisabled(sectionIndex);
|
||||
Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, Prefab.Health);
|
||||
Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth);
|
||||
|
||||
if (attacker != null && damageDiff != 0.0f)
|
||||
{
|
||||
@@ -1235,6 +1262,7 @@ namespace Barotrauma
|
||||
Submarine = submarine,
|
||||
ID = (ushort)int.Parse(element.Attribute("ID").Value)
|
||||
};
|
||||
s.OriginalID = s.ID;
|
||||
|
||||
SerializableProperty.DeserializeProperties(s, element);
|
||||
|
||||
@@ -1245,7 +1273,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString())
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "section":
|
||||
int index = subElement.GetAttributeInt("i", -1);
|
||||
@@ -1262,6 +1290,22 @@ namespace Barotrauma
|
||||
s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f);
|
||||
}
|
||||
break;
|
||||
case "upgrade":
|
||||
{
|
||||
var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty);
|
||||
UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
|
||||
int level = subElement.GetAttributeInt("level", 1);
|
||||
if (upgradePrefab != null)
|
||||
{
|
||||
s.AddUpgrade(new Upgrade(s, upgradePrefab, level, subElement));
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"An upgrade with identifier \"{upgradeIdentifier}\" on {s.Name} was not found. " +
|
||||
"It's effect will not be applied and won't be saved after the round ends.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1332,6 +1376,11 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
|
||||
foreach (var upgrade in Upgrades)
|
||||
{
|
||||
upgrade.Save(element);
|
||||
}
|
||||
|
||||
parentElement.Add(element);
|
||||
|
||||
|
||||
@@ -92,11 +92,25 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
public float MinHealth
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(100.0f, false)]
|
||||
public float Health
|
||||
{
|
||||
get { return health; }
|
||||
set { health = Math.Max(value, 0.0f); }
|
||||
set { health = Math.Max(value, MinHealth); }
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool IndestructibleInOutposts
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
@@ -236,7 +250,8 @@ namespace Barotrauma
|
||||
var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty;
|
||||
|
||||
string nameIdentifier = element.GetAttributeString("nameidentifier", "");
|
||||
|
||||
string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", "");
|
||||
|
||||
if (string.IsNullOrEmpty(sp.originalName))
|
||||
{
|
||||
if (string.IsNullOrEmpty(nameIdentifier))
|
||||
@@ -374,7 +389,11 @@ namespace Barotrauma
|
||||
|
||||
if (string.IsNullOrEmpty(sp.Description))
|
||||
{
|
||||
if (string.IsNullOrEmpty(nameIdentifier))
|
||||
if (!string.IsNullOrEmpty(descriptionIdentifier))
|
||||
{
|
||||
sp.Description = TextManager.Get("EntityDescription." + descriptionIdentifier, returnNull: true) ?? string.Empty;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(nameIdentifier))
|
||||
{
|
||||
sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -226,9 +226,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public int CalculateBasePrice()
|
||||
{
|
||||
int minPrice = 1000;
|
||||
float volume = Hull.hullList.Where(h => h.Submarine == this).Sum(h => h.Volume);
|
||||
float itemValue = Item.ItemList.Where(it => it.Submarine == this).Sum(it => it.Prefab.GetMinPrice() ?? 0);
|
||||
float price = volume / 500.0f + itemValue / 100.0f;
|
||||
System.Diagnostics.Debug.Assert(price >= 0);
|
||||
return Math.Max(minPrice, (int)price);
|
||||
}
|
||||
|
||||
public void MakeWreck()
|
||||
{
|
||||
Info.Type = SubmarineInfo.SubmarineType.Wreck;
|
||||
Info.Type = SubmarineType.Wreck;
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
TeamID = Character.TeamType.None;
|
||||
@@ -359,7 +369,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Attempt to find a spawn position close to the specified position where the sub doesn't collide with walls/ruins
|
||||
/// </summary>
|
||||
public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f)
|
||||
public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f, int verticalMoveDir = 0)
|
||||
{
|
||||
Rectangle dockedBorders = GetDockedBorders();
|
||||
Vector2 diffFromDockedBorders =
|
||||
@@ -369,58 +379,99 @@ namespace Barotrauma
|
||||
int minWidth = Math.Max(submarineSize.HasValue ? submarineSize.Value.X : dockedBorders.Width, 500);
|
||||
int minHeight = Math.Max(submarineSize.HasValue ? submarineSize.Value.Y : dockedBorders.Height, 1000);
|
||||
//a bit of extra padding to prevent the sub from spawning in a super tight gap between walls
|
||||
minHeight += 500;
|
||||
int padding = 100;
|
||||
minWidth += padding;
|
||||
minHeight += padding;
|
||||
|
||||
float minX = float.MinValue, maxX = float.MaxValue;
|
||||
foreach (VoronoiCell cell in Level.Loaded.GetAllCells())
|
||||
Vector2 limits = GetHorizontalLimits(spawnPos, minWidth, minHeight, 0);
|
||||
if (verticalMoveDir != 0)
|
||||
{
|
||||
if (cell.Edges.All(e => e.Point1.Y < Level.Loaded.Size.Y - minHeight && e.Point2.Y < Level.Loaded.Size.Y - minHeight)) { continue; }
|
||||
|
||||
//find the closest wall at the left and right side of the spawnpos
|
||||
if (cell.Site.Coord.X < spawnPos.X)
|
||||
verticalMoveDir = Math.Sign(verticalMoveDir);
|
||||
//do a raycast towards the top/bottom of the level depending on direction
|
||||
Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
|
||||
if (PickBody(ConvertUnits.ToSimUnits(spawnPos), ConvertUnits.ToSimUnits(potentialPos), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
|
||||
{
|
||||
minX = Math.Max(minX, cell.Edges.Max(e => Math.Max(e.Point1.X, e.Point2.X)));
|
||||
//if the raycast hit a wall, attempt to place the spawnpos there
|
||||
potentialPos.Y = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) - 10;
|
||||
}
|
||||
else
|
||||
|
||||
//step away from the top/bottom of the level, or from whatever wall the raycast hit,
|
||||
//until we found a spot where there's enough room to place the sub
|
||||
float dist = Math.Abs(potentialPos.Y - spawnPos.Y);
|
||||
for (float d = dist; d > 0; d -= 100.0f)
|
||||
{
|
||||
maxX = Math.Min(maxX, cell.Edges.Min(e => Math.Min(e.Point1.X, e.Point2.X)));
|
||||
float y = spawnPos.Y + verticalMoveDir * d;
|
||||
limits = GetHorizontalLimits(new Vector2(spawnPos.X, y), minWidth, minHeight, verticalMoveDir);
|
||||
if (limits.Y - limits.X > minWidth)
|
||||
{
|
||||
spawnPos = new Vector2(spawnPos.X, y - (dockedBorders.Height * 0.5f * verticalMoveDir));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var ruin in Level.Loaded.Ruins)
|
||||
static Vector2 GetHorizontalLimits(Vector2 spawnPos, float minWidth, float minHeight, int verticalMoveDir)
|
||||
{
|
||||
if (ruin.Area.Y + ruin.Area.Height < Level.Loaded.Size.Y - minHeight) { continue; }
|
||||
if (ruin.Area.X < spawnPos.X)
|
||||
Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir);
|
||||
|
||||
float minX = float.MinValue, maxX = float.MaxValue;
|
||||
foreach (VoronoiCell cell in Level.Loaded.GetAllCells())
|
||||
{
|
||||
minX = Math.Max(minX, ruin.Area.Right + 100.0f);
|
||||
foreach (GraphEdge e in cell.Edges)
|
||||
{
|
||||
if ((e.Point1.Y < refPos.Y - minHeight * 0.5f && e.Point2.Y < refPos.Y - minHeight * 0.5f) ||
|
||||
(e.Point1.Y > refPos.Y + minHeight * 0.5f && e.Point2.Y > refPos.Y + minHeight * 0.5f))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cell.Site.Coord.X < refPos.X)
|
||||
{
|
||||
minX = Math.Max(minX, Math.Max(e.Point1.X, e.Point2.X));
|
||||
}
|
||||
else
|
||||
{
|
||||
maxX = Math.Min(maxX, Math.Min(e.Point1.X, e.Point2.X));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
foreach (var ruin in Level.Loaded.Ruins)
|
||||
{
|
||||
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
|
||||
if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; }
|
||||
if (ruin.Area.Center.X < refPos.X)
|
||||
{
|
||||
minX = Math.Max(minX, ruin.Area.Right + 100.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
|
||||
}
|
||||
}
|
||||
return new Vector2(Math.Max(minX, spawnPos.X - minWidth), Math.Min(maxX, spawnPos.X + minWidth));
|
||||
}
|
||||
|
||||
if (minX < 0.0f && maxX > Level.Loaded.Size.X)
|
||||
|
||||
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
|
||||
{
|
||||
//no walls found at either side, just use the initial spawnpos and hope for the best
|
||||
}
|
||||
else if (minX < 0)
|
||||
else if (limits.X < 0)
|
||||
{
|
||||
//no wall found at the left side, spawn to the left from the right-side wall
|
||||
spawnPos.X = maxX - minWidth - 100.0f + subDockingPortOffset;
|
||||
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
|
||||
}
|
||||
else if (maxX > Level.Loaded.Size.X)
|
||||
else if (limits.Y > Level.Loaded.Size.X)
|
||||
{
|
||||
//no wall found at right side, spawn to the right from the left-side wall
|
||||
spawnPos.X = minX + minWidth + 100.0f + subDockingPortOffset;
|
||||
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
//walls found at both sides, use their midpoint
|
||||
spawnPos.X = (minX + maxX) / 2 + subDockingPortOffset;
|
||||
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
|
||||
}
|
||||
|
||||
spawnPos.Y = Math.Min(spawnPos.Y, Level.Loaded.Size.Y - dockedBorders.Height / 2 - 10);
|
||||
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - 10);
|
||||
return spawnPos - diffFromDockedBorders;
|
||||
}
|
||||
|
||||
@@ -429,6 +480,7 @@ namespace Barotrauma
|
||||
DrawPosition = interpolate ?
|
||||
Timing.Interpolate(prevPosition, Position) :
|
||||
Position;
|
||||
if (!interpolate) { prevPosition = Position; }
|
||||
}
|
||||
|
||||
//math/physics stuff ----------------------------------------------------
|
||||
@@ -888,6 +940,76 @@ namespace Barotrauma
|
||||
if (subBody != null) subBody.ApplyForce(force);
|
||||
}
|
||||
|
||||
public void EnableMaintainPosition()
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != this) { continue; }
|
||||
var steering = item.GetComponent<Steering>();
|
||||
if (steering == null) { continue; }
|
||||
steering.MaintainPos = true;
|
||||
steering.AutoPilot = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void NeutralizeBallast()
|
||||
{
|
||||
float neutralBallastLevel = 0.5f;
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != this) { continue; }
|
||||
var steering = item.GetComponent<Steering>();
|
||||
if (steering == null) { continue; }
|
||||
neutralBallastLevel = Math.Min(neutralBallastLevel, steering.NeutralBallastLevel);
|
||||
}
|
||||
|
||||
HashSet<Hull> ballastHulls = new HashSet<Hull>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != this) { continue; }
|
||||
var pump = item.GetComponent<Pump>();
|
||||
if (pump == null || !item.HasTag("ballast") || item.CurrentHull == null) { continue; }
|
||||
pump.FlowPercentage = 0.0f;
|
||||
ballastHulls.Add(item.CurrentHull);
|
||||
}
|
||||
|
||||
float waterVolume = 0.0f;
|
||||
float volume = 0.0f;
|
||||
float excessWater = 0.0f;
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine != this) { continue; }
|
||||
waterVolume += hull.WaterVolume;
|
||||
volume += hull.Volume;
|
||||
if (!ballastHulls.Contains(hull)) { excessWater += hull.WaterVolume; }
|
||||
}
|
||||
|
||||
neutralBallastLevel -= excessWater / volume;
|
||||
//reduce a bit to be on the safe side (better to float up than sink)
|
||||
neutralBallastLevel *= 0.9f;
|
||||
|
||||
foreach (Hull hull in ballastHulls)
|
||||
{
|
||||
hull.WaterVolume = hull.Volume * neutralBallastLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the power logic so the sub is already powered up at the start of the round (as long as the reactor was on)
|
||||
/// </summary>
|
||||
public void WarmStartPower()
|
||||
{
|
||||
for (int i = 0; i < 600; i++)
|
||||
{
|
||||
Powered.UpdatePower((float)Timing.Step);
|
||||
foreach (Entity e in Item.ItemList)
|
||||
{
|
||||
if (!(e is Item item) || item.GetComponent<Powered>() == null || e.Submarine != this) { continue; }
|
||||
item.Update((float)Timing.Step, GameMain.GameScreen.Cam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPrevTransform(Vector2 position)
|
||||
{
|
||||
prevPosition = position;
|
||||
@@ -978,14 +1100,14 @@ namespace Barotrauma
|
||||
return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs));
|
||||
}
|
||||
|
||||
public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs)
|
||||
public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam = false, bool allowDifferentType = false)
|
||||
{
|
||||
if (entity == null) { return false; }
|
||||
if (entity.Submarine == this) { return true; }
|
||||
if (entity.Submarine == null) { return false; }
|
||||
if (includingConnectedSubs)
|
||||
{
|
||||
return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID && entity.Submarine.Info.Type == Info.Type);
|
||||
return GetConnectedSubs().Any(s => s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1030,7 +1152,7 @@ namespace Barotrauma
|
||||
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
|
||||
}
|
||||
|
||||
public Submarine(SubmarineInfo info, bool showWarningMessages = true) : base(null)
|
||||
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null) : base(null)
|
||||
{
|
||||
Loading = true;
|
||||
|
||||
@@ -1040,12 +1162,12 @@ namespace Barotrauma
|
||||
|
||||
//place the sub above the top of the level
|
||||
HiddenSubPosition = HiddenSubStartPosition;
|
||||
if (GameMain.GameSession != null && GameMain.GameSession.Level != null)
|
||||
if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null)
|
||||
{
|
||||
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.Level.Size.Y;
|
||||
HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
|
||||
}
|
||||
|
||||
foreach (Submarine sub in Submarine.loaded)
|
||||
foreach (Submarine sub in loaded)
|
||||
{
|
||||
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
|
||||
}
|
||||
@@ -1057,9 +1179,17 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
List<MapEntity> newEntities = new List<MapEntity>();
|
||||
if (Info.SubmarineElement != null)
|
||||
if (loadEntities == null)
|
||||
{
|
||||
newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath);
|
||||
if (Info.SubmarineElement != null)
|
||||
{
|
||||
newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newEntities = loadEntities(this);
|
||||
newEntities.ForEach(me => me.Submarine = this);
|
||||
}
|
||||
|
||||
Vector2 center = Vector2.Zero;
|
||||
@@ -1082,26 +1212,7 @@ namespace Barotrauma
|
||||
center.X -= center.X % GridSize.X;
|
||||
center.Y -= center.Y % GridSize.Y;
|
||||
|
||||
if (center != Vector2.Zero)
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != this) continue;
|
||||
|
||||
var wire = item.GetComponent<Items.Components.Wire>();
|
||||
if (wire != null)
|
||||
{
|
||||
wire.MoveNodes(-center);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < MapEntity.mapEntityList.Count; i++)
|
||||
{
|
||||
if (MapEntity.mapEntityList[i].Submarine != this) continue;
|
||||
|
||||
MapEntity.mapEntityList[i].Move(-center);
|
||||
}
|
||||
}
|
||||
RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this));
|
||||
|
||||
subBody = new SubmarineBody(this, showWarningMessages);
|
||||
subBody.SetPosition(HiddenSubPosition);
|
||||
@@ -1117,7 +1228,9 @@ namespace Barotrauma
|
||||
if (me.Submarine != this) { continue; }
|
||||
if (me is Item item)
|
||||
{
|
||||
if (item.GetComponent<Repairable>() != null)
|
||||
item.SpawnedInOutpost = true;
|
||||
if (item.GetComponent<Repairable>() != null &&
|
||||
(GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts))
|
||||
{
|
||||
item.Indestructible = true;
|
||||
}
|
||||
@@ -1128,23 +1241,20 @@ namespace Barotrauma
|
||||
//prevent rewiring
|
||||
connectionPanel.Locked = true;
|
||||
}
|
||||
else if (ic is Holdable holdable && holdable.Attached)
|
||||
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
|
||||
{
|
||||
//prevent deattaching items from walls
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TutorialMode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
|
||||
#endif
|
||||
holdable.CanBePicked = false;
|
||||
holdable.CanBeSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (me is Structure structure)
|
||||
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts)
|
||||
{
|
||||
structure.Indestructible = true;
|
||||
structure.Indestructible = GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1178,7 +1288,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (Hull hull in matchingHulls)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hull.RoomName) || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hull.RoomName = hull.CreateRoomName();
|
||||
}
|
||||
@@ -1189,7 +1299,10 @@ namespace Barotrauma
|
||||
#endif
|
||||
//if the sub was made using an older version,
|
||||
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
|
||||
if (showWarningMessages && Screen.Selected != GameMain.SubEditorScreen && (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
|
||||
if (showWarningMessages &&
|
||||
!string.IsNullOrEmpty(Info.FilePath) &&
|
||||
Screen.Selected != GameMain.SubEditorScreen &&
|
||||
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
|
||||
{
|
||||
DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. "
|
||||
+ "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion.");
|
||||
@@ -1207,17 +1320,38 @@ namespace Barotrauma
|
||||
|
||||
public static Submarine Load(SubmarineInfo info, bool unloadPrevious)
|
||||
{
|
||||
if (unloadPrevious) Unload();
|
||||
if (unloadPrevious) { Unload(); }
|
||||
|
||||
Submarine sub = new Submarine(info, false);
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
public static void RepositionEntities(Vector2 moveAmount, IEnumerable<MapEntity> entities)
|
||||
{
|
||||
if (moveAmount.LengthSquared() < 0.00001f) { return; }
|
||||
foreach (MapEntity entity in entities)
|
||||
{
|
||||
if (entity is Item item)
|
||||
{
|
||||
item.GetComponent<Wire>()?.MoveNodes(moveAmount);
|
||||
}
|
||||
entity.Move(moveAmount);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveToXElement(XElement element)
|
||||
{
|
||||
element.Add(new XAttribute("name", Info.Name));
|
||||
element.Add(new XAttribute("description", Info.Description ?? ""));
|
||||
element.Add(new XAttribute("checkval", Rand.Int(int.MaxValue)));
|
||||
element.Add(new XAttribute("price", Info.Price));
|
||||
element.Add(new XAttribute("initialsuppliesspawned", Info.InitialSuppliesSpawned));
|
||||
element.Add(new XAttribute("type", Info.Type.ToString()));
|
||||
if (Info.IsPlayer && !Info.HasTag(SubmarineTag.Shuttle))
|
||||
{
|
||||
element.Add(new XAttribute("class", Info.SubmarineClass.ToString()));
|
||||
}
|
||||
element.Add(new XAttribute("tags", Info.Tags.ToString()));
|
||||
element.Add(new XAttribute("gameversion", GameMain.Version.ToString()));
|
||||
|
||||
@@ -1228,6 +1362,11 @@ namespace Barotrauma
|
||||
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? ""));
|
||||
element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages)));
|
||||
|
||||
if (Info.Type == SubmarineType.OutpostModule)
|
||||
{
|
||||
Info.OutpostModuleInfo?.Save(element);
|
||||
}
|
||||
|
||||
foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID))
|
||||
{
|
||||
if (e.Submarine != this || !e.ShouldBeSaved) continue;
|
||||
@@ -1242,12 +1381,12 @@ namespace Barotrauma
|
||||
{
|
||||
var newInfo = new SubmarineInfo(this)
|
||||
{
|
||||
GameVersion = GameMain.Version,
|
||||
Type = Info.Type,
|
||||
FilePath = filePath,
|
||||
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
|
||||
Name = Path.GetFileNameWithoutExtension(filePath)
|
||||
};
|
||||
Info.Dispose(); Info = newInfo;
|
||||
|
||||
return newInfo.SaveAs(filePath, previewImage);
|
||||
}
|
||||
|
||||
@@ -1312,6 +1451,7 @@ namespace Barotrauma
|
||||
{
|
||||
base.Remove();
|
||||
|
||||
subBody?.Remove();
|
||||
subBody = null;
|
||||
|
||||
if (entityGrid != null)
|
||||
|
||||
@@ -453,7 +453,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Position.Y > DamageDepth) { return; }
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession.GameMode is SubTestMode) { return; }
|
||||
if (GameMain.GameSession.GameMode is TestGameMode) { return; }
|
||||
#endif
|
||||
float depth = DamageDepth - Position.Y;
|
||||
|
||||
@@ -657,6 +657,13 @@ namespace Barotrauma
|
||||
|
||||
private void HandleLevelCollision(Impact impact)
|
||||
{
|
||||
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10)
|
||||
{
|
||||
//ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall
|
||||
//(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up)
|
||||
return;
|
||||
}
|
||||
|
||||
float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal);
|
||||
|
||||
ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
|
||||
@@ -787,7 +794,7 @@ namespace Barotrauma
|
||||
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = impact * 2.0f;
|
||||
if (submarine.Info.Type == SubmarineInfo.SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineInfo.SubmarineType.Player))
|
||||
if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player))
|
||||
{
|
||||
float angularVelocity =
|
||||
(impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y
|
||||
@@ -860,5 +867,9 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
Body.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace Barotrauma
|
||||
HideInMenus = 2
|
||||
}
|
||||
|
||||
public enum SubmarineType { Player, Outpost, OutpostModule, Wreck }
|
||||
public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver }
|
||||
|
||||
partial class SubmarineInfo : IDisposable
|
||||
{
|
||||
public const string SavePath = "Submarines";
|
||||
@@ -39,6 +42,11 @@ namespace Barotrauma
|
||||
public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2;
|
||||
public string RecommendedCrewExperience;
|
||||
|
||||
/// <summary>
|
||||
/// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match
|
||||
/// </summary>
|
||||
public int EqualityCheckVal { get; private set; }
|
||||
|
||||
public HashSet<string> RequiredContentPackages = new HashSet<string>();
|
||||
|
||||
public string Name
|
||||
@@ -59,19 +67,36 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
public int Price
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool InitialSuppliesSpawned
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Version GameVersion
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public SubmarineType Type { get; set; }
|
||||
|
||||
public SubmarineClass SubmarineClass;
|
||||
|
||||
public OutpostModuleInfo OutpostModuleInfo { get; set; }
|
||||
|
||||
public bool IsOutpost => Type == SubmarineType.Outpost;
|
||||
public bool IsWreck => Type == SubmarineType.Wreck;
|
||||
|
||||
public bool IsPlayer => Type == SubmarineType.Player;
|
||||
|
||||
public enum SubmarineType { Player, Outpost, Wreck }
|
||||
public SubmarineType Type { get; set; }
|
||||
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
|
||||
public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);
|
||||
|
||||
public Md5Hash MD5Hash
|
||||
{
|
||||
@@ -142,12 +167,19 @@ namespace Barotrauma
|
||||
return subsLeftBehind.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<ushort> LeftBehindDockingPortIDs = new List<ushort>();
|
||||
public readonly List<ushort> BlockedDockingPortIDs = new List<ushort>();
|
||||
|
||||
public bool LeftBehindSubDockingPortOccupied
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public OutpostGenerationParams OutpostGenerationParams;
|
||||
|
||||
public readonly Dictionary<string, List<Character>> OutpostNPCs = new Dictionary<string, List<Character>>();
|
||||
|
||||
//constructors & generation ----------------------------------------------------
|
||||
public SubmarineInfo()
|
||||
{
|
||||
@@ -209,18 +241,26 @@ namespace Barotrauma
|
||||
Name = original.Name;
|
||||
DisplayName = original.DisplayName;
|
||||
Description = original.Description;
|
||||
Price = original.Price;
|
||||
InitialSuppliesSpawned = original.InitialSuppliesSpawned;
|
||||
GameVersion = original.GameVersion;
|
||||
Type = original.Type;
|
||||
SubmarineClass = original.SubmarineClass;
|
||||
hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null;
|
||||
Dimensions = original.Dimensions;
|
||||
FilePath = original.FilePath;
|
||||
RequiredContentPackages = new HashSet<string>(original.RequiredContentPackages);
|
||||
IsFileCorrupted = original.IsFileCorrupted;
|
||||
SubmarineElement = original.SubmarineElement;
|
||||
EqualityCheckVal = original.EqualityCheckVal;
|
||||
RecommendedCrewExperience = original.RecommendedCrewExperience;
|
||||
RecommendedCrewSizeMin = original.RecommendedCrewSizeMin;
|
||||
RecommendedCrewSizeMax = original.RecommendedCrewSizeMax;
|
||||
Tags = original.Tags;
|
||||
if (original.OutpostModuleInfo != null)
|
||||
{
|
||||
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
|
||||
}
|
||||
#if CLIENT
|
||||
PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage.Texture, null, null) : null;
|
||||
#endif
|
||||
@@ -258,6 +298,12 @@ namespace Barotrauma
|
||||
Description = TextManager.Get("Submarine.Description." + Name, true);
|
||||
if (string.IsNullOrEmpty(Description)) { Description = SubmarineElement.GetAttributeString("description", ""); }
|
||||
|
||||
EqualityCheckVal = SubmarineElement.GetAttributeInt("checkval", 0);
|
||||
|
||||
Price = SubmarineElement.GetAttributeInt("price", 1000);
|
||||
|
||||
InitialSuppliesSpawned = SubmarineElement.GetAttributeBool("initialsuppliesspawned", false);
|
||||
|
||||
GameVersion = new Version(SubmarineElement.GetAttributeString("gameversion", "0.0.0.0"));
|
||||
if (Enum.TryParse(SubmarineElement.GetAttributeString("tags", ""), out SubmarineTag tags))
|
||||
{
|
||||
@@ -268,6 +314,33 @@ namespace Barotrauma
|
||||
RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0);
|
||||
RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown");
|
||||
|
||||
if (SubmarineElement?.Attribute("type") != null)
|
||||
{
|
||||
if (Enum.TryParse(SubmarineElement.GetAttributeString("type", ""), out SubmarineType type))
|
||||
{
|
||||
Type = type;
|
||||
if (Type == SubmarineType.OutpostModule)
|
||||
{
|
||||
OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Type == SubmarineType.Player)
|
||||
{
|
||||
if (SubmarineElement?.Attribute("class") != null)
|
||||
{
|
||||
if (Enum.TryParse(SubmarineElement.GetAttributeString("class", "Undefined"), out SubmarineClass submarineClass))
|
||||
{
|
||||
SubmarineClass = submarineClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SubmarineClass = SubmarineClass.Undefined;
|
||||
}
|
||||
|
||||
//backwards compatibility (use text tags instead of the actual text)
|
||||
if (RecommendedCrewExperience == "Beginner")
|
||||
{
|
||||
@@ -304,7 +377,10 @@ namespace Barotrauma
|
||||
var vanilla = GameMain.VanillaContent;
|
||||
if (vanilla != null)
|
||||
{
|
||||
var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine);
|
||||
var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine)
|
||||
.Concat(vanilla.GetFilesOfType(ContentType.Wreck))
|
||||
.Concat(vanilla.GetFilesOfType(ContentType.Outpost))
|
||||
.Concat(vanilla.GetFilesOfType(ContentType.OutpostModule));
|
||||
string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant();
|
||||
if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare))
|
||||
{
|
||||
@@ -351,6 +427,8 @@ namespace Barotrauma
|
||||
|
||||
subsLeftBehind = false;
|
||||
LeftBehindSubDockingPortOccupied = false;
|
||||
LeftBehindDockingPortIDs.Clear();
|
||||
BlockedDockingPortIDs.Clear();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
@@ -358,10 +436,12 @@ namespace Barotrauma
|
||||
|
||||
subsLeftBehind = true;
|
||||
ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0);
|
||||
LeftBehindDockingPortIDs.Add(targetDockingPortID);
|
||||
XElement targetPortElement = targetDockingPortID == 0 ? null :
|
||||
element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID);
|
||||
if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0)
|
||||
{
|
||||
BlockedDockingPortIDs.Add(targetDockingPortID);
|
||||
LeftBehindSubDockingPortOccupied = true;
|
||||
}
|
||||
}
|
||||
@@ -369,12 +449,17 @@ namespace Barotrauma
|
||||
|
||||
|
||||
//saving/loading ----------------------------------------------------
|
||||
public bool SaveAs(string filePath, System.IO.MemoryStream previewImage=null)
|
||||
public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null)
|
||||
{
|
||||
var newElement = new XElement(SubmarineElement.Name,
|
||||
SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
!string.Equals(a.Name.LocalName, "name", StringComparison.InvariantCultureIgnoreCase)),
|
||||
SubmarineElement.Elements());
|
||||
if (Type == SubmarineType.OutpostModule)
|
||||
{
|
||||
OutpostModuleInfo.Save(newElement);
|
||||
OutpostModuleInfo = new OutpostModuleInfo(this, newElement);
|
||||
}
|
||||
XDocument doc = new XDocument(newElement);
|
||||
|
||||
doc.Root.Add(new XAttribute("name", Name));
|
||||
@@ -383,10 +468,10 @@ namespace Barotrauma
|
||||
{
|
||||
doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray())));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SaveUtil.CompressStringToFile(filePath, doc.ToString());
|
||||
Md5Hash.RemoveFromCache(filePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -426,7 +511,9 @@ namespace Barotrauma
|
||||
|
||||
public static void RefreshSavedSubs()
|
||||
{
|
||||
var contentPackageSubs = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Submarine);
|
||||
var contentPackageSubs = ContentPackage.GetFilesOfType(
|
||||
GameMain.Config.SelectedContentPackages,
|
||||
ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, ContentType.Wreck);
|
||||
|
||||
for (int i = savedSubmarines.Count - 1; i >= 0; i--)
|
||||
{
|
||||
|
||||
@@ -18,28 +18,23 @@ namespace Barotrauma
|
||||
|
||||
public static bool ShowWayPoints = true, ShowSpawnPoints = true;
|
||||
|
||||
public const float LadderWaypointInterval = 100.0f;
|
||||
|
||||
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;
|
||||
|
||||
private List<string> tags;
|
||||
|
||||
public bool isObstructed;
|
||||
|
||||
private ushort gapId;
|
||||
public Gap ConnectedGap
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
set;
|
||||
}
|
||||
|
||||
public Door ConnectedDoor
|
||||
@@ -47,10 +42,7 @@ namespace Barotrauma
|
||||
get { return ConnectedGap?.ConnectedDoor; }
|
||||
}
|
||||
|
||||
public Hull CurrentHull
|
||||
{
|
||||
get { return currentHull; }
|
||||
}
|
||||
public Hull CurrentHull { get; private set; }
|
||||
|
||||
public SpawnType SpawnType
|
||||
{
|
||||
@@ -66,11 +58,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public string IdCardDesc
|
||||
{
|
||||
get { return idCardDesc; }
|
||||
private set { idCardDesc = value; }
|
||||
}
|
||||
public string IdCardDesc { get; private set; }
|
||||
public string[] IdCardTags
|
||||
{
|
||||
get { return idCardTags; }
|
||||
@@ -84,11 +72,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public JobPrefab AssignedJob
|
||||
public IEnumerable<string> Tags
|
||||
{
|
||||
get { return assignedJob; }
|
||||
get { return tags; }
|
||||
}
|
||||
|
||||
public JobPrefab AssignedJob { get; private set; }
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -120,6 +110,7 @@ namespace Barotrauma
|
||||
{
|
||||
rect = newRect;
|
||||
idCardTags = new string[0];
|
||||
tags = new List<string>();
|
||||
|
||||
#if CLIENT
|
||||
if (iconSprites == null)
|
||||
@@ -142,17 +133,18 @@ namespace Barotrauma
|
||||
|
||||
DebugConsole.Log("Created waypoint (" + ID + ")");
|
||||
|
||||
currentHull = Hull.FindHull(WorldPosition);
|
||||
CurrentHull = Hull.FindHull(WorldPosition);
|
||||
}
|
||||
|
||||
public override MapEntity Clone()
|
||||
{
|
||||
var clone = new WayPoint(rect, Submarine)
|
||||
{
|
||||
idCardDesc = idCardDesc,
|
||||
IdCardDesc = IdCardDesc,
|
||||
idCardTags = idCardTags,
|
||||
tags = tags,
|
||||
spawnType = spawnType,
|
||||
assignedJob = assignedJob
|
||||
AssignedJob = AssignedJob
|
||||
};
|
||||
|
||||
return clone;
|
||||
@@ -184,110 +176,106 @@ namespace Barotrauma
|
||||
door.Body.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
float diffFromHullEdge = 50;
|
||||
float minDist = 150.0f;
|
||||
float heightFromFloor = 110.0f;
|
||||
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Rect.Height < 150) continue;
|
||||
if (hull.Rect.Height < 150) { continue; }
|
||||
|
||||
WayPoint prevWaypoint = null;
|
||||
|
||||
if (hull.Rect.Width < minDist * 3.0f)
|
||||
if (hull.Rect.Width < diffFromHullEdge * 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)
|
||||
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; 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);
|
||||
|
||||
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)
|
||||
if (submarine.Info.Type != SubmarineType.OutpostModule)
|
||||
{
|
||||
borders.Inflate(outSideWaypointInterval*2 - borders.Width, 0);
|
||||
}
|
||||
int outsideWaypointDist = 100;
|
||||
|
||||
if (borders.Height <= outSideWaypointInterval * 2)
|
||||
{
|
||||
int inflateAmount = (int)(outSideWaypointInterval * 2) - borders.Height;
|
||||
borders.Y += inflateAmount / 2;
|
||||
Rectangle borders = Hull.GetBorders();
|
||||
borders.X -= outsideWaypointDist;
|
||||
borders.Y += outsideWaypointDist;
|
||||
borders.Width += outsideWaypointDist * 2;
|
||||
borders.Height += outsideWaypointDist * 2;
|
||||
borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
|
||||
|
||||
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)
|
||||
if (borders.Width <= outSideWaypointInterval * 2)
|
||||
{
|
||||
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]);
|
||||
}
|
||||
borders.Inflate(outSideWaypointInterval * 2 - borders.Width, 0);
|
||||
}
|
||||
|
||||
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)
|
||||
if (borders.Height <= outSideWaypointInterval * 2)
|
||||
{
|
||||
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]);
|
||||
}
|
||||
int inflateAmount = (int)(outSideWaypointInterval * 2) - borders.Height;
|
||||
borders.Y += inflateAmount / 2;
|
||||
borders.Height += inflateAmount;
|
||||
}
|
||||
|
||||
wayPoint.ConnectTo(cornerWaypoint[0, i]);
|
||||
}
|
||||
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 (!(me is Structure stairs)) { continue; }
|
||||
|
||||
if (stairs.StairDirection != Direction.None) stairList.Add(stairs);
|
||||
}
|
||||
@@ -322,16 +310,18 @@ namespace Barotrauma
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
var ladders = item.GetComponent<Ladder>();
|
||||
if (ladders == null) continue;
|
||||
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));
|
||||
List<WayPoint> ladderPoints = new List<WayPoint>
|
||||
{
|
||||
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)
|
||||
for (float y = ladderPoints[0].Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
|
||||
{
|
||||
//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)
|
||||
@@ -349,7 +339,8 @@ namespace Barotrauma
|
||||
{
|
||||
//no door, check for walls
|
||||
pickedBody = Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false);
|
||||
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false,
|
||||
(Fixture f) => f.Body.UserData is Structure);
|
||||
}
|
||||
|
||||
if (pickedBody == null)
|
||||
@@ -362,7 +353,6 @@ namespace Barotrauma
|
||||
ignoredBodies.Add(pickedBody);
|
||||
}
|
||||
|
||||
|
||||
if (pickedDoor != null)
|
||||
{
|
||||
WayPoint newPoint = new WayPoint(pickedDoor.Item.Position, SpawnType.Path, submarine);
|
||||
@@ -398,7 +388,7 @@ namespace Barotrauma
|
||||
|
||||
for (int dir = -1; dir <= 1; dir += 2)
|
||||
{
|
||||
WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 10f));
|
||||
WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 50f));
|
||||
if (closest == null) continue;
|
||||
ladderPoint.ConnectTo(closest);
|
||||
}
|
||||
@@ -472,39 +462,40 @@ namespace Barotrauma
|
||||
|
||||
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null)
|
||||
{
|
||||
if (dir != -1 && dir != 1) return 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;
|
||||
if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
|
||||
|
||||
float dist = 0.0f;
|
||||
float diff = 0.0f;
|
||||
if (horizontalSearch)
|
||||
{
|
||||
if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) continue;
|
||||
|
||||
if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) { continue; }
|
||||
diff = wp.Position.X - Position.X;
|
||||
dist = Math.Abs(diff) + Math.Abs(wp.Position.Y - Position.Y) / 5.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) continue;
|
||||
|
||||
if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) { continue; }
|
||||
diff = wp.Position.Y - Position.Y;
|
||||
dist = Math.Abs(diff) + Math.Abs(wp.Position.X - Position.X) / 5.0f;
|
||||
//prefer ladder waypoints when moving vertically
|
||||
if (wp.Ladders != null) { dist *= 0.5f; }
|
||||
}
|
||||
|
||||
if (Math.Sign(diff) != dir) continue;
|
||||
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;
|
||||
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
|
||||
}
|
||||
|
||||
closestDist = dist;
|
||||
@@ -515,12 +506,12 @@ namespace Barotrauma
|
||||
return closest;
|
||||
}
|
||||
|
||||
private void ConnectTo(WayPoint wayPoint2)
|
||||
public 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);
|
||||
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, Ruin ruin = null, bool useSyncedRand = false)
|
||||
@@ -529,7 +520,7 @@ namespace Barotrauma
|
||||
wp.Submarine == sub &&
|
||||
wp.ParentRuin == ruin &&
|
||||
wp.spawnType == spawnType &&
|
||||
(assignedJob == null || (assignedJob != null && wp.assignedJob == assignedJob.Prefab))
|
||||
(assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob.Prefab))
|
||||
, useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
@@ -546,7 +537,7 @@ namespace Barotrauma
|
||||
//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;
|
||||
if (crew[i].Job.Prefab != unassignedWayPoints[n].AssignedJob) continue;
|
||||
assignedWayPoints[i] = unassignedWayPoints[n];
|
||||
unassignedWayPoints.RemoveAt(n);
|
||||
|
||||
@@ -562,7 +553,7 @@ namespace Barotrauma
|
||||
//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;
|
||||
if (wp.spawnType != SpawnType.Human || wp.AssignedJob != crew[i].Job.Prefab) continue;
|
||||
|
||||
assignedWayPoints[i] = wp;
|
||||
break;
|
||||
@@ -570,7 +561,7 @@ namespace Barotrauma
|
||||
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);
|
||||
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)];
|
||||
@@ -596,21 +587,19 @@ namespace Barotrauma
|
||||
|
||||
public void FindHull()
|
||||
{
|
||||
currentHull = Hull.FindHull(WorldPosition, CurrentHull);
|
||||
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
InitializeLinks();
|
||||
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
|
||||
FindStairs();
|
||||
}
|
||||
|
||||
private void FindStairs()
|
||||
{
|
||||
Stairs = null;
|
||||
Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs);
|
||||
if (pickedBody != null && pickedBody.UserData is Structure)
|
||||
{
|
||||
@@ -622,6 +611,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializeLinks()
|
||||
{
|
||||
if (gapId > 0)
|
||||
{
|
||||
ConnectedGap = FindEntityByID(gapId) as Gap;
|
||||
gapId = 0;
|
||||
}
|
||||
if (ladderId > 0)
|
||||
{
|
||||
Item ladderItem = FindEntityByID(ladderId) as Item;
|
||||
if (ladderItem != null) { Ladders = ladderItem.GetComponent<Ladder>(); }
|
||||
ladderId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static WayPoint Load(XElement element, Submarine submarine)
|
||||
{
|
||||
Rectangle rect = new Rectangle(
|
||||
@@ -635,7 +639,7 @@ namespace Barotrauma
|
||||
{
|
||||
ID = (ushort)int.Parse(element.Attribute("ID").Value)
|
||||
};
|
||||
|
||||
w.OriginalID = w.ID;
|
||||
w.spawnType = spawnType;
|
||||
|
||||
string idCardDescString = element.GetAttributeString("idcarddesc", "");
|
||||
@@ -649,10 +653,12 @@ namespace Barotrauma
|
||||
w.IdCardTags = idCardTagString.Split(',');
|
||||
}
|
||||
|
||||
w.tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true).ToList();
|
||||
|
||||
string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(jobIdentifier))
|
||||
{
|
||||
w.assignedJob =
|
||||
w.AssignedJob =
|
||||
JobPrefab.Get(jobIdentifier) ??
|
||||
JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
@@ -680,13 +686,17 @@ namespace Barotrauma
|
||||
new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)),
|
||||
new XAttribute("spawn", spawnType));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(idCardDesc)) element.Add(new XAttribute("idcarddesc", idCardDesc));
|
||||
if (!string.IsNullOrWhiteSpace(IdCardDesc)) element.Add(new XAttribute("idcarddesc", IdCardDesc));
|
||||
if (idCardTags.Length > 0)
|
||||
{
|
||||
element.Add(new XAttribute("idcardtags", string.Join(",", idCardTags)));
|
||||
}
|
||||
if (tags.Count > 0)
|
||||
{
|
||||
element.Add(new XAttribute("tags", string.Join(",", tags)));
|
||||
}
|
||||
|
||||
if (assignedJob != null) element.Add(new XAttribute("job", assignedJob.Identifier));
|
||||
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));
|
||||
|
||||
@@ -697,7 +707,8 @@ namespace Barotrauma
|
||||
int i = 0;
|
||||
foreach (MapEntity e in linkedTo)
|
||||
{
|
||||
if (!e.ShouldBeSaved) continue;
|
||||
if (!e.ShouldBeSaved || e.Removed) { continue; }
|
||||
if (e.Submarine?.Info.Type != Submarine?.Info.Type) { continue; }
|
||||
element.Add(new XAttribute("linkedto" + i, e.ID));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user