#nullable enable using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Particles { class ParticleEmitterProperties : ISerializableEntity { private const float MinValue = int.MinValue, MaxValue = int.MaxValue; public string Name => nameof(ParticleEmitter); private float angleMin, angleMax; public float AngleMinRad { get; private set; } public float AngleMaxRad { get; private set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)] public float AngleMin { get => angleMin; set { angleMin = value; AngleMinRad = MathHelper.ToRadians(MathHelper.Clamp(value, -360.0f, 360.0f)); } } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)] public float AngleMax { get => angleMax; set { angleMax = value; AngleMaxRad = MathHelper.ToRadians(MathHelper.Clamp(value, -360.0f, 360.0f)); } } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float DistanceMin { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float DistanceMax { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float VelocityMin { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float VelocityMax { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)] public float ScaleMin { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)] public float ScaleMax { get; set; } [Editable(), Serialize("1,1", IsPropertySaveable.Yes)] public Vector2 ScaleMultiplier { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)] public float EmitInterval { get; set; } [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, IsPropertySaveable.Yes, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] public int ParticleAmount { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)] public float ParticlesPerSecond { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] public float EmitAcrossRayInterval { get; set; } [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "Delay before the emitter becomes active after being created.")] public float InitialDelay { get; set; } [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool HighQualityCollisionDetection { get; set; } [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool CopyEntityAngle { get; set; } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")] public bool CopyEntityDir { get; set; } [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")] public bool CopyTargetAngle { get; set; } [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for particles spawned by another particle. Makes the emitter copy the scale of the parent particle.")] public bool CopyParentParticleScale { get; set; } [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } [Editable, Serialize(1f, IsPropertySaveable.Yes)] public float LifeTimeMultiplier { get; set; } [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DrawOnTop { get; set; } [Serialize(0f, IsPropertySaveable.Yes)] public float Angle { get => AngleMin; set => AngleMin = AngleMax = value; } [Serialize(0f, IsPropertySaveable.Yes)] public float Distance { get => DistanceMin; set => DistanceMin = DistanceMax = value; } [Serialize(0f, IsPropertySaveable.Yes)] public float Velocity { get => VelocityMin; set => VelocityMin = VelocityMax = value; } public Dictionary SerializableProperties { get; } public ParticleEmitterProperties(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } } class ParticleEmitter { private float emitTimer; private float burstEmitTimer; private float initialDelay; public readonly ParticleEmitterPrefab Prefab; public ParticleEmitter(ContentXElement element) { Prefab = new ParticleEmitterPrefab(element); } public ParticleEmitter(ParticleEmitterPrefab prefab) { System.Diagnostics.Debug.Assert(prefab != null, "The prefab of a particle emitter cannot be null"); Prefab = prefab; } public void Emit(float deltaTime, Vector2 position, Hull? hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple? tracerPoints = null) { if (GameMain.Client?.MidRoundSyncing ?? false) { return; } if (initialDelay < Prefab.Properties.InitialDelay) { initialDelay += deltaTime; return; } emitTimer += deltaTime * amountMultiplier; burstEmitTimer -= deltaTime; if (Prefab.Properties.EmitAcrossRayInterval > 0.0f && tracerPoints != null) { Vector2 dir = tracerPoints.Item2 - tracerPoints.Item1; if (dir.LengthSquared() > 0.001f) { float dist = dir.Length(); dir /= dist; for (float z = 0.0f; z < dist; z += Prefab.Properties.EmitAcrossRayInterval) { Vector2 pos = tracerPoints.Item1 + dir * z; Emit(pos, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: null); } } } if (Prefab.Properties.ParticlesPerSecond > 0) { float emitInterval = 1.0f / Prefab.Properties.ParticlesPerSecond; while (emitTimer > emitInterval) { Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints); emitTimer -= emitInterval; } } if (burstEmitTimer > 0.0f) { return; } burstEmitTimer = Prefab.Properties.EmitInterval; for (int i = 0; i < Prefab.Properties.ParticleAmount * amountMultiplier; i++) { Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints); } } private void Emit(Vector2 position, Hull? hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple? tracerPoints = null) { var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; if (particlePrefab == null) { DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\".", contentPackage: Prefab.ContentPackage); return; } Vector2 velocity = Vector2.Zero; if (!MathUtils.NearlyEqual(Prefab.Properties.VelocityMax * velocityMultiplier, 0.0f) || !MathUtils.NearlyEqual(Prefab.Properties.DistanceMax, 0.0f)) { angle += Rand.Range(Prefab.Properties.AngleMinRad, Prefab.Properties.AngleMaxRad) * (mirrorAngle ? -1 : 1); Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); velocity = dir * Rand.Range(Prefab.Properties.VelocityMin, Prefab.Properties.VelocityMax) * velocityMultiplier; position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, particlePrefab.DrawOnTop || Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { particle.Size *= Rand.Range(Prefab.Properties.ScaleMin, Prefab.Properties.ScaleMax) * sizeMultiplier; particle.Size *= Prefab.Properties.ScaleMultiplier; particle.HighQualityCollisionDetection = Prefab.Properties.HighQualityCollisionDetection; if (colorMultiplier.HasValue) { particle.ColorMultiplier = colorMultiplier.Value.ToVector4(); } else if (Prefab.Properties.ColorMultiplier != Color.White) { particle.ColorMultiplier = Prefab.Properties.ColorMultiplier.ToVector4(); } } } public Rectangle CalculateParticleBounds(Vector2 startPosition) { Rectangle bounds = new Rectangle((int)startPosition.X, (int)startPosition.Y, (int)startPosition.X, (int)startPosition.Y); if (Prefab.ParticlePrefab == null) { return bounds; } for (float angle = Prefab.Properties.AngleMinRad; angle <= Prefab.Properties.AngleMaxRad; angle += 0.1f) { Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Prefab.Properties.VelocityMax; Vector2 endPosition = Prefab.ParticlePrefab.CalculateEndPosition(startPosition, velocity); Vector2 endSize = Prefab.ParticlePrefab.CalculateEndSize(); float spriteExtent = 0.0f; foreach (Sprite sprite in Prefab.ParticlePrefab.Sprites) { if (sprite is SpriteSheet spriteSheet) { spriteExtent = Math.Max(spriteExtent, Math.Max(spriteSheet.FrameSize.X * endSize.X, spriteSheet.FrameSize.Y * endSize.Y)); } else { spriteExtent = Math.Max(spriteExtent, Math.Max(sprite.size.X * endSize.X, sprite.size.Y * endSize.Y)); } } bounds = new Rectangle( (int)Math.Min(bounds.X, endPosition.X - Prefab.Properties.DistanceMax - spriteExtent / 2), (int)Math.Min(bounds.Y, endPosition.Y - Prefab.Properties.DistanceMax - spriteExtent / 2), (int)Math.Max(bounds.X, endPosition.X + Prefab.Properties.DistanceMax + spriteExtent / 2), (int)Math.Max(bounds.Y, endPosition.Y + Prefab.Properties.DistanceMax + spriteExtent / 2)); } bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y); return bounds; } } class ParticleEmitterPrefab { public readonly Identifier ParticlePrefabName; public ParticlePrefab? ParticlePrefab { get { ParticlePrefab.Prefabs.TryGet(ParticlePrefabName, out var prefab); return prefab; } } public readonly ParticleEmitterProperties Properties; public readonly ContentPackage? ContentPackage; public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab is { DrawOnTop: true }; public ParticleEmitterPrefab(ContentXElement element) { if (element == null) { throw new ArgumentNullException(nameof(element)); } Properties = new ParticleEmitterProperties(element!); ParticlePrefabName = element.GetAttributeIdentifier("particle", ""); ContentPackage = element.ContentPackage; } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) { Properties = properties; ParticlePrefabName = prefab.Identifier; } } }