Track LocalMods as part of monolith
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.MoreLevelContent.Shared.Utils;
|
||||
using HarmonyLib;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MoreLevelContent.Shared.AI
|
||||
{
|
||||
public class MLCAIObjectiveManager : Singleton<MLCAIObjectiveManager>
|
||||
{
|
||||
public override void Setup()
|
||||
{
|
||||
MethodInfo info = AccessTools.Method(typeof(AIObjectiveManager), nameof(AIObjectiveManager.CreateObjective));
|
||||
Main.Patch(info, postfix: new HarmonyMethod(typeof(MLCAIObjectiveManager), nameof(MLCAIObjectiveManager.AIObjectiveManager_CreateObjective)));
|
||||
Log.Debug("Setup AI override");
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
*
|
||||
*
|
||||
Exception: Object reference not set to an instance of an object. (System.NullReferenceException)
|
||||
Target site: Void AIObjectiveManager_CreateObjective(Barotrauma.AIObjective ByRef, Barotrauma.AIObjectiveManager, Barotrauma.Character, Barotrauma.Order)
|
||||
Stack trace:
|
||||
at MoreLevelContent.Shared.AI.MLCAIObjectiveManager.AIObjectiveManager_CreateObjective(AIObjective& __result, AIObjectiveManager __instance, Character ___character, Order order)
|
||||
at Barotrauma.AIObjectiveManager.CreateObjective_Patch1
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
internal static void AIObjectiveManager_CreateObjective(ref AIObjective __result, AIObjectiveManager __instance, Character ___character, Order order, float priorityModifier)
|
||||
{
|
||||
if (order == null || order.IsDismissal) { return; }
|
||||
AIObjective newObjective;
|
||||
switch (order.Identifier.Value.ToLowerInvariant())
|
||||
{
|
||||
case "traitorinjectitem":
|
||||
newObjective = new AITraitorObjectiveInjectItem(___character, __instance, priorityModifier, order.Option, order.GetTargetItems(order.Option));
|
||||
break;
|
||||
case "fightintrudersanysub":
|
||||
newObjective = new AIFightIntrudersAnySubObjective(___character, __instance, priorityModifier);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (newObjective != null)
|
||||
{
|
||||
newObjective.Identifier = order.Identifier;
|
||||
}
|
||||
__result = newObjective;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using static Barotrauma.AIObjectiveIdle;
|
||||
|
||||
namespace MoreLevelContent.Shared.AI
|
||||
{
|
||||
// Targets list not populating is probably the issue
|
||||
internal class AIFightIntrudersAnySubObjective : AIObjectiveFightIntruders
|
||||
{
|
||||
public AIFightIntrudersAnySubObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
|
||||
public override void FindTargets()
|
||||
{
|
||||
foreach (Character target in GetList())
|
||||
{
|
||||
if (!IsValidTarget(target)) { continue; }
|
||||
if (!character.CanSeeTarget(target)) { continue; }
|
||||
if (!ignoreList.Contains(target))
|
||||
{
|
||||
Targets.Add(target);
|
||||
if (Targets.Count > MaxTargets)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using static Barotrauma.AIObjectiveIdle;
|
||||
|
||||
namespace MoreLevelContent.Shared.AI
|
||||
{
|
||||
internal class AITraitorObjectiveInjectItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "traitorinject".ToIdentifier();
|
||||
// public override bool IsLoop { get => true; set => throw new NotImplementedException(); }
|
||||
public override bool CanBeCompleted => true;
|
||||
public AITraitorObjectiveInjectItem(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default, ImmutableArray<Identifier> targetItems = default) : base(character, objectiveManager, priorityModifier, option)
|
||||
{
|
||||
targetItemIdentifier = targetItems.GetRandomUnsynced();
|
||||
|
||||
// Pick our hated job
|
||||
hatedJob = JobPrefab.Prefabs.Where(p => !p.HiddenJob).GetRandomUnsynced().Identifier;
|
||||
Log.Debug($"Hated Job: {hatedJob}");
|
||||
character.IsEscorted = true; // lets them wander on the players sub
|
||||
|
||||
// for testing, makes them walk around
|
||||
// like normal people
|
||||
if (!Main.IsRelase)
|
||||
{
|
||||
var idleObjective = objectiveManager.GetObjective<AIObjectiveIdle>();
|
||||
if (idleObjective != null)
|
||||
{
|
||||
idleObjective.Behavior = AIObjectiveIdle.BehaviorType.Active;
|
||||
}
|
||||
}
|
||||
|
||||
actCasualTimer = 5;//Rand.Range(60, 120, Rand.RandSync.Unsynced);
|
||||
ForceWalkPermanently = true;
|
||||
character.OnAttacked += OnAttacked;
|
||||
}
|
||||
private bool HasItem => targetItem != null && character.Inventory.Contains(targetItem);
|
||||
private bool IsActingCasual => actCasualTimer > 0;
|
||||
|
||||
private readonly Identifier targetItemIdentifier;
|
||||
private readonly Identifier hatedJob;
|
||||
|
||||
|
||||
const string TraitorTeamChangeIdentifier = "traitor";
|
||||
const float CloseEnoughToInject = 100.0f;
|
||||
const float InjectDelay = 0.5f;
|
||||
|
||||
private float actCasualTimer;
|
||||
private bool _hasDoneSusAction = false;
|
||||
private bool _victimWasUsingItemWhenPicked = false;
|
||||
private bool _injectedVictim = false;
|
||||
private float _injectTimer = InjectDelay;
|
||||
|
||||
readonly List<Character> previousVictims = new List<Character>();
|
||||
AIObjectiveGetItem findItemTask;
|
||||
AIObjectiveGoTo gotoVictimTask;
|
||||
Item targetItem;
|
||||
Character victim;
|
||||
|
||||
// Set the priority of this to be low if we're cuffed or we're still acting /casual/
|
||||
public override float GetPriority()
|
||||
{
|
||||
Priority = IsActingCasual ? 0 : AIObjectiveManager.RunPriority - 0.5f;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
private void OnAttacked(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
if (attacker == null) return;
|
||||
if (_hasDoneSusAction && attacker.IsOnPlayerTeam && (attackResult.Damage > 1))
|
||||
{
|
||||
// we're made! switch team and start attacking
|
||||
if (!character.HasTeamChange(TraitorTeamChangeIdentifier))
|
||||
{
|
||||
_ = character.TryAddNewTeamChange(TraitorTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful));
|
||||
var fight = objectiveManager.GetObjective<AIFightIntrudersAnySubObjective>();
|
||||
fight.ForceHighestPriority = true;
|
||||
objectiveManager.AddObjective(fight);
|
||||
character.Speak(TextManager.Get($"dialog.{character.JobIdentifier}.found").Value);
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (character.Submarine == null) return;
|
||||
if (!character.Submarine.IsConnectedTo(Submarine.MainSub)) return;
|
||||
if (actCasualTimer > 0) actCasualTimer -= deltaTime;
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
AIObjectiveEscapeHandcuffs _EscapeHandcuffsSubObjective;
|
||||
public override void Act(float deltaTime)
|
||||
{
|
||||
// don't do anything if we're cuffed
|
||||
if (character.LockHands)
|
||||
{
|
||||
// If we're cuffed, try to break out
|
||||
_ = TryAddSubObjective(ref _EscapeHandcuffsSubObjective, () => new AIObjectiveEscapeHandcuffs(character, objectiveManager));
|
||||
return;
|
||||
}
|
||||
|
||||
// don't do anything if we're not in the main sub and not docked to it
|
||||
if (!character.Submarine.IsConnectedTo(Submarine.MainSub)) return;
|
||||
|
||||
// We're on the submarine, lets act casual for awhile
|
||||
if (IsActingCasual) return;
|
||||
|
||||
// Time to spring into action
|
||||
if (!HasItem)
|
||||
{
|
||||
// We don't have the target item yet, lets try to find it
|
||||
FindTargetItem();
|
||||
return;
|
||||
}
|
||||
|
||||
// We've found our target item, lets find a target to inject it into
|
||||
if (victim == null || victim.IsDead || victim.Removed)
|
||||
{
|
||||
victim = FindVictim();
|
||||
previousVictims.Add(victim);
|
||||
_victimWasUsingItemWhenPicked = victim.SelectedItem != null;
|
||||
DebugSpeak($"Picked new victim: {victim.Name}");
|
||||
|
||||
_ = TryAddSubObjective(ref gotoVictimTask, () =>
|
||||
new AIObjectiveGoTo(victim, character, objectiveManager, closeEnough: CloseEnoughToInject)
|
||||
{
|
||||
ForceWalkPermanently = true
|
||||
},
|
||||
() => GotToVictim(),
|
||||
() => CouldntGetToVictim());
|
||||
}
|
||||
|
||||
// Wait until we finish all the sub-objectives before doing anything
|
||||
if (subObjectives.Any()) return;
|
||||
|
||||
if (!character.CanInteractWith(victim))
|
||||
{
|
||||
// Go to the victim and select it
|
||||
RemoveSubObjective(ref gotoVictimTask);
|
||||
_ = TryAddSubObjective(ref gotoVictimTask, () => new AIObjectiveGoTo(victim, character, objectiveManager, closeEnough: CloseEnoughToInject)
|
||||
{
|
||||
ForceWalkPermanently = true
|
||||
},
|
||||
onCompleted: () => GotToVictim(),
|
||||
onAbandon: () => CouldntGetToVictim()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We're at the target, time to posion them!
|
||||
if (!_injectedVictim) InjectItem(deltaTime);
|
||||
}
|
||||
|
||||
// Leave the sceen of the crime
|
||||
private void Scram()
|
||||
{
|
||||
character.DeselectCharacter();
|
||||
Hull targetHull = GetEscapeHull();
|
||||
_ = TryAddSubObjective(ref gotoVictimTask, () =>
|
||||
{
|
||||
DebugSpeak("Getting outta dodge!");
|
||||
return new AIObjectiveGoTo(targetHull, character, objectiveManager)
|
||||
{
|
||||
ForceWalkPermanently = true
|
||||
};
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
float time = Rand.Range(60, 120, Rand.RandSync.Unsynced);
|
||||
actCasualTimer = time;
|
||||
DebugSpeak($"Time to act casual for {time}");
|
||||
_victimWasUsingItemWhenPicked = false;
|
||||
_injectedVictim = false;
|
||||
_injectTimer = InjectDelay;
|
||||
victim = null;
|
||||
targetItem = null;
|
||||
RemoveSubObjective(ref findItemTask);
|
||||
RemoveSubObjective(ref gotoVictimTask);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private Hull GetEscapeHull()
|
||||
{
|
||||
List<Hull> potentialEscapeHulls = new List<Hull>();
|
||||
List<float> hullWeights = new List<float>();
|
||||
if (character.Submarine == null) return null;
|
||||
foreach (var hull in character.Submarine.GetHulls(true))
|
||||
{
|
||||
// taken form AIObjectiveIdle
|
||||
if (hull == null || hull.AvoidStaying || hull.IsWetRoom) { continue; }
|
||||
// Ignore very narrow hulls.
|
||||
if (hull.RectWidth < 200) { continue; }
|
||||
// Ignore hulls that are too low to stand inside.
|
||||
if (character.AnimController is HumanoidAnimController animController)
|
||||
{
|
||||
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!potentialEscapeHulls.Contains(hull))
|
||||
{
|
||||
float weight = hull.RectWidth;
|
||||
|
||||
// prefer distant hulls
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
|
||||
yDist = yDist > 100 ? yDist * 5 : 0;
|
||||
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
|
||||
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist));
|
||||
|
||||
// prefer hulls with less water
|
||||
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
|
||||
weight *= distanceFactor * waterFactor;
|
||||
|
||||
potentialEscapeHulls.Add(hull);
|
||||
hullWeights.Add(weight);
|
||||
}
|
||||
}
|
||||
|
||||
return !potentialEscapeHulls.Any() ? null : ToolBox.SelectWeightedRandom(potentialEscapeHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
private void InjectItem(float deltaTime)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
|
||||
if (character.SelectedCharacter != victim)
|
||||
{
|
||||
character.SelectCharacter(victim);
|
||||
}
|
||||
|
||||
if (_injectTimer > 0.0f)
|
||||
{
|
||||
_injectTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
_injectTimer = InjectDelay;
|
||||
|
||||
targetItem.ApplyTreatment(character, victim, victim.AnimController.MainLimb);
|
||||
_injectedVictim = true;
|
||||
DebugSpeak("Injected the item!");
|
||||
|
||||
// We did it, time to get outta here!
|
||||
Scram();
|
||||
}
|
||||
|
||||
private void GotToVictim()
|
||||
{
|
||||
// We got to them
|
||||
RemoveSubObjective(ref gotoVictimTask);
|
||||
|
||||
// If they were using an item when we picked them, check if they're still using it
|
||||
if (_victimWasUsingItemWhenPicked && victim.SelectedItem == null)
|
||||
{
|
||||
// They're not using it, abort
|
||||
_victimWasUsingItemWhenPicked = false;
|
||||
victim = null;
|
||||
DebugSpeak($"They're not using the item anymore, abort!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void CouldntGetToVictim()
|
||||
{
|
||||
// We couldn't get to them
|
||||
RemoveSubObjective(ref gotoVictimTask);
|
||||
DebugSpeak($"Couldn't get to victim: {victim.Name}! Picking a new one...");
|
||||
_victimWasUsingItemWhenPicked = false;
|
||||
victim = null;
|
||||
return;
|
||||
}
|
||||
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (targetItem == null)
|
||||
{
|
||||
_ = TryAddSubObjective(ref findItemTask, () =>
|
||||
{
|
||||
DebugSpeak("Going to find the poison :)");
|
||||
return new AIObjectiveGetItem(character, targetItemIdentifier, objectiveManager, spawnItemIfNotFound: false, checkInventory: true)
|
||||
{
|
||||
ForceWalkPermanently = true,
|
||||
AllowStealing = true,
|
||||
SpeakIfFails = false
|
||||
};
|
||||
}, FoundItem, FailedToFindItem);
|
||||
|
||||
void FoundItem()
|
||||
{
|
||||
RemoveSubObjective(ref findItemTask);
|
||||
_hasDoneSusAction = true;
|
||||
TrySetTargetItem(character.Inventory.FindItemByIdentifier(targetItemIdentifier, true));
|
||||
DebugSpeak("Found the poison");
|
||||
actCasualTimer = 5; // act casual for a bit
|
||||
}
|
||||
|
||||
void FailedToFindItem()
|
||||
{
|
||||
// Wait for a bit before trying to find it again
|
||||
actCasualTimer = 10;
|
||||
|
||||
// Try to spawn the item in a traitor pannel
|
||||
ItemPrefab prefab = FindItemPrefab(targetItemIdentifier);
|
||||
if (!TryFindSuitableContainer(out Item container))
|
||||
{
|
||||
// We couldn't find a spot to spawn the item, just spawn it in our inventory
|
||||
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory);
|
||||
DebugSpeak("Couldn't find a place to spawn it, spawning it in my inventory!");
|
||||
return;
|
||||
}
|
||||
|
||||
// We found a spot to spawn the item in, lets spawn it there
|
||||
Entity.Spawner.AddItemToSpawnQueue(prefab, container.OwnInventory);
|
||||
DebugSpeak("Spawned the item in a hidden container!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected ItemPrefab FindItemPrefab(Identifier identifier) => (ItemPrefab)MapEntityPrefab.List.FirstOrDefault(prefab => prefab is ItemPrefab && prefab.Identifier == identifier);
|
||||
|
||||
private void TrySetTargetItem(Item item)
|
||||
{
|
||||
if (targetItem == item)
|
||||
{
|
||||
Log.Debug("Failed to get item!");
|
||||
return;
|
||||
}
|
||||
targetItem = item;
|
||||
}
|
||||
|
||||
private Character FindVictim()
|
||||
{
|
||||
return Character.CharacterList.Where(c => Filter(c)).OrderByDescending(c => CheckPriority(c)).First();
|
||||
|
||||
bool Filter(Character c) =>
|
||||
!c.Removed &&
|
||||
!c.IsDead &&
|
||||
c.IsHuman &&
|
||||
c.IsOnPlayerTeam &&
|
||||
c.Submarine != null &&
|
||||
c.Submarine.IsConnectedTo(Submarine.MainSub);
|
||||
|
||||
int CheckPriority(Character potentialVictim)
|
||||
{
|
||||
int priority = 0;
|
||||
|
||||
// Things that increase priority
|
||||
if (potentialVictim.IsPlayer) priority += 2;
|
||||
if (potentialVictim.JobIdentifier == hatedJob) priority += 2;
|
||||
if (potentialVictim.SelectedItem != null)
|
||||
{
|
||||
if (potentialVictim.SelectedItem.Tags.Contains("turret")) priority += 3;
|
||||
if (potentialVictim.SelectedItem.Tags.Contains("navterminal")) priority += 2;
|
||||
if (potentialVictim.SelectedItem.Tags.Contains("fabricator")) priority++;
|
||||
}
|
||||
|
||||
// Things that decrease priority
|
||||
if (potentialVictim.SelectedItem == null) priority--;
|
||||
if (potentialVictim.IsUnconscious) priority--;
|
||||
if (potentialVictim.IsIncapacitated) priority--;
|
||||
if (HumanAIController.GetHullSafety(potentialVictim.CurrentHull, potentialVictim) > HumanAIController.HULL_SAFETY_THRESHOLD) priority -= 4;
|
||||
if (previousVictims.Contains(potentialVictim)) priority -= 4;
|
||||
|
||||
if (potentialVictim.IsBot)
|
||||
{
|
||||
// Reduce priority of bots more if we're in multiplayer
|
||||
if (GameMain.IsMultiplayer && potentialVictim.IsBot) priority = 0;
|
||||
else priority--;
|
||||
}
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindSuitableContainer(out Item container)
|
||||
{
|
||||
List<Item> suitableItems = new List<Item>();
|
||||
List<Identifier> allowedContainerIdentifiers = new List<Identifier>()
|
||||
{
|
||||
"loosevent",
|
||||
"loosepanel"
|
||||
};
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.HiddenInGame || item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
|
||||
if (item.Submarine == null || !item.Submarine.IsConnectedTo(character.Submarine))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (item.GetComponent<ItemContainer>() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier))
|
||||
{
|
||||
if ((!item.OwnInventory.IsFull()))
|
||||
{
|
||||
suitableItems.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
container = suitableItems.GetRandomUnsynced();
|
||||
return container != null;
|
||||
}
|
||||
|
||||
protected void DebugSpeak(string msg)
|
||||
{
|
||||
if (!Main.IsRelase)
|
||||
{
|
||||
character.Speak(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// never abort
|
||||
//protected bool CheckObjectiveSpecific() => false;
|
||||
public override bool CheckObjectiveState() => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user