using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { partial class LevelObjectPrefab : PrefabWithUintIdentifier, ISerializableEntity { public readonly static PrefabCollection Prefabs = new PrefabCollection(); public class ChildObject { public List AllowedNames; public int MinCount, MaxCount; public ChildObject() { AllowedNames = new List(); MinCount = 1; MaxCount = 1; } public ChildObject(XElement element) { AllowedNames = element.GetAttributeStringArray("names", Array.Empty()).ToList(); MinCount = element.GetAttributeInt("mincount", 1); MaxCount = Math.Max(element.GetAttributeInt("maxcount", 1), MinCount); } } [Flags] public enum SpawnPosType { None = 0, MainPathWall = 1, SidePathWall = 2, CaveWall = 4, NestWall = 8, RuinWall = 16, SeaFloor = 32, MainPath = 64, LevelStart = 128, LevelEnd = 256, OutpostWall = 512, Wall = MainPathWall | SidePathWall | CaveWall, } public List Sprites { get; private set; } = new List(); public DeformableSprite DeformableSprite { get; private set; } [Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public float MinSize { get; private set; } [Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public float MaxSize { get; private set; } /// /// Which sides of a wall the object can appear on. /// [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), IsPropertySaveable.Yes, description: "Which sides of a wall the object can spawn on."), Editable] public Alignment Alignment { get; private set; } [Serialize(SpawnPosType.Wall, IsPropertySaveable.No), Editable()] public SpawnPosType SpawnPos { get; private set; } public XElement Config { get; private set; } public readonly List LevelTriggerElements; /// /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// public readonly Dictionary OverrideCommonness; public XElement PhysicsBodyElement { get; private set; } public int PhysicsBodyTriggerIndex { get; private set; } = -1; public Dictionary SpriteSpecificPhysicsBodyElements { get; private set; } = new Dictionary(); [Serialize(10000, IsPropertySaveable.No, 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", IsPropertySaveable.Yes, description: "The sprite depth of the object (min, max). Values of 0 or less make the object render in front of walls, values larger than 0 make it render behind walls with a parallax effect."), Editable] public Vector2 DepthRange { get; private set; } [Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f), Serialize(0.0f, IsPropertySaveable.Yes, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")] /// /// The tendency for the prefab to form clusters. Used as an exponent for perlin noise values /// that are used to determine the probability for an object to spawn at a specific position. /// public float ClusteringAmount { get; private set; } [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.0f, IsPropertySaveable.Yes, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " + " for an object to spawn at a specific position. Using the same (or close) value for different objects means the objects tend " + "to form clusters in the same areas.")] /// /// A value between 0-1 that determines the z-coordinate to sample perlin noise from when /// determining the probability for an object to spawn at a specific position. /// Using the same (or close) value for different objects means the objects tend to form clusters /// in the same areas. /// public float ClusteringGroup { get; private set; } [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "Random offset from the surface the object spawns on.")] public Vector2 RandomOffset { get; private set; } [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the object be rotated to align it with the wall surface it spawns on.")] public bool AlignWithSurface { get; private set; } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the start of the level.")] public bool AllowAtStart { get; private set; } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the end of the level.")] public bool AllowAtEnd { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] /// /// Minimum length of a graph edge the object can spawn on. /// public float MinSurfaceWidth { get; private set; } private Vector2 randomRotation; [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the rotation of the object can vary (min and max values in degrees).")] public Vector2 RandomRotation { get { return new Vector2(MathHelper.ToDegrees(randomRotation.X), MathHelper.ToDegrees(randomRotation.Y)); } private set { randomRotation = new Vector2(MathHelper.ToRadians(value.X), MathHelper.ToRadians(value.Y)); } } public Vector2 RandomRotationRad => randomRotation; private float swingAmount; [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)] public float SwingAmount { get { return MathHelper.ToDegrees(swingAmount); } private set { swingAmount = MathHelper.ToRadians(value); } } public float SwingAmountRad => swingAmount; [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SwingFrequency { get; private set; } [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")] public Vector2 ScaleOscillation { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ScaleOscillationFrequency { get; private set; } [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely it is for the object to spawn in a level. " + "This is relative to the commonness of the other objects - for example, having an object with " + "a commonness of 1 and another with a commonness of 10 would mean the latter appears in levels 10 times as frequently as the former. " + "The commonness value can be overridden on specific level types.")] public float Commonness { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SonarDisruption { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable] public bool TakeLevelWallDamage { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should the object disappear if the object is destroyed? Only relevant if TakeLevelWallDamage is true."), Editable] public bool HideWhenBroken { get; private set; } [Serialize(100.0f, IsPropertySaveable.Yes, description: "Amount of health the object has. Only relevant if TakeLevelWallDamage is true."), Editable] public float Health { get; private set; } [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] public Color SpriteColor { get; private set; } public string Name => Identifier.Value; public List ChildObjects { get; private set; } public Dictionary SerializableProperties { get; private set; } /// /// A list of prefabs whose properties override this one's properties when a trigger is active. /// E.g. if a trigger in the index 1 of the trigger list is active, the properties in index 1 in this list are used (unless it's null) /// public List OverrideProperties { get; private set; } public override string ToString() { return "LevelObjectPrefab (" + Identifier + ")"; } public LevelObjectPrefab(ContentXElement element, LevelObjectPrefabsFile file, Identifier identifierOverride = default) : base(file, ParseIdentifier(identifierOverride, element)) { ChildObjects = new List(); LevelTriggerElements = new List(); OverrideProperties = new List(); OverrideCommonness = new Dictionary(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (element != null) { Config = element; LoadElements(file, element, -1); InitProjSpecific(element); } //use the maximum width of the sprite as the minimum surface width if no value is given if (element != null && !element.Attributes("minsurfacewidth").Any()) { if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize; if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize); } } public static Identifier ParseIdentifier(Identifier identifierOverride, XElement element) { if (!identifierOverride.IsEmpty) { return identifierOverride; } Identifier identifier = element.GetAttributeIdentifier("identifier", ""); if (identifier.IsEmpty) { #if DEBUG DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); #else DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); #endif identifier = element.NameAsIdentifier(); } return identifier; } private void LoadElements(LevelObjectPrefabsFile file, ContentXElement element, int parentTriggerIndex) { int propertyOverrideCount = 0; //load sprites first, OverrideProperties may need them (defaulting to the default sprite if no override is defined) foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": var newSprite = new Sprite(subElement, lazyLoad: true); Sprites.Add(newSprite); var spriteSpecificPhysicsBodyElement = subElement.GetChildElement("PhysicsBody") ?? subElement.GetChildElement("Body") ?? subElement.GetChildElement("physicsbody") ?? subElement.GetChildElement("body"); if (spriteSpecificPhysicsBodyElement != null) { SpriteSpecificPhysicsBodyElements.Add(newSprite, spriteSpecificPhysicsBodyElement); } break; case "deformablesprite": DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); break; } } foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "overridecommonness": Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); } break; case "leveltrigger": case "trigger": OverrideProperties.Add(null); LevelTriggerElements.Add(subElement); LoadElements(file, subElement, LevelTriggerElements.Count - 1); break; case "childobject": ChildObjects.Add(new ChildObject(subElement)); break; case "overrideproperties": var propertyOverride = new LevelObjectPrefab(subElement, file, identifierOverride: $"{Identifier}-{propertyOverrideCount}".ToIdentifier()); OverrideProperties[OverrideProperties.Count - 1] = propertyOverride; if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null) { propertyOverride.Sprites = Sprites; propertyOverride.DeformableSprite = DeformableSprite; } propertyOverrideCount++; break; case "body": case "physicsbody": PhysicsBodyElement = subElement; PhysicsBodyTriggerIndex = parentTriggerIndex; break; } } } partial void InitProjSpecific(ContentXElement element); public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true) { if (generationParams != null && generationParams.Identifier != Identifier.Empty && OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) { return commonness; } return requireCaveSpecificOverride ? 0.0f : Commonness; } public float GetCommonness(LevelData levelData) { if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty && OverrideCommonness.TryGetValue(levelData.GenerationParams.Identifier, out float commonness) || (!levelData.GenerationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(levelData.GenerationParams.OldIdentifier, out commonness))) { return commonness; } if (levelData?.Biome != null) { if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness)) { return biomeCommonness; } } return Commonness; } public override void Dispose() { } } }