#if CLIENT using Barotrauma.Particles; #endif using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Voronoi2; namespace Barotrauma { partial class BackgroundSprite { public readonly BackgroundSpritePrefab Prefab; public Vector3 Position; public float Scale; public float Rotation; public LevelTrigger Trigger; public BackgroundSprite(BackgroundSpritePrefab prefab, Vector3 position, float scale, float rotation = 0.0f) { this.Prefab = prefab; this.Position = position; this.Scale = scale; this.Rotation = rotation; if (prefab.LevelTriggerElement != null) { Vector2 triggerPosition = prefab.LevelTriggerElement.GetAttributeVector2("position", Vector2.Zero) * scale; if (rotation != 0.0f) { var ca = (float)Math.Cos(rotation); var sa = (float)Math.Sin(rotation); triggerPosition = new Vector2( ca * triggerPosition.X + sa * triggerPosition.Y, -sa * triggerPosition.X + ca * triggerPosition.Y); } this.Trigger = new LevelTrigger(prefab.LevelTriggerElement, new Vector2(position.X, position.Y) + triggerPosition, -rotation, scale); } #if CLIENT if (prefab.ParticleEmitterPrefabs != null) { ParticleEmitters = new List(); foreach (ParticleEmitterPrefab emitterPrefab in prefab.ParticleEmitterPrefabs) { ParticleEmitters.Add(new ParticleEmitter(emitterPrefab)); } } if (prefab.SoundElement != null) { Sound = Sound.Load(prefab.SoundElement, true); } #endif } public Vector2 LocalToWorld(Vector2 localPosition, float swingState = 0.0f) { Vector2 emitterPos = localPosition * Scale; if (Rotation != 0.0f || Prefab.SwingAmount != 0.0f) { float rot = Rotation + swingState * Prefab.SwingAmount; var ca = (float)Math.Cos(rot); var sa = (float)Math.Sin(rot); emitterPos = new Vector2( ca * emitterPos.X + sa * emitterPos.Y, -sa * emitterPos.X + ca * emitterPos.Y); } return new Vector2(Position.X, Position.Y) + emitterPos; } } partial class BackgroundSpriteManager { const int GridSize = 2000; private List prefabs = new List(); private List sprites; private List[,] spriteGrid; private float swingTimer, swingState; public BackgroundSpriteManager(string configPath) { LoadConfig(configPath); } public BackgroundSpriteManager(List files) { foreach (var file in files) { LoadConfig(file); } } private void LoadConfig(string configPath) { try { XDocument doc = XMLExtensions.TryLoadXml(configPath); if (doc == null || doc.Root == null) return; foreach (XElement element in doc.Root.Elements()) { prefabs.Add(new BackgroundSpritePrefab(element)); } } catch (Exception e) { DebugConsole.ThrowError(String.Format("Failed to load BackgroundSprites from {0}", configPath), e); } } public void PlaceSprites(Level level, int amount) { spriteGrid = new List[ (int)Math.Ceiling(level.Size.X / GridSize), (int)Math.Ceiling((level.Size.Y - level.BottomPos) / GridSize)]; sprites = new List(); for (int i = 0 ; i < amount; i++) { BackgroundSpritePrefab prefab = GetRandomPrefab(level.GenerationParams.Name); GraphEdge selectedEdge = null; Vector2 edgeNormal = Vector2.One; Vector2? pos = FindSpritePosition(level, prefab, out selectedEdge, out edgeNormal); if (pos == null) continue; float rotation = 0.0f; if (prefab.AlignWithSurface) { rotation = MathUtils.VectorToAngle(new Vector2(edgeNormal.Y, edgeNormal.X)); } rotation += Rand.Range(prefab.RandomRotation.X, prefab.RandomRotation.Y, Rand.RandSync.Server); var newSprite = new BackgroundSprite(prefab, new Vector3((Vector2)pos, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.Scale.X, prefab.Scale.Y, Rand.RandSync.Server), rotation); //calculate the positions of the corners of the rotated sprite Vector2 halfSize = newSprite.Prefab.Sprite.size * newSprite.Scale / 2; var spriteCorners = new List { -halfSize, new Vector2(-halfSize.X, halfSize.Y), halfSize, new Vector2(halfSize.X, -halfSize.Y) }; Vector2 pivotOffset = newSprite.Prefab.Sprite.Origin * newSprite.Scale - halfSize; pivotOffset.X = -pivotOffset.X; pivotOffset = new Vector2( (float)(pivotOffset.X * Math.Cos(-rotation) - pivotOffset.Y * Math.Sin(-rotation)), (float)(pivotOffset.X * Math.Sin(-rotation) + pivotOffset.Y * Math.Cos(-rotation))); for (int j = 0; j < 4; j++) { spriteCorners[j] = new Vector2( (float)(spriteCorners[j].X * Math.Cos(-rotation) - spriteCorners[j].Y * Math.Sin(-rotation)), (float)(spriteCorners[j].X * Math.Sin(-rotation) + spriteCorners[j].Y * Math.Cos(-rotation))); spriteCorners[j] += pos.Value + pivotOffset; } float minX = spriteCorners.Min(c => c.X) - newSprite.Position.Z; float maxX = spriteCorners.Max(c => c.X) + newSprite.Position.Z; float minY = spriteCorners.Min(c => c.Y) - newSprite.Position.Z - level.BottomPos; float maxY = spriteCorners.Max(c => c.Y) + newSprite.Position.Z - level.BottomPos; #if CLIENT if (newSprite.ParticleEmitters != null) { foreach (ParticleEmitter emitter in newSprite.ParticleEmitters) { Rectangle particleBounds = emitter.CalculateParticleBounds(pos.Value); minX = Math.Min(minX, particleBounds.X); maxX = Math.Max(maxX, particleBounds.Right); minY = Math.Min(minY, particleBounds.Y - level.BottomPos); maxY = Math.Max(maxY, particleBounds.Bottom - level.BottomPos); } } #endif sprites.Add(newSprite); int xStart = (int)Math.Floor(minX / GridSize); int xEnd = (int)Math.Floor(maxX / GridSize); if (xEnd < 0 || xStart >= spriteGrid.GetLength(0)) continue; int yStart = (int)Math.Floor(minY / GridSize); int yEnd = (int)Math.Floor(maxY / GridSize); if (yEnd < 0 || yStart >= spriteGrid.GetLength(1)) continue; xStart = Math.Max(xStart, 0); xEnd = Math.Min(xEnd, spriteGrid.GetLength(0) - 1); yStart = Math.Max(yStart, 0); yEnd = Math.Min(yEnd, spriteGrid.GetLength(1) - 1); for (int x = xStart; x <= xEnd; x++) { for (int y = yStart; y <= yEnd; y++) { if (spriteGrid[x, y] == null) spriteGrid[x, y] = new List(); spriteGrid[x, y].Add(newSprite); } } } } private Vector2? FindSpritePosition(Level level, BackgroundSpritePrefab prefab, out GraphEdge closestEdge, out Vector2 edgeNormal) { closestEdge = null; edgeNormal = Vector2.One; Vector2 randomPos = new Vector2( Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server), Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server)); if (prefab.SpawnPos == BackgroundSpritePrefab.SpawnPosType.None) return randomPos; List edges = new List(); List normals = new List(); System.Diagnostics.Debug.Assert(level.ExtraWalls.Length == 1); List cells = new List(); if (prefab.SpawnPos.HasFlag(BackgroundSpritePrefab.SpawnPosType.Wall)) cells.AddRange(level.GetCells(randomPos)); if (prefab.SpawnPos.HasFlag(BackgroundSpritePrefab.SpawnPosType.SeaFloor)) cells.AddRange(level.ExtraWalls[0].Cells); if (cells.Any()) { VoronoiCell cell = cells[Rand.Int(cells.Count, Rand.RandSync.Server)]; foreach (GraphEdge edge in cell.edges) { if (!edge.isSolid || edge.OutsideLevel) continue; Vector2 normal = edge.GetNormal(cell); if (prefab.Alignment.HasFlag(Alignment.Bottom) && normal.Y < -0.5f) { edges.Add(edge); } else if (prefab.Alignment.HasFlag(Alignment.Top) && normal.Y > 0.5f) { edges.Add(edge); } else if (prefab.Alignment.HasFlag(Alignment.Left) && normal.X < -0.5f) { edges.Add(edge); } else if (prefab.Alignment.HasFlag(Alignment.Right) && normal.X > 0.5f) { edges.Add(edge); } else { continue; } normals.Add(normal); } } if (prefab.SpawnPos.HasFlag(BackgroundSpritePrefab.SpawnPosType.RuinWall)) { foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) { Rectangle expandedArea = ruin.Area; expandedArea.Inflate(ruin.Area.Width, ruin.Area.Height); if (!expandedArea.Contains(randomPos)) continue; foreach (var ruinShape in ruin.RuinShapes) { foreach (var wall in ruinShape.Walls) { if (!prefab.Alignment.HasFlag(ruinShape.GetLineAlignment(wall))) continue; edges.Add(new GraphEdge(wall.A, wall.B)); normals.Add((wall.A + wall.B) / 2.0f - ruinShape.Center); } } } } if (!edges.Any()) return null; int index = Rand.Int(edges.Count, Rand.RandSync.Server); closestEdge = edges[index]; edgeNormal = normals[index]; float length = Vector2.Distance(closestEdge.point1, closestEdge.point2); Vector2 dir = (closestEdge.point1 - closestEdge.point2) / length; Vector2 pos = closestEdge.point2 + dir * Rand.Range(prefab.Sprite.size.X / 2.0f, length - prefab.Sprite.size.X / 2.0f, Rand.RandSync.Server); return pos; } public void Update(float deltaTime) { swingTimer += deltaTime; swingState = (float)Math.Sin(swingTimer * 0.1f); foreach (BackgroundSprite sprite in sprites) { sprite.Trigger?.Update(deltaTime); } UpdateProjSpecific(deltaTime); } partial void UpdateProjSpecific(float deltaTime); private BackgroundSpritePrefab GetRandomPrefab(string levelType) { int totalCommonness = 0; foreach (BackgroundSpritePrefab prefab in prefabs) { totalCommonness += prefab.GetCommonness(levelType); } float randomNumber = Rand.Int(totalCommonness+1, Rand.RandSync.Server); foreach (BackgroundSpritePrefab prefab in prefabs) { if (randomNumber <= prefab.GetCommonness(levelType)) { return prefab; } randomNumber -= prefab.GetCommonness(levelType); } return null; } } }