359 lines
16 KiB
C#
359 lines
16 KiB
C#
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<Item> items = new List<Item>();
|
|
private readonly Dictionary<Item, StatusEffect> statusEffectOnApproach = new Dictionary<Item, StatusEffect>();
|
|
|
|
//key = monster to spawn, point = min,max
|
|
private readonly List<Tuple<CharacterPrefab, Point>> monsterPrefabs = new List<Tuple<CharacterPrefab, Point>>();
|
|
|
|
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"))
|
|
{
|
|
if (GameMain.NetworkMember == null && monsterElement.GetAttributeBool("multiplayeronly", false)) { continue; }
|
|
|
|
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, Point>(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<GraphEdge> spawnEdges = new List<GraphEdge>();
|
|
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<GraphEdge> validEdges = new List<GraphEdge>();
|
|
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;
|
|
}
|
|
}
|
|
}
|