343 lines
14 KiBLFS
C#
Executable File
343 lines
14 KiBLFS
C#
Executable File
using Barotrauma;
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System;
|
|
using MoreLevelContent.Shared.Utils;
|
|
using static Barotrauma.Level;
|
|
using Voronoi2;
|
|
using MoreLevelContent.Shared.Generation;
|
|
using HarmonyLib;
|
|
|
|
namespace MoreLevelContent.Shared.AI
|
|
{
|
|
public class CaveAiConfig
|
|
{
|
|
public Identifier Entity => "thalamus";
|
|
public Identifier DefensiveAgent => "Leucocyte";
|
|
public string OffensiveAgent => "Terminalcell";
|
|
public string Brain => "thalamusbrain_cave";
|
|
public string Spawner => "cellspawnorgan_cave";
|
|
public float AgentSpawnDelay => 10;
|
|
public float AgentSpawnDelayRandomFactor => 0.25f;
|
|
public float AgentSpawnDelayDifficultyMultiplier => 1.0f;
|
|
public float AgentSpawnCountDifficultyMultiplier => 1.0f;
|
|
public int MaxAgentCount => 30;
|
|
public bool KillAgentsWhenEntityDies => true;
|
|
public float DeadEntityColorMultiplier => 0.5f;
|
|
public float DeadEntityColorFadeOutTime => 1;
|
|
}
|
|
|
|
partial class CaveAI : IServerSerializable
|
|
{
|
|
public bool IsAlive { get; private set; }
|
|
|
|
public readonly List<Item> ThalamusItems;
|
|
public readonly Cave Cave;
|
|
private readonly List<Turret> turrets = new List<Turret>();
|
|
private readonly List<Item> spawnOrgans = new List<Item>();
|
|
private readonly List<VoronoiCell> spawnPoints = new List<VoronoiCell>();
|
|
private readonly Item brain;
|
|
// Auto operate turrets need to have a submarine to work
|
|
public readonly Submarine DummySub;
|
|
|
|
private bool initialCellsSpawned;
|
|
|
|
public readonly CaveAiConfig Config = new CaveAiConfig();
|
|
|
|
private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
|
|
|
|
private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
|
|
|
|
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
|
|
|
|
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
|
|
|
|
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
|
|
|
|
public CaveAI(List<Item> allThalamusItems, GraphEdge spawnEdge, Cave cave)
|
|
{
|
|
Log.Debug($"it {allThalamusItems == null} se: {spawnEdge == null} cave: {cave == null}");
|
|
this.Cave = cave;
|
|
DummySub = new Submarine(new SubmarineInfo(), showErrorMessages: false)
|
|
{
|
|
TeamID = CharacterTeamType.None,
|
|
ShowSonarMarker = false
|
|
};
|
|
DummySub.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static;
|
|
DummySub.Info.Type = SubmarineType.EnemySubmarine;
|
|
|
|
allThalamusItems.ForEach(i => i.Submarine = DummySub);
|
|
|
|
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
|
|
var brainPrefab = thalamusPrefabs.Where(p => p.Tags.Contains(Config.Brain)).FirstOrDefault();
|
|
if (brainPrefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
|
|
return;
|
|
}
|
|
ThalamusItems = allThalamusItems;
|
|
|
|
brain = new Item(brainPrefab, Vector2.Zero, null);
|
|
ThalamusItems.Add(brain);
|
|
_ = MLCUtils.PositionItemOnEdge(brain, spawnEdge, 120, true);
|
|
|
|
// Setup spawner organs
|
|
spawnPoints = cave.Tunnels.SelectMany(t => t.Cells.Where(c => c.CellType != CellType.Solid && c.CellType != CellType.Removed)).ToList();
|
|
|
|
foreach (var item in allThalamusItems)
|
|
{
|
|
var turret = item.GetComponent<Turret>();
|
|
if (turret != null)
|
|
{
|
|
turrets.Add(turret);
|
|
turret.AutoOperate = false;
|
|
}
|
|
if (item.HasTag(Config.Spawner))
|
|
{
|
|
if (!spawnOrgans.Contains(item))
|
|
{
|
|
spawnOrgans.Add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// need to setup positions for initial cells to spawn
|
|
IsAlive = true;
|
|
ClearCave();
|
|
}
|
|
|
|
private readonly List<Item> destroyedOrgans = new List<Item>();
|
|
public void Update(float deltaTime)
|
|
{
|
|
// General AI management
|
|
if (!IsAlive) { return; }
|
|
if (Cave == null)
|
|
{
|
|
Remove();
|
|
return;
|
|
}
|
|
if (brain == null || brain.Removed || brain.Condition <= 0)
|
|
{
|
|
Kill();
|
|
return;
|
|
}
|
|
|
|
// Manage organs
|
|
destroyedOrgans.Clear();
|
|
foreach (var organ in spawnOrgans)
|
|
{
|
|
if (organ.Condition <= 0)
|
|
{
|
|
destroyedOrgans.Add(organ);
|
|
}
|
|
}
|
|
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
|
|
|
|
// Manage agro
|
|
bool someoneNearby = false;
|
|
float minDist = Sonar.DefaultSonarRange * 2.0f;
|
|
foreach (Submarine submarine in Submarine.Loaded)
|
|
{
|
|
if (submarine.Info.Type != SubmarineType.Player) { continue; }
|
|
if (Vector2.DistanceSquared(submarine.WorldPosition, Cave.StartPos.ToVector2()) < minDist * minDist)
|
|
{
|
|
someoneNearby = true;
|
|
break;
|
|
}
|
|
}
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
|
|
if (Vector2.DistanceSquared(c.WorldPosition, Cave.StartPos.ToVector2()) < minDist * minDist)
|
|
{
|
|
someoneNearby = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!someoneNearby) { return; }
|
|
OperateTurrets(deltaTime);
|
|
if (!IsClient)
|
|
{
|
|
if (!initialCellsSpawned)
|
|
{
|
|
SpawnInitialCells();
|
|
}
|
|
UpdateReinforcements(deltaTime);
|
|
}
|
|
}
|
|
|
|
private void ClearCave()
|
|
{
|
|
var wallsNearCave = Loaded.ExtraWalls.Where(w =>
|
|
w.Cells.Any(c => c.IsDestructible &&
|
|
(Cave.Area.Contains(c.Center) ||
|
|
Vector2.DistanceSquared(Cave.StartPos.ToVector2(), c.Center) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)));
|
|
|
|
foreach (var wall in wallsNearCave)
|
|
{
|
|
if (wall is DestructibleLevelWall destructible)
|
|
{
|
|
destructible.Destroy();
|
|
destructible.NetworkUpdatePending = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SpawnInitialCells()
|
|
{
|
|
int closeBrainCells = Rand.Range(5, 8);
|
|
|
|
for (int i = 0; i < closeBrainCells; i++)
|
|
{
|
|
if (!TrySpawnCell(out _, brain)) { break; }
|
|
}
|
|
|
|
int initalCells = Rand.Range(5, MaxCellCount);
|
|
for (int i = 0; i < initalCells; i++)
|
|
{
|
|
if (!TrySpawnCell(out _)) { break; }
|
|
}
|
|
|
|
initialCellsSpawned = true;
|
|
}
|
|
|
|
public void Kill()
|
|
{
|
|
ThalamusItems.ForEach(i => i.Condition = 0);
|
|
foreach (var turret in turrets)
|
|
{
|
|
// Snap all tendons
|
|
foreach (Item item in turret.ActiveProjectiles)
|
|
{
|
|
if (item.GetComponent<Projectile>()?.IsStuckToTarget ?? false)
|
|
{
|
|
item.Condition = 0;
|
|
}
|
|
}
|
|
}
|
|
FadeOutColors();
|
|
protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath);
|
|
if (!IsClient)
|
|
{
|
|
if (Config != null)
|
|
{
|
|
if (Config.KillAgentsWhenEntityDies)
|
|
{
|
|
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
|
|
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
|
|
{
|
|
foreach (var character in Character.CharacterList)
|
|
{
|
|
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
|
|
// but as long as spawning is handled via status effects, I don't know if there is any better way.
|
|
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
|
|
// And if there was, the distance check should prevent killing the agents of a different organism.
|
|
if (character.SpeciesName == Config.OffensiveAgent)
|
|
{
|
|
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
|
|
float maxDistance = Sonar.DefaultSonarRange;
|
|
if (Vector2.DistanceSquared(character.WorldPosition, Cave.StartPos.ToVector2()) < maxDistance * maxDistance)
|
|
{
|
|
character.Kill(CauseOfDeathType.Unknown, null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
protectiveCells.Clear();
|
|
IsAlive = false;
|
|
}
|
|
|
|
partial void FadeOutColors();
|
|
|
|
public void Remove()
|
|
{
|
|
Kill();
|
|
ThalamusItems?.Clear();
|
|
Log.Debug("Removed thalacave");
|
|
}
|
|
|
|
public void RemoveThalamusItems()
|
|
{
|
|
foreach (MapEntity thalamusItem in ThalamusItems)
|
|
{
|
|
if (thalamusItem.Removed) continue;
|
|
thalamusItem.Remove();
|
|
}
|
|
}
|
|
|
|
// The client doesn't use these, so we don't have to sync them.
|
|
private readonly List<Character> protectiveCells = new List<Character>();
|
|
private float cellSpawnTimer;
|
|
|
|
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
|
|
|
|
private int CalculateCellCount(int minValue, int maxValue)
|
|
{
|
|
if (maxValue == 0) { return 0; }
|
|
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
|
|
float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
|
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
|
|
}
|
|
|
|
private float GetSpawnTime()
|
|
{
|
|
float randomFactor = Config.AgentSpawnDelayRandomFactor;
|
|
float delay = Config.AgentSpawnDelay;
|
|
float min = delay;
|
|
float max = delay * 6;
|
|
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
|
|
float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
|
|
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
|
|
}
|
|
|
|
void UpdateReinforcements(float deltaTime)
|
|
{
|
|
if (spawnOrgans.Count == 0) { return; }
|
|
cellSpawnTimer -= deltaTime;
|
|
if (cellSpawnTimer < 0)
|
|
{
|
|
TrySpawnCell(out _, spawnOrgans.GetRandomUnsynced());
|
|
cellSpawnTimer = GetSpawnTime();
|
|
}
|
|
}
|
|
|
|
bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
|
|
{
|
|
cell = null;
|
|
if (protectiveCells.Count >= MaxCellCount) { return false; }
|
|
Vector2 worldSpawnPosition = targetEntity == null ? spawnPoints.GetRandomUnsynced().Center : targetEntity.WorldPosition;
|
|
|
|
// Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs.
|
|
cell = Character.Create(Config.DefensiveAgent, worldSpawnPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
|
|
protectiveCells.Add(cell);
|
|
cell.OnDeath += OnCellDeath;
|
|
cellSpawnTimer = GetSpawnTime();
|
|
return true;
|
|
}
|
|
|
|
void OperateTurrets(float deltaTime)
|
|
{
|
|
foreach (var turret in turrets)
|
|
{
|
|
turret.UpdateAutoOperate(deltaTime, true, Config.Entity);
|
|
}
|
|
}
|
|
|
|
void OnCellDeath(Character character, CauseOfDeath causeOfDeath) => protectiveCells.Remove(character);
|
|
|
|
#if SERVER
|
|
public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null)
|
|
{
|
|
msg.WriteBoolean(IsAlive);
|
|
}
|
|
#endif
|
|
}
|
|
}
|