using Barotrauma.Extensions; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using Voronoi2; namespace Barotrauma { partial class NestMission : Mission { private readonly ContentXElement itemConfig; private readonly List items = new List(); private readonly Dictionary statusEffectOnApproach = new Dictionary(); //string = filename, point = min,max private readonly HashSet> monsterPrefabs = new HashSet>(); private float itemSpawnRadius = 800.0f; private readonly float approachItemsRadius = 1000.0f; private readonly float nestObjectRadius = 1000.0f; private readonly float monsterSpawnRadius = 3000.0f; private readonly int nestObjectAmount = 10; private readonly bool requireDelivery; private readonly Level.PositionType spawnPositionType; private Vector2 nestPosition; private Level.Cave selectedCave; public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { yield break; } else { yield return (Prefab.SonarLabel, nestPosition); } } } public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { itemConfig = prefab.ConfigElement.GetChildElement("Items"); itemSpawnRadius = prefab.ConfigElement.GetAttributeFloat("itemspawnradius", 800.0f); approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f); monsterSpawnRadius = prefab.ConfigElement.GetAttributeFloat("monsterspawnradius", approachItemsRadius * 2.0f); nestObjectRadius = prefab.ConfigElement.GetAttributeFloat("nestobjectradius", itemSpawnRadius * 2.0f); nestObjectAmount = prefab.ConfigElement.GetAttributeInt("nestobjectamount", 10); requireDelivery = prefab.ConfigElement.GetAttributeBool("requiredelivery", false); string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) { spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { defaultCount = monsterElement.GetAttributeInt("amount", 1); } int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255); int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255); var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab != null) { monsterPrefabs.Add(new Tuple(characterPrefab, new Point(min, max))); } else { DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", contentPackage: Prefab.ContentPackage); } } } protected override void StartMissionSpecific(Level level) { if (items.Any()) { #if DEBUG throw new Exception($"items.Count > 0 ({items.Count})"); #else DebugConsole.AddWarning("Item list was not empty at the start of a nest mission. The mission instance may not have been ended correctly on previous rounds."); items.Clear(); #endif } if (!IsClient) { //ruin/cave/wreck items are allowed to spawn close to the sub float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ? 0.0f : Level.Loaded.Size.X * 0.3f; nestPosition = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); List spawnEdges = new List(); if (spawnPositionType == Level.PositionType.Cave) { Level.Cave closestCave = null; float closestCaveDist = float.PositiveInfinity; foreach (var cave in Level.Loaded.Caves) { float dist = Vector2.DistanceSquared(nestPosition, cave.Area.Center.ToVector2()); if (dist < closestCaveDist) { closestCave = cave; closestCaveDist = dist; } } if (closestCave != null) { selectedCave = closestCave; selectedCave.MissionsToDisplayOnSonar.Add(this); SpawnNestObjects(level, closestCave); } var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3); if (nearbyCells.Any()) { List validEdges = new List(); foreach (var edge in nearbyCells.SelectMany(c => c.Edges)) { if (!edge.NextToCave || !edge.IsSolid) { continue; } if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(edge.Center + edge.GetNormal(edge.Cell1 ?? edge.Cell2) * 100.0f))) { continue; } validEdges.Add(edge); } if (validEdges.Any()) { spawnEdges.AddRange(validEdges.Where(e => MathUtils.LineSegmentToPointDistanceSquared(e.Point1.ToPoint(), e.Point2.ToPoint(), nestPosition.ToPoint()) < itemSpawnRadius * itemSpawnRadius).Distinct()); } //no valid edges found close enough to the nest position, find the closest one if (!spawnEdges.Any()) { GraphEdge closestEdge = null; float closestDistSqr = float.PositiveInfinity; foreach (var edge in nearbyCells.SelectMany(c => c.Edges)) { if (!edge.NextToCave || !edge.IsSolid) { continue; } float dist = Vector2.DistanceSquared(edge.Center, nestPosition); if (dist < closestDistSqr) { closestEdge = edge; closestDistSqr = dist; } } if (closestEdge != null) { spawnEdges.Add(closestEdge); itemSpawnRadius = Math.Max(itemSpawnRadius, (float)Math.Sqrt(closestDistSqr) * 1.5f); } } } } foreach (var subElement in itemConfig.Elements()) { var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found", contentPackage: Prefab.ContentPackage); continue; } Vector2 spawnPos = nestPosition; float rotation = 0.0f; if (spawnEdges.Any()) { const float MinDistanceFromOtherItems = 30.0f; const int MaxTries = 10; for (int i = 0; i < MaxTries; i++) { var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); Vector2 normal = Vector2.UnitY; if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) { normal = edge.GetNormal(edge.Cell1); } else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid) { normal = edge.GetNormal(edge.Cell2); } spawnPos += normal * 10.0f; rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2; if (items.All(it => Vector2.DistanceSquared(it.WorldPosition, spawnPos) > MinDistanceFromOtherItems)) { break; } } } var item = new Item(itemPrefab, spawnPos, null); item.body.FarseerBody.BodyType = BodyType.Kinematic; item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation); item.FindHull(); item.AddTag("nestmission"); item.AddTag(Prefab.Identifier); items.Add(item); var statusEffectElement = subElement.GetChildElement("StatusEffectOnApproach") ?? subElement.GetChildElement("statuseffectonapproach"); if (statusEffectElement != null) { statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier.Value)); } } } } private void SpawnNestObjects(Level level, Level.Cave cave) { level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount); } protected override void UpdateMissionSpecific(float deltaTime) { if (IsClient) { foreach (Item item in items) { if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } } return; } switch (State) { case 0: foreach (Item item in items) { if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } if (statusEffectOnApproach.ContainsKey(item)) { foreach (Character character in Character.CharacterList) { if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < approachItemsRadius * approachItemsRadius) { statusEffectOnApproach[item].Apply(statusEffectOnApproach[item].type, 1.0f, item, item); statusEffectOnApproach.Remove(item); break; } } } } if (monsterPrefabs.Any()) { foreach (Character character in Character.CharacterList) { if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < monsterSpawnRadius * monsterSpawnRadius) { foreach (var monster in monsterPrefabs) { int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); for (int i = 0; i < amount; i++) { Vector2 offsetPosition; int tries = 0; do { offsetPosition = nestPosition + Rand.Vector(100.0f); tries++; if (tries > 10) { offsetPosition = nestPosition; break; } } while (Level.Loaded.IsPositionInsideWall(offsetPosition)); Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true); } } if (Level.Loaded.IsPositionInsideWall(nestPosition)) { DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).", Prefab.ContentPackage); } monsterPrefabs.Clear(); break; } } } //continue when all items are in the sub or destroyed if (AllItemsDestroyedOrRetrieved()) { State = 1; } break; case 1: if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } } private bool AllItemsDestroyedOrRetrieved() { if (requireDelivery) { foreach (Item item in items) { Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; if (parentSub?.Info?.Type == SubmarineType.Player) { continue; } return false; } } else { foreach (Item item in items) { if (item.Removed || item.Condition <= 0.0f) { continue; } if (Vector2.Distance(item.WorldPosition, nestPosition) > Math.Max(itemSpawnRadius * 2, 3000.0f)) { continue; } Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; if (parentSub?.Info?.Type == SubmarineType.Player) { continue; } return false; } } return true; } protected override bool DetermineCompleted() { return AllItemsDestroyedOrRetrieved(); } protected override void EndMissionSpecific(bool completed) { foreach (Item item in items) { if (item != null && !item.Removed) { item.Remove(); } } items.Clear(); failed = !completed && state > 0; } } }