using Barotrauma.SpriteDeformations; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma { class BackgroundCreature : ISteerable { const float MaxDepth = 10000.0f; const float CheckWallsInterval = 5.0f; public bool Visible; public readonly BackgroundCreaturePrefab Prefab; private readonly List uniqueSpriteDeformations = new List(); private readonly List spriteDeformations = new List(); private readonly List lightSpriteDeformations = new List(); private Vector2 position; private Vector3 velocity; public float Depth { get; private set; } private float alpha = 1.0f; private readonly SteeringManager steeringManager; private float checkWallsTimer, flashTimer; private float wanderZPhase; private Vector2 obstacleDiff; private float obstacleDist; public Swarm Swarm; Vector2 drawPosition; private bool flippedHorizontally; public Vector2[,] CurrentSpriteDeformation { get; private set; } public Vector2[,] CurrentLightSpriteDeformation { get; private set; } public Vector2 SimPosition { get { return FarseerPhysics.ConvertUnits.ToSimUnits(position); } } public Vector2 WorldPosition { get { return position; } } public Vector2 Velocity { get { return new Vector2(velocity.X, velocity.Y); } } public Vector2 Steering { get; set; } public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position) { this.Prefab = prefab; this.position = position; drawPosition = position; steeringManager = new SteeringManager(this); velocity = new Vector3( Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly), Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly), Rand.Range(0.0f, prefab.WanderZAmount, Rand.RandSync.ClientOnly)); Depth = Rand.Range(prefab.MinDepth, prefab.MaxDepth, Rand.RandSync.ClientOnly); checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval, Rand.RandSync.ClientOnly); foreach (var subElement in prefab.Config.Elements()) { List deformationList = null; switch (subElement.Name.ToString().ToLowerInvariant()) { case "deformablesprite": deformationList = spriteDeformations; break; case "deformablelightsprite": deformationList = lightSpriteDeformations; break; default: continue; } int j = 0; foreach (XElement animationElement in subElement.Elements()) { SpriteDeformation deformation = null; int sync = animationElement.GetAttributeInt("sync", -1); if (sync > -1) { string typeName = animationElement.GetAttributeString("type", "").ToLowerInvariant(); deformation = uniqueSpriteDeformations.Find(d => d.TypeName == typeName && d.Sync == sync); } if (deformation == null) { deformation = SpriteDeformation.Load(animationElement, prefab.Name); if (deformation != null) { deformation.Params = Prefab.SpriteDeformations[j].Params; uniqueSpriteDeformations.Add(deformation); if (prefab.DeformableSprite != null) { if (deformation.Resolution.X > prefab.DeformableSprite.Subdivisions.X || deformation.Resolution.Y > prefab.DeformableSprite.Subdivisions.Y) { DebugConsole.AddWarning( $"Potential error in background creature {Prefab.Identifier}: deformation {deformation.GetType()} has a larger resolution ({deformation.Resolution})"+ $" than the amount of subdivisions on the deformable sprite ({prefab.DeformableSprite.Subdivisions}). Should the sprite be subdivided further to make full use of the deformation?", contentPackage: Prefab.ContentPackage); } } j++; } } if (deformation != null) { deformationList.Add(deformation); } } } flashTimer = Rand.Range(0.0f, prefab.FlashInterval, Rand.RandSync.Unsynced); } public void Update(float deltaTime) { position += new Vector2(velocity.X, velocity.Y) * deltaTime; Depth = MathHelper.Clamp(Depth + velocity.Z * deltaTime, Prefab.MinDepth, Prefab.MaxDepth); if (Prefab.FlashInterval > 0.0f) { flashTimer -= deltaTime; if (flashTimer > 0.0f) { alpha = 0.0f; } else { //value goes from 0 to 1 and back to 0 during the flash alpha = (float)Math.Sin(-flashTimer / Prefab.FlashDuration * MathHelper.Pi) * PerlinNoise.GetPerlin((float)Timing.TotalTime, (float)Timing.TotalTime * 0.5f); if (flashTimer < -Prefab.FlashDuration) { flashTimer = Prefab.FlashInterval; } } } checkWallsTimer -= deltaTime; if (checkWallsTimer <= 0.0f && Level.Loaded != null) { checkWallsTimer = CheckWallsInterval; obstacleDiff = Vector2.Zero; if (position.Y > Level.Loaded.Size.Y) { obstacleDiff = Vector2.UnitY; } else if (position.Y < 0.0f) { obstacleDiff = -Vector2.UnitY; } else if (position.X < 0.0f) { obstacleDiff = -Vector2.UnitX; } else if (position.X > Level.Loaded.Size.X) { obstacleDiff = Vector2.UnitX; } else { var cells = Level.Loaded.GetCells(position, 1); if (cells.Count > 0) { int cellCount = 0; foreach (Voronoi2.VoronoiCell cell in cells) { Vector2 diff = cell.Center - position; if (diff.LengthSquared() > 5000.0f * 5000.0f) { continue; } obstacleDiff += diff; cellCount++; } if (cellCount > 0) { obstacleDiff /= cellCount; obstacleDist = obstacleDiff.Length(); obstacleDiff = Vector2.Normalize(obstacleDiff); } } } } if (Swarm != null) { Vector2 midPoint = Swarm.MidPoint(); float midPointDist = Vector2.Distance(SimPosition, midPoint) * 100.0f; if (midPointDist > Swarm.MaxDistance) { steeringManager.SteeringSeek(midPoint, ((midPointDist / Swarm.MaxDistance) - 1.0f) * Prefab.Speed); } steeringManager.SteeringManual(deltaTime, Swarm.AvgVelocity() * Swarm.Cohesion); } if (Prefab.WanderAmount > 0.0f) { steeringManager.SteeringWander(Prefab.Speed); } if (obstacleDiff != Vector2.Zero) { steeringManager.SteeringManual(deltaTime, -obstacleDiff * (1.0f - obstacleDist / 5000.0f) * Prefab.Speed); } steeringManager.Update(Prefab.Speed); if (Prefab.WanderZAmount > 0.0f) { wanderZPhase += Rand.Range(-Prefab.WanderZAmount, Prefab.WanderZAmount); velocity.Z = (float)Math.Sin(wanderZPhase) * Prefab.Speed; } velocity = Vector3.Lerp(velocity, new Vector3(Steering.X, Steering.Y, velocity.Z), deltaTime); //only flip if there's some horizontal movement speed (10% of the creature's speed) //otherwise a creature swimming roughly up/down can flip around very frequently when the horizontal speed fluctuates around 0 if (Math.Abs(velocity.X) > Prefab.Speed * 0.1f) { flippedHorizontally = !Prefab.DisableFlipping && velocity.X < 0.0f; } UpdateDeformations(deltaTime, flippedHorizontally); } public void DrawLightSprite(SpriteBatch spriteBatch, Camera cam) { Draw(spriteBatch, cam, Prefab.LightSprite, Prefab.DeformableLightSprite, CurrentLightSpriteDeformation, Color.White * alpha); } public void Draw(SpriteBatch spriteBatch, Camera cam) { Color color = Prefab.FadeOut ? Color.Lerp(Color.White, Level.Loaded.BackgroundColor, Depth / Prefab.FadeOutDepth) * alpha : Color.White * alpha; Draw(spriteBatch, cam, Prefab.Sprite, Prefab.DeformableSprite, CurrentSpriteDeformation, color); } private void Draw(SpriteBatch spriteBatch, Camera cam, Sprite sprite, DeformableSprite deformableSprite, Vector2[,] currentSpriteDeformation, Color color) { if (sprite == null && deformableSprite == null) { return; } if (color.A == 0) { return; } float rotation = 0.0f; if (!Prefab.DisableRotation) { rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y)); if (flippedHorizontally) { rotation -= MathHelper.Pi; } } drawPosition = GetDrawPosition(cam); float scale = GetScale(); sprite?.Draw(spriteBatch, new Vector2(drawPosition.X, -drawPosition.Y), color, rotation, scale, flippedHorizontally ? SpriteEffects.FlipHorizontally : SpriteEffects.None, Math.Min(Depth / MaxDepth, 1.0f)); if (deformableSprite != null) { if (currentSpriteDeformation != null) { deformableSprite.Deform(currentSpriteDeformation); } else { deformableSprite.Reset(); } deformableSprite?.Draw(cam, new Vector3(drawPosition.X, drawPosition.Y, Math.Min(Depth / 10000.0f, 1.0f)), deformableSprite.Origin, rotation, Vector2.One * scale, color, mirror: flippedHorizontally); } } public Vector2 GetDrawPosition(Camera cam) { Vector2 drawPosition = WorldPosition; if (Depth >= 0) { Vector2 camOffset = drawPosition - cam.WorldViewCenter; drawPosition -= camOffset * Depth / MaxDepth; } return drawPosition; } public float GetScale() { return Math.Max(1.0f - Depth / MaxDepth, 0.05f) * Prefab.Scale; } public Rectangle GetExtents(Camera cam) { Vector2 min = GetDrawPosition(cam); Vector2 max = min; float scale = GetScale(); GetSpriteExtents(Prefab.Sprite, ref min, ref max); GetSpriteExtents(Prefab.LightSprite, ref min, ref max); GetSpriteExtents(Prefab.DeformableSprite?.Sprite, ref min, ref max); GetSpriteExtents(Prefab.DeformableLightSprite?.Sprite, ref min, ref max); return new Rectangle(min.ToPoint(), (max - min).ToPoint()); void GetSpriteExtents(Sprite sprite, ref Vector2 min, ref Vector2 max) { if (sprite == null) { return; } min.X = Math.Min(min.X, min.X - sprite.size.X * sprite.RelativeOrigin.X * scale); min.Y = Math.Min(min.Y, min.Y - sprite.size.Y * sprite.RelativeOrigin.Y * scale); max.X = Math.Max(max.X, max.X + sprite.size.X * (1.0f - sprite.RelativeOrigin.X) * scale); max.Y = Math.Max(max.Y, max.Y + sprite.size.Y * (1.0f - sprite.RelativeOrigin.Y) * scale); } } private void UpdateDeformations(float deltaTime, bool flippedHorizontally) { foreach (SpriteDeformation deformation in uniqueSpriteDeformations) { deformation.Update(deltaTime); } if (spriteDeformations.Count > 0) { CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, Prefab.DeformableSprite.Size, flippedHorizontally: flippedHorizontally); } if (lightSpriteDeformations.Count > 0) { CurrentLightSpriteDeformation = SpriteDeformation.GetDeformation(lightSpriteDeformations, Prefab.DeformableLightSprite.Size, flippedHorizontally: flippedHorizontally); } } } class Swarm { public List Members; public readonly float MaxDistance; public readonly float Cohesion; public Vector2 MidPoint() { if (Members.Count == 0) return Vector2.Zero; Vector2 midPoint = Vector2.Zero; foreach (BackgroundCreature member in Members) { midPoint += member.SimPosition; } midPoint /= Members.Count; return midPoint; } public Vector2 AvgVelocity() { if (Members.Count == 0) return Vector2.Zero; Vector2 avgVel = Vector2.Zero; foreach (BackgroundCreature member in Members) { avgVel += member.Velocity; } avgVel /= Members.Count; return avgVel; } public Swarm(List members, float maxDistance, float cohesion) { Members = members; MaxDistance = maxDistance; Cohesion = cohesion; foreach (BackgroundCreature bgSprite in members) { bgSprite.Swarm = this; } } } }