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 ThalamusItems; public readonly Cave Cave; private readonly List turrets = new List(); private readonly List spawnOrgans = new List(); private readonly List spawnPoints = new List(); 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 GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); private static IEnumerable 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 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(); 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 destroyedOrgans = new List(); 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()?.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 protectiveCells = new List(); 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 } }