(d622ac851) Fix double updates of current orders.

This commit is contained in:
Joonas Rikkonen
2019-05-16 05:47:36 +03:00
parent c09df5c602
commit 068089ae5c
54 changed files with 1714 additions and 1761 deletions
@@ -91,7 +91,7 @@ namespace Barotrauma
GUI.SmallFont.DrawString(spriteBatch, GUI.SmallFont.DrawString(spriteBatch,
currentNode.ID.ToString(), currentNode.ID.ToString(),
new Vector2(currentNode.DrawPosition.X + 20, -currentNode.DrawPosition.Y - 20), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30),
Color.Red); Color.Red);
} }
} }
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using FarseerPhysics; using FarseerPhysics;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -37,16 +38,20 @@ namespace Barotrauma
var currentOrder = ObjectiveManager.CurrentOrder; var currentOrder = ObjectiveManager.CurrentOrder;
if (currentOrder != null) if (currentOrder != null)
{ {
GUI.DrawString(spriteBatch, pos + textOffset, $"ORDER: {currentOrder.DebugTag} ({currentOrder.GetPriority(ObjectiveManager).FormatZeroDecimal()})", Color.White, Color.Black); GUI.DrawString(spriteBatch, pos + textOffset, $"ORDER: {currentOrder.DebugTag} ({currentOrder.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black);
}
else if (ObjectiveManager.WaitTimer > 0)
{
GUI.DrawString(spriteBatch, pos + textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black);
} }
var currentObjective = ObjectiveManager.CurrentObjective; var currentObjective = ObjectiveManager.CurrentObjective;
if (currentObjective != null) if (currentObjective != null)
{ {
GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.GetPriority(ObjectiveManager).FormatZeroDecimal()})", Color.White, Color.Black); GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black);
var subObjective = currentObjective.CurrentSubObjective; var subObjective = currentObjective.SubObjectives.FirstOrDefault();
if (subObjective != null) if (subObjective != null)
{ {
GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.GetPriority(ObjectiveManager).FormatZeroDecimal()})", Color.White, Color.Black); GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black);
} }
} }
} }
@@ -75,8 +80,8 @@ namespace Barotrauma
GUI.SmallFont.DrawString(spriteBatch, GUI.SmallFont.DrawString(spriteBatch,
currentNode.ID.ToString(), currentNode.ID.ToString(),
new Vector2(currentNode.DrawPosition.X + 20, -currentNode.DrawPosition.Y - 20), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30),
Color.SkyBlue); Color.Blue);
} }
} }
} }
@@ -108,6 +108,61 @@ namespace Barotrauma
} }
} }
//unconscious/dead characters can't correct their position using AnimController movement
// -> we need to correct it manually
if (!character.AllowInput)
{
float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, Collider.SimPosition);
float mainLimbErrorTolerance = 0.1f;
//if the main limb is roughly at the correct position and the collider isn't moving (much at least),
//don't attempt to correct the position.
if (mainLimbDistSqrd > mainLimbErrorTolerance || Collider.LinearVelocity.LengthSquared() > 0.05f)
{
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true;
}
character.SelectedConstruction = character.MemState[0].SelectedItem;
}
if (character.MemState[0].Animation == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
else if (character.AnimController.Anim == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.None;
}
Vector2 newVelocity = Collider.LinearVelocity;
Vector2 newPosition = Collider.SimPosition;
float newRotation = Collider.Rotation;
float newAngularVelocity = Collider.AngularVelocity;
Collider.CorrectPosition(character.MemState, out newPosition, out newVelocity, out newRotation, out newAngularVelocity);
newVelocity = newVelocity.ClampLength(100.0f);
if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; }
overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero;
Collider.LinearVelocity = newVelocity;
Collider.AngularVelocity = newAngularVelocity;
float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition);
float errorTolerance = character.AllowInput ? 0.01f : 0.2f;
if (distSqrd > errorTolerance)
{
if (distSqrd > 10.0f || !character.AllowInput)
{
Collider.TargetRotation = newRotation;
SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false);
}
else
{
Collider.TargetRotation = newRotation;
Collider.TargetPosition = newPosition;
Collider.MoveToTargetPosition(true);
}
}
//unconscious/dead characters can't correct their position using AnimController movement //unconscious/dead characters can't correct their position using AnimController movement
// -> we need to correct it manually // -> we need to correct it manually
if (!character.AllowInput) if (!character.AllowInput)
@@ -151,32 +206,34 @@ namespace Barotrauma
} }
} }
character.MemLocalState.Clear();
if (character.MemLocalState.Count > 120) character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120);
character.MemState.Clear();
} }
} else
partial void ImpactProjSpecific(float impact, Body body)
{
float volume = MathHelper.Clamp(impact - 3.0f, 0.5f, 1.0f);
if (body.UserData is Limb limb && character.Stun <= 0f)
{ {
if (impact > 3.0f) { PlayImpactSound(limb); } //remove states with a timestamp (there may still timestamp-based states
} //in the list if the controlled character switches from timestamp-based interpolation to ID-based)
else if (body.UserData is Limb || body == Collider.FarseerBody) character.MemState.RemoveAll(m => m.Timestamp > 0.0f);
{
if (!character.IsRemotePlayer && impact > ImpactTolerance) for (int i = 0; i < character.MemLocalState.Count; i++)
{ {
SoundPlayer.PlayDamageSound("LimbBlunt", strongestImpact, Collider); if (character.Submarine == null)
{
//transform in-sub coordinates to outside coordinates
if (character.MemLocalState[i].Position.Y > lowestSubPos)
{
character.MemLocalState[i].TransformInToOutside();
}
}
else if (currentHull?.Submarine != null)
{
//transform outside coordinates to in-sub coordinates
if (character.MemLocalState[i].Position.Y < lowestSubPos)
{
character.MemLocalState[i].TransformOutToInside(currentHull.Submarine);
}
}
} }
}
if (Character.Controlled == character)
{
GameMain.GameScreen.Cam.Shake = Math.Min(Math.Max(strongestImpact, GameMain.GameScreen.Cam.Shake), 3.0f);
}
}
if (character.MemState.Count < 1) return; if (character.MemState.Count < 1) return;
@@ -9,10 +9,8 @@ namespace Barotrauma
public class GUIMessageBox : GUIFrame public class GUIMessageBox : GUIFrame
{ {
public static List<GUIComponent> MessageBoxes = new List<GUIComponent>(); public static List<GUIComponent> MessageBoxes = new List<GUIComponent>();
private static int DefaultWidth
{ public const int DefaultWidth = 400, DefaultHeight = 250;
get { return Math.Max(400, 400 * (GameMain.GraphicsWidth / 1920)); }
}
public List<GUIButton> Buttons { get; private set; } = new List<GUIButton>(); public List<GUIButton> Buttons { get; private set; } = new List<GUIButton>();
//public GUIFrame BackgroundFrame { get; private set; } //public GUIFrame BackgroundFrame { get; private set; }
@@ -24,30 +22,22 @@ namespace Barotrauma
public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault();
public GUIMessageBox(string headerText, string text, Vector2? relativeSize = null, Point? minSize = null) public GUIMessageBox(string headerText, string text)
: this(headerText, text, new string[] { "OK" }, relativeSize, minSize) : this(headerText, text, new string[] {"OK"}, DefaultWidth, 0)
{ {
this.Buttons[0].OnClicked = Close; this.Buttons[0].OnClicked = Close;
} }
public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, string tag = "") public GUIMessageBox(string headerText, string text, int width, int height)
: this(headerText, text, new string[] { "OK" }, width, height)
{
this.Buttons[0].OnClicked = Close;
}
// TODO: allow to use a relative size.
public GUIMessageBox(string headerText, string text, string[] buttons, int width = DefaultWidth, int height = 0, Alignment textAlignment = Alignment.TopLeft, string tag = "")
: base(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "") : base(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "")
{ {
int width = DefaultWidth, height = 0;
if (relativeSize.HasValue)
{
width = (int)(GameMain.GraphicsWidth * relativeSize.Value.X);
height = (int)(GameMain.GraphicsHeight * relativeSize.Value.Y);
}
if (minSize.HasValue)
{
width = Math.Max(width, minSize.Value.X);
if (height > 0)
{
height = Math.Max(height, minSize.Value.Y);
}
}
InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, Anchor.Center) { IsFixedSize = false }, style: null); InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, Anchor.Center) { IsFixedSize = false }, style: null);
GUI.Style.Apply(InnerFrame, "", this); GUI.Style.Apply(InnerFrame, "", this);
@@ -59,7 +49,7 @@ namespace Barotrauma
GUI.Style.Apply(Header, "", this); GUI.Style.Apply(Header, "", this);
Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height);
if (height == 0) if (!string.IsNullOrWhiteSpace(text))
{ {
Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform),
text, textAlignment: textAlignment, wrap: true); text, textAlignment: textAlignment, wrap: true);
@@ -84,10 +74,6 @@ namespace Barotrauma
height += Header.Rect.Height + Content.AbsoluteSpacing; height += Header.Rect.Height + Content.AbsoluteSpacing;
height += (Text == null ? 0 : Text.Rect.Height) + Content.AbsoluteSpacing; height += (Text == null ? 0 : Text.Rect.Height) + Content.AbsoluteSpacing;
height += buttonContainer.Rect.Height; height += buttonContainer.Rect.Height;
if (minSize.HasValue)
{
height = Math.Max(height, minSize.Value.Y);
}
InnerFrame.RectTransform.NonScaledSize = InnerFrame.RectTransform.NonScaledSize =
new Point(InnerFrame.Rect.Width, (int)Math.Max(height / Content.RectTransform.RelativeSize.Y, height + 50)); new Point(InnerFrame.Rect.Width, (int)Math.Max(height / Content.RectTransform.RelativeSize.Y, height + 50));
@@ -18,38 +18,21 @@ namespace Barotrauma
private Sprite languageSelectionCursor; private Sprite languageSelectionCursor;
private ScalableFont languageSelectionFont; private ScalableFont languageSelectionFont;
private Video currSplashScreen; private Video splashScreen;
private DateTime videoStartTime; public Video SplashScreen
private Queue<Pair<string, Point>> pendingSplashScreens = new Queue<Pair<string, Point>>();
/// <summary>
/// Pair.first = filepath, Pair.second = resolution
/// </summary>
public Queue<Pair<string, Point>> PendingSplashScreens
{ {
get get
{ {
lock (loadMutex) lock (loadMutex)
{ {
return pendingSplashScreens; return splashScreen;
} }
} }
set set
{ {
lock (loadMutex) lock (loadMutex)
{ {
pendingSplashScreens = value; splashScreen = value;
}
}
}
public bool PlayingSplashScreen
{
get
{
lock (loadMutex)
{
return currSplashScreen != null;
} }
} }
} }
@@ -117,8 +100,8 @@ namespace Barotrauma
{ {
try try
{ {
DrawSplashScreen(spriteBatch, graphics); DrawSplashScreen(spriteBatch);
if (currSplashScreen != null || PendingSplashScreens.Count > 0) { return; } if (SplashScreen != null && SplashScreen.IsPlaying) return;
} }
catch (Exception e) catch (Exception e)
{ {
@@ -218,74 +201,43 @@ namespace Barotrauma
{ {
if (languageSelectionFont == null) if (languageSelectionFont == null)
{ {
languageSelectionFont = new ScalableFont("Content/Fonts/BebasNeue-Regular.otf", (uint)(30 * (GameMain.GraphicsHeight / 1080.0f)), graphicsDevice); languageSelectionFont = new ScalableFont("Content/Fonts/BebasNeue-Regular.otf", 28, graphicsDevice);
} }
if (languageSelectionCursor == null) if (languageSelectionCursor == null)
{ {
languageSelectionCursor = new Sprite("Content/UI/cursor.png", Vector2.Zero); languageSelectionCursor = new Sprite("Content/UI/cursor.png", Vector2.Zero);
} }
Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.3f); Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.25f);
Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / TextManager.AvailableLanguages.Count()); Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / TextManager.AvailableLanguages.Count());
foreach (string language in TextManager.AvailableLanguages) foreach (string language in TextManager.AvailableLanguages)
{ {
Vector2 textSize = languageSelectionFont.MeasureString(language); languageSelectionFont.DrawString(spriteBatch, language, textPos - languageSelectionFont.MeasureString(language) / 2, Color.White * 0.8f);
bool hover =
Math.Abs(PlayerInput.MousePosition.X - textPos.X) < textSize.X / 2 &&
Math.Abs(PlayerInput.MousePosition.Y - textPos.Y) < textSpacing.Y / 2;
//TODO: display the name of the language in the target language?
languageSelectionFont.DrawString(spriteBatch, language, textPos - textSize / 2,
hover ? Color.White : Color.White * 0.6f);
if (hover && PlayerInput.LeftButtonClicked())
{
GameMain.Config.Language = language;
WaitForLanguageSelection = false;
}
textPos += textSpacing; textPos += textSpacing;
} }
languageSelectionCursor.Draw(spriteBatch, PlayerInput.LatestMousePosition); languageSelectionCursor.Draw(spriteBatch, PlayerInput.LatestMousePosition);
} }
private void DrawSplashScreen(SpriteBatch spriteBatch, GraphicsDevice graphics) private void DrawSplashScreen(SpriteBatch spriteBatch)
{ {
if (currSplashScreen == null && PendingSplashScreens.Count == 0) { return; } if (SplashScreen != null)
if (currSplashScreen == null)
{ {
var newSplashScreen = PendingSplashScreens.Dequeue(); if (SplashScreen.IsPlaying)
string fileName = newSplashScreen.First;
Point resolution = newSplashScreen.Second;
try
{ {
currSplashScreen = new Video(graphics, GameMain.SoundManager, fileName, (uint)resolution.X, (uint)resolution.Y); spriteBatch.Begin();
videoStartTime = DateTime.Now; spriteBatch.Draw(SplashScreen.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White);
} spriteBatch.End();
catch (Exception e)
{
GameMain.Config.EnableSplashScreen = false;
DebugConsole.ThrowError("Playing the splash screen \"" + fileName + "\" failed.", e);
PendingSplashScreens.Clear();
currSplashScreen = null;
}
}
if (currSplashScreen.IsPlaying) if (PlayerInput.KeyHit(Keys.Space) || PlayerInput.KeyHit(Keys.Enter) || PlayerInput.LeftButtonDown())
{ {
spriteBatch.Begin(); SplashScreen.Dispose(); SplashScreen = null;
spriteBatch.Draw(currSplashScreen.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); }
spriteBatch.End(); }
else
if (PlayerInput.KeyHit(Keys.Space) || PlayerInput.KeyHit(Keys.Enter) || PlayerInput.LeftButtonDown()) {
{ SplashScreen.Dispose(); SplashScreen = null;
currSplashScreen.Dispose(); currSplashScreen = null;
} }
}
else if (DateTime.Now > videoStartTime + new TimeSpan(0, 0, 0, 0, milliseconds: 500))
{
currSplashScreen.Dispose(); currSplashScreen = null;
} }
} }
+11 -18
View File
@@ -270,17 +270,15 @@ namespace Barotrauma
WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content); WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content);
loadingScreenOpen = true; loadingScreenOpen = true;
TitleScreen = new LoadingScreen(GraphicsDevice) TitleScreen = new LoadingScreen(GraphicsDevice);
{ TitleScreen.WaitForLanguageSelection = Config.ShowLanguageSelectionPrompt;
WaitForLanguageSelection = Config.ShowLanguageSelectionPrompt
};
bool canLoadInSeparateThread = false; bool canLoadInSeparateThread = false;
#if WINDOWS #if WINDOWS
canLoadInSeparateThread = true; canLoadInSeparateThread = true;
#endif #endif
loadingCoroutine = CoroutineManager.StartCoroutine(Load(canLoadInSeparateThread), "", canLoadInSeparateThread); loadingCoroutine = CoroutineManager.StartCoroutine(Load(), "", canLoadInSeparateThread);
} }
private void InitUserStats() private void InitUserStats()
@@ -336,21 +334,16 @@ namespace Barotrauma
SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume); SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume);
if (Config.EnableSplashScreen) if (Config.EnableSplashScreen)
{ {
var pendingSplashScreens = TitleScreen.PendingSplashScreens; try
pendingSplashScreens?.Enqueue(new Pair<string, Point>("Content/Splash_UTG.mp4", new Point(1280, 720)));
pendingSplashScreens?.Enqueue(new Pair<string, Point>("Content/Splash_FF.mp4", new Point(1280, 720)));
pendingSplashScreens?.Enqueue(new Pair<string, Point>("Content/Splash_Daedalic.mp4", new Point(1920, 1080)));
}
//if not loading in a separate thread, wait for the splash screens to finish before continuing the loading
//otherwise the videos will look extremely choppy
if (!isSeparateThread)
{
while (TitleScreen.PlayingSplashScreen || TitleScreen.PendingSplashScreens.Count > 0)
{ {
yield return CoroutineStatus.Running; (TitleScreen as LoadingScreen).SplashScreen = new Video(base.GraphicsDevice, SoundManager, "Content/splashscreen.mp4", 1280, 720);
} }
} catch (Exception e)
{
Config.EnableSplashScreen = false;
DebugConsole.ThrowError("Playing the splash screen failed.", e);
}
}
GUI.Init(Window, Config.SelectedContentPackages, GraphicsDevice); GUI.Init(Window, Config.SelectedContentPackages, GraphicsDevice);
DebugConsole.Init(); DebugConsole.Init();
@@ -74,17 +74,12 @@ namespace Barotrauma
public CrewManager(XElement element, bool isSinglePlayer) public CrewManager(XElement element, bool isSinglePlayer)
: this(isSinglePlayer) : this(isSinglePlayer)
{ {
if (GameMain.Client != null) if (!isSinglePlayer)
{ {
//let the server create random conversations in MP DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace);
return; return;
} }
List<Character> availableSpeakers = Character.CharacterList.FindAll(c => if (string.IsNullOrEmpty(text)) { return; }
c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
}
var characterInfo = new CharacterInfo(subElement); var characterInfo = new CharacterInfo(subElement);
characterInfos.Add(characterInfo); characterInfos.Add(characterInfo);
@@ -95,6 +90,7 @@ namespace Barotrauma
break; break;
} }
} }
ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender));
} }
partial void InitProjectSpecific() partial void InitProjectSpecific()
@@ -199,6 +195,7 @@ namespace Barotrauma
if (Character.Controlled == null || Character.Controlled.SpeechImpediment >= 100.0f) return false; if (Character.Controlled == null || Character.Controlled.SpeechImpediment >= 100.0f) return false;
SetCharacterOrder(null, order, null, Character.Controlled); SetCharacterOrder(null, order, null, Character.Controlled);
HumanAIController.PropagateHullSafety(Character.Controlled, Character.Controlled.CurrentHull); HumanAIController.PropagateHullSafety(Character.Controlled, Character.Controlled.CurrentHull);
HumanAIController.RefreshTargets(Character.Controlled, order, Character.Controlled.CurrentHull);
return true; return true;
}, },
UserData = order, UserData = order,
@@ -242,24 +239,27 @@ namespace Barotrauma
public IEnumerable<Character> GetCharacters() public IEnumerable<Character> GetCharacters()
{ {
if (characterInfos.Contains(characterInfo)) if (character?.Inventory == null) return null;
{
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace);
return;
}
characterInfos.Add(characterInfo); var radioItem = character.Inventory.Items.FirstOrDefault(it => it != null && it.GetComponent<WifiComponent>() != null);
if (radioItem == null) return null;
if (requireEquipped && !character.HasEquippedItem(radioItem)) return null;
return radioItem.GetComponent<WifiComponent>();
} }
public IEnumerable<CharacterInfo> GetCharacterInfos() public IEnumerable<CharacterInfo> GetCharacterInfos()
{ {
if (character == null) if (GameMain.Client != null)
{ {
DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace); //let the server create random conversations in MP
return; return;
} }
characters.Remove(character); List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
if (removeInfo) characterInfos.Remove(character.Info); c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
} }
public void AddCharacter(Character character) public void AddCharacter(Character character)
@@ -633,183 +633,9 @@ namespace Barotrauma
{ {
characterListBox.BarScroll = roundedPos; characterListBox.BarScroll = roundedPos;
} }
var characterArea = new GUIButton(new RectTransform(new Point(characterInfoWidth, frame.Rect.Height), frame.RectTransform, Anchor.CenterLeft), style: "GUITextBox") soundIcon.Visible = !muted && !mutedLocally;
{ soundIconDisabled.Visible = muted || mutedLocally;
UserData = character, soundIconDisabled.ToolTip = TextManager.Get(mutedLocally ? "MutedLocally" : "MutedGlobally");
Color = frame.Color,
SelectedColor = frame.SelectedColor,
HoverColor = frame.HoverColor,
ToolTip = characterToolTip
};
var soundIcon = new GUIImage(new RectTransform(new Point((int)(characterArea.Rect.Height * 0.5f)), characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) },
"GUISoundIcon")
{
UserData = "soundicon",
CanBeFocused = false,
Visible = true
};
soundIcon.Color = new Color(soundIcon.Color, 0.0f);
new GUIImage(new RectTransform(new Point((int)(characterArea.Rect.Height * 0.5f)), characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) },
"GUISoundIconDisabled")
{
UserData = "soundicondisabled",
CanBeFocused = true,
Visible = false
};
if (isSinglePlayer)
{
characterArea.OnClicked = CharacterClicked;
}
else
{
characterArea.CanBeFocused = false;
characterArea.CanBeSelected = false;
}
var characterImage = new GUICustomComponent(new RectTransform(new Point(characterArea.Rect.Height), characterArea.RectTransform, Anchor.CenterLeft),
onDraw: (sb, component) => character.Info.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2()))
{
CanBeFocused = false,
HoverColor = Color.White,
SelectedColor = Color.White,
ToolTip = characterToolTip
};
var characterName = new GUITextBlock(new RectTransform(new Point(characterArea.Rect.Width - characterImage.Rect.Width - soundIcon.Rect.Width - 10, characterArea.Rect.Height),
characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(soundIcon.Rect.Width + 10, 0) },
character.Name, textColor: frame.Color, font: GUI.SmallFont, wrap: true)
{
Color = frame.Color,
HoverColor = Color.Transparent,
SelectedColor = Color.Transparent,
CanBeFocused = false,
ToolTip = characterToolTip,
AutoScale = true
};
//---------------- order buttons ----------------
var orderButtonFrame = new GUILayoutGroup(new RectTransform(new Point(100, frame.Rect.Height), frame.RectTransform)
{ AbsoluteOffset = new Point(characterInfoWidth + spacing, 0) },
isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
AbsoluteSpacing = (int)(10 * GUI.Scale),
UserData = "orderbuttons",
CanBeFocused = false
};
//listbox for holding the orders inappropriate for this character
//(so we can easily toggle their visibility)
var wrongOrderList = new GUIListBox(new RectTransform(new Point(50, orderButtonFrame.Rect.Height), orderButtonFrame.RectTransform), isHorizontal: true, style: null)
{
ScrollBarEnabled = false,
ScrollBarVisible = false,
Enabled = false,
Spacing = spacing,
ClampMouseRectToParent = false
};
wrongOrderList.Content.ClampMouseRectToParent = false;
for (int i = 0; i < orders.Count; i++)
{
var order = orders[i];
if (order.TargetAllCharacters) continue;
RectTransform btnParent = (i >= correctOrderCount + neutralOrderCount) ?
wrongOrderList.Content.RectTransform :
orderButtonFrame.RectTransform;
var btn = new GUIButton(new RectTransform(new Point(iconSize, iconSize), btnParent, Anchor.CenterLeft),
style: null)
{
UserData = order
};
new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlow")
{
Color = Color.Lerp(order.Color, frame.Color, 0.5f) * 0.8f,
HoverColor = Color.Lerp(order.Color, frame.Color, 0.5f) * 1.0f,
PressedColor = Color.Lerp(order.Color, frame.Color, 0.5f) * 0.6f,
UserData = "selected",
CanBeFocused = false,
Visible = false
};
var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), order.Prefab.SymbolSprite);
img.Scale = iconSize / (float)img.SourceRect.Width;
img.Color = Color.Lerp(order.Color, frame.Color, 0.5f);
img.ToolTip = order.Name;
img.HoverColor = Color.Lerp(img.Color, Color.White, 0.5f);
btn.OnClicked += (GUIButton button, object userData) =>
{
if (Character.Controlled == null || Character.Controlled.SpeechImpediment >= 100.0f) return false;
if (btn.GetChildByUserData("selected").Visible)
{
SetCharacterOrder(character, Order.PrefabList.Find(o => o.AITag == "dismissed"), null, Character.Controlled);
}
else
{
if (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0 || order.Options.Length > 1)
{
CreateOrderTargetFrame(button, character, order);
}
else
{
SetCharacterOrder(character, order, null, Character.Controlled);
}
}
return true;
};
btn.UserData = order;
btn.ToolTip = order.Name;
//divider between different groups of orders
if (i == correctOrderCount - 1 || i == correctOrderCount + neutralOrderCount - 1)
{
//TODO: divider sprite
new GUIFrame(new RectTransform(new Point(8, iconSize), orderButtonFrame.RectTransform), style: "GUIButton");
}
}
var toggleWrongOrderBtn = new GUIButton(new RectTransform(new Point((int)(30 * GUI.Scale), wrongOrderList.Rect.Height), wrongOrderList.Content.RectTransform),
"", style: "UIToggleButton")
{
UserData = "togglewrongorder",
CanBeFocused = false
};
wrongOrderList.RectTransform.NonScaledSize = new Point(
wrongOrderList.Content.Children.Sum(c => c.Rect.Width + wrongOrderList.Spacing),
wrongOrderList.RectTransform.NonScaledSize.Y);
wrongOrderList.RectTransform.SetAsLastChild();
new GUIFrame(new RectTransform(new Point(
wrongOrderList.Rect.Width - toggleWrongOrderBtn.Rect.Width - wrongOrderList.Spacing * 2,
wrongOrderList.Rect.Height), wrongOrderList.Content.RectTransform),
style: null)
{
CanBeFocused = false
};
//scale to fit the content
orderButtonFrame.RectTransform.NonScaledSize = new Point(
orderButtonFrame.Children.Sum(c => c.Rect.Width + orderButtonFrame.AbsoluteSpacing),
orderButtonFrame.RectTransform.NonScaledSize.Y);
frame.RectTransform.NonScaledSize = new Point(
characterInfoWidth + spacing + (orderButtonFrame.Rect.Width - wrongOrderList.Rect.Width),
frame.RectTransform.NonScaledSize.Y);
characterListBox.RectTransform.NonScaledSize = new Point(
characterListBox.Content.Children.Max(c => c.Rect.Width) + wrongOrderList.Rect.Width,
characterListBox.RectTransform.NonScaledSize.Y);
characterListBox.Content.RectTransform.NonScaledSize = characterListBox.RectTransform.NonScaledSize;
characterListBox.UpdateScrollBarSize();
return frame;
} }
private IEnumerable<object> KillCharacterAnim(GUIComponent component) private IEnumerable<object> KillCharacterAnim(GUIComponent component)
@@ -953,12 +779,6 @@ namespace Barotrauma
} }
return; return;
} }
List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
}
character.SetOrder(order, option, orderGiver, speak: orderGiver != character); character.SetOrder(order, option, orderGiver, speak: orderGiver != character);
if (IsSinglePlayer) if (IsSinglePlayer)
@@ -1016,23 +836,19 @@ namespace Barotrauma
} }
} }
} }
//only one target (or an order with no particular targets), just show options
character.SetOrder(order, option, orderGiver, speak: orderGiver != character); else
if (IsSinglePlayer)
{ {
orderGiver?.Speak( orderTargetFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.2f + order.Options.Length * 0.1f, 0.18f), GUI.Canvas)
order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); { AbsoluteOffset = new Point(orderButton.Rect.Center.X, orderButton.Rect.Bottom) },
} isHorizontal: true, childAnchor: Anchor.BottomLeft)
else if (orderGiver != null)
{
OrderChatMessage msg = new OrderChatMessage(order, option, order.TargetItemComponent?.Item, character, orderGiver);
if (GameMain.Client != null)
{ {
GameMain.Client.SendChatMessage(msg); UserData = character,
} Stretch = true
} };
DisplayCharacterOrder(character, order); //line connecting the order button to the option buttons
} //TODO: sprite
new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), orderTargetFrame.RectTransform), style: null);
/// <summary> /// <summary>
/// Create the UI panel that's used to select the target and options for a given order /// Create the UI panel that's used to select the target and options for a given order
@@ -776,6 +776,37 @@ namespace Barotrauma
ic.DrawHUD(spriteBatch, character); ic.DrawHUD(spriteBatch, character);
} }
} }
}
List<ColoredText> texts = new List<ColoredText>();
public List<ColoredText> GetHUDTexts(Character character)
{
texts.Clear();
foreach (ItemComponent ic in components)
{
if (string.IsNullOrEmpty(ic.DisplayMsg)) continue;
if (!ic.CanBePicked && !ic.CanBeSelected) continue;
if (ic is Holdable holdable && !holdable.CanBeDeattached()) continue;
Color color = Color.Gray;
bool hasRequiredSkillsAndItems = ic.HasRequiredSkills(character) && ic.HasRequiredItems(character, false);
if (hasRequiredSkillsAndItems)
{
if (ic is Repairable repairable)
{
if (Condition < repairable.ShowRepairUIThreshold)
{
color = Color.Cyan;
}
}
else
{
color = Color.Cyan;
}
}
texts.Add(new ColoredText(ic.DisplayMsg, color, false));
}
return texts; return texts;
} }
@@ -491,6 +491,18 @@ namespace Barotrauma
} }
} }
foreach (MapEntity e in MapEntity.mapEntityList)
{
if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000)
{
//move disabled items (wires, items inside containers) inside the sub
if (e is Item item && item.body != null && !item.body.Enabled)
{
item.SetTransform(ConvertUnits.ToSimUnits(HiddenSubPosition), 0.0f);
}
}
}
foreach (MapEntity e in MapEntity.mapEntityList) foreach (MapEntity e in MapEntity.mapEntityList)
{ {
if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000) if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000)
@@ -1012,7 +1012,7 @@ namespace Barotrauma.Networking
} }
} }
GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("PermissionsChanged"), msg) GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("PermissionsChanged"), msg, GUIMessageBox.DefaultWidth, 0)
{ {
UserData = "permissions" UserData = "permissions"
}; };
@@ -1707,7 +1707,7 @@ namespace Barotrauma.Networking
infoButton.UserData = newSub; infoButton.UserData = newSub;
infoButton.OnClicked = (component, userdata) => infoButton.OnClicked = (component, userdata) =>
{ {
((Submarine)userdata).CreatePreviewWindow(new GUIMessageBox("", "", new Vector2(0.25f, 0.25f), new Point(500, 400))); ((Submarine)userdata).CreatePreviewWindow(new GUIMessageBox("", "", 550, 400));
return true; return true;
}; };
} }
@@ -2401,7 +2401,7 @@ namespace Barotrauma.Networking
{ {
var banReasonPrompt = new GUIMessageBox( var banReasonPrompt = new GUIMessageBox(
TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"),
"", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.2f), new Point(400, 200)); "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, 400, 300);
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center));
var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform))
@@ -254,6 +254,8 @@ namespace Barotrauma
private GUILayoutGroup subPreviewContainer; private GUILayoutGroup subPreviewContainer;
private GUILayoutGroup subPreviewContainer;
private GUIButton loadGameButton; private GUIButton loadGameButton;
public Action<Submarine, string, string> StartNewGame; public Action<Submarine, string, string> StartNewGame;
@@ -2043,13 +2043,12 @@ namespace Barotrauma
GUI.AddMessage(GetCharacterEditorTranslation("RagdollReset"), Color.WhiteSmoke, font: GUI.Font); GUI.AddMessage(GetCharacterEditorTranslation("RagdollReset"), Color.WhiteSmoke, font: GUI.Font);
return true; return true;
}; };
Vector2 messageBoxRelSize = new Vector2(0.5f, 0.5f);
int messageBoxWidth = GameMain.GraphicsWidth / 2; int messageBoxWidth = GameMain.GraphicsWidth / 2;
int messageBoxHeight = GameMain.GraphicsHeight / 2; int messageBoxHeight = GameMain.GraphicsHeight / 2;
var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll")); var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll"));
saveRagdollButton.OnClicked += (button, userData) => saveRagdollButton.OnClicked += (button, userData) =>
{ {
var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxWidth, messageBoxHeight);
var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, 30), box.Content.RectTransform, Anchor.Center), RagdollParams.Name); var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, 30), box.Content.RectTransform, Anchor.Center), RagdollParams.Name);
box.Buttons[0].OnClicked += (b, d) => box.Buttons[0].OnClicked += (b, d) =>
{ {
@@ -2077,7 +2076,7 @@ namespace Barotrauma
var loadRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadRagdoll")); var loadRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadRagdoll"));
loadRagdollButton.OnClicked += (button, userData) => loadRagdollButton.OnClicked += (button, userData) =>
{ {
var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxWidth, messageBoxHeight);
loadBox.Buttons[0].OnClicked += loadBox.Close; loadBox.Buttons[0].OnClicked += loadBox.Close;
var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)); var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter));
var deleteButton = loadBox.Buttons[2]; var deleteButton = loadBox.Buttons[2];
@@ -2123,7 +2122,7 @@ namespace Barotrauma
var msgBox = new GUIMessageBox( var msgBox = new GUIMessageBox(
TextManager.Get("DeleteDialogLabel"), TextManager.Get("DeleteDialogLabel"),
TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile), TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile),
new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }, messageBoxWidth - 100, messageBoxHeight - 100);
msgBox.Buttons[0].OnClicked += (b, d) => msgBox.Buttons[0].OnClicked += (b, d) =>
{ {
try try
@@ -2164,7 +2163,7 @@ namespace Barotrauma
var saveAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveAnimation")); var saveAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveAnimation"));
saveAnimationButton.OnClicked += (button, userData) => saveAnimationButton.OnClicked += (button, userData) =>
{ {
var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxWidth, messageBoxHeight);
var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null); var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null);
var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: ");
var inputField = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), textArea.RectTransform, Anchor.TopRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); var inputField = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), textArea.RectTransform, Anchor.TopRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name);
@@ -2212,7 +2211,7 @@ namespace Barotrauma
var loadAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadAnimation")); var loadAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadAnimation"));
loadAnimationButton.OnClicked += (button, userData) => loadAnimationButton.OnClicked += (button, userData) =>
{ {
var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxWidth, messageBoxHeight);
loadBox.Buttons[0].OnClicked += loadBox.Close; loadBox.Buttons[0].OnClicked += loadBox.Close;
var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)); var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform));
var deleteButton = loadBox.Buttons[2]; var deleteButton = loadBox.Buttons[2];
@@ -2274,7 +2273,7 @@ namespace Barotrauma
var msgBox = new GUIMessageBox( var msgBox = new GUIMessageBox(
TextManager.Get("DeleteDialogLabel"), TextManager.Get("DeleteDialogLabel"),
TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile), TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile),
new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }, messageBoxWidth - 100, messageBoxHeight - 100);
msgBox.Buttons[0].OnClicked += (b, d) => msgBox.Buttons[0].OnClicked += (b, d) =>
{ {
try try
@@ -4426,7 +4425,7 @@ namespace Barotrauma
protected override GUIMessageBox Create() protected override GUIMessageBox Create()
{ {
var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Next") }, new Vector2(0.5f, 1.0f)); var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Next") }, GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight);
box.Content.ChildAnchor = Anchor.TopCenter; box.Content.ChildAnchor = Anchor.TopCenter;
box.Content.AbsoluteSpacing = 20; box.Content.AbsoluteSpacing = 20;
int elementSize = 30; int elementSize = 30;
@@ -4542,7 +4541,7 @@ namespace Barotrauma
protected override GUIMessageBox Create() protected override GUIMessageBox Create()
{ {
var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new string[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.5f, 1.0f)); var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new string[] { TextManager.Get("Previous"), TextManager.Get("Create") }, GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight);
box.Content.ChildAnchor = Anchor.TopCenter; box.Content.ChildAnchor = Anchor.TopCenter;
box.Content.AbsoluteSpacing = 20; box.Content.AbsoluteSpacing = 20;
int elementSize = 30; int elementSize = 30;
@@ -4622,7 +4621,7 @@ namespace Barotrauma
{ {
if (htmlBox == null) if (htmlBox == null)
{ {
htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new string[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.5f, 1.0f)); htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new string[] { TextManager.Get("Close"), TextManager.Get("Load") }, GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight);
var element = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); var element = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f);
new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath"));
var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), element.RectTransform, Anchor.TopRight), $"Content/Characters/{Name}/{Name}.html"); var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), element.RectTransform, Anchor.TopRight), $"Content/Characters/{Name}/{Name}.html");
@@ -602,7 +602,7 @@ namespace Barotrauma
public GUIMessageBox Create() public GUIMessageBox Create()
{ {
var box = new GUIMessageBox(TextManager.Get("LevelEditorCreateLevelObj"), string.Empty, var box = new GUIMessageBox(TextManager.Get("LevelEditorCreateLevelObj"), string.Empty,
new string[] { TextManager.Get("Cancel"), TextManager.Get("Done") }, new Vector2(0.5f, 0.8f)); new string[] { TextManager.Get("Cancel"), TextManager.Get("Done") }, GameMain.GraphicsWidth / 2, (int)(GameMain.GraphicsHeight * 0.8f));
box.Content.ChildAnchor = Anchor.TopCenter; box.Content.ChildAnchor = Anchor.TopCenter;
box.Content.AbsoluteSpacing = 20; box.Content.AbsoluteSpacing = 20;
@@ -1212,7 +1212,7 @@ namespace Barotrauma
}; };
infoButton.OnClicked += (component, userdata) => infoButton.OnClicked += (component, userdata) =>
{ {
((Submarine)userdata).CreatePreviewWindow(new GUIMessageBox("", "", new Vector2(0.25f, 0.25f), new Point(500, 400))); ((Submarine)userdata).CreatePreviewWindow(new GUIMessageBox("", "", 550, 600));
return true; return true;
}; };
} }
@@ -2005,8 +2005,7 @@ namespace Barotrauma
return false; return false;
} }
var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }, 400, 300)
new string[] { TextManager.Get("Yes"), TextManager.Get("No") })
{ {
UserData = "request" + subName UserData = "request" + subName
}; };
@@ -906,7 +906,7 @@ namespace Barotrauma
public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox)
{ {
var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, width: 300, height: 400);
msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked = msgBox.Close;
var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter))
@@ -36,6 +36,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveLoop.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveLoop.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveManager.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveOperateItem.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveOperateItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveFightIntruders.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectivePumpWater.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectivePumpWater.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRepairItem.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRepairItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRepairItems.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRepairItems.cs" />
@@ -527,15 +527,6 @@
<Content Include="$(MSBuildThisFileDirectory)Content\NPCConversations\NpcConversations_TraditionalChinese.xml"> <Content Include="$(MSBuildThisFileDirectory)Content\NPCConversations\NpcConversations_TraditionalChinese.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Splash_Daedalic.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Splash_FF.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Splash_UTG.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Texts\BrazilianPortugueseVanilla.xml"> <Content Include="$(MSBuildThisFileDirectory)Content\Texts\BrazilianPortugueseVanilla.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@@ -1640,6 +1631,9 @@
<Content Include="$(MSBuildThisFileDirectory)Content\Particles\UnderwaterExplosionSheet.png"> <Content Include="$(MSBuildThisFileDirectory)Content\Particles\UnderwaterExplosionSheet.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="$(MSBuildThisFileDirectory)Content\splashscreen.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Texts\EnglishVanilla.xml"> <Content Include="$(MSBuildThisFileDirectory)Content\Texts\EnglishVanilla.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@@ -3152,6 +3146,9 @@
<None Include="$(MSBuildThisFileDirectory)Content\Sounds\PickItemFail.ogg"> <None Include="$(MSBuildThisFileDirectory)Content\Sounds\PickItemFail.ogg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="$(MSBuildThisFileDirectory)Content\Sounds\StartDrone.ogg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)Content\Sounds\UI\ChatMsg.ogg"> <None Include="$(MSBuildThisFileDirectory)Content\Sounds\UI\ChatMsg.ogg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
@@ -3,6 +3,8 @@ using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma namespace Barotrauma
{ {
@@ -50,7 +52,6 @@ namespace Barotrauma
{ {
insideSteering = new IndoorsSteeringManager(this, true, false); insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this); outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c); objectiveManager = new AIObjectiveManager(c);
updateObjectiveTimer = Rand.Range(0.0f, UpdateObjectiveInterval); updateObjectiveTimer = Rand.Range(0.0f, UpdateObjectiveInterval);
InitProjSpecific(); InitProjSpecific();
@@ -89,23 +90,18 @@ namespace Barotrauma
if (Character.SpeechImpediment < 100.0f) if (Character.SpeechImpediment < 100.0f)
{ {
// TODO: add a timer -> once per second is enough?
ReportProblems(); ReportProblems();
UpdateSpeaking(); UpdateSpeaking();
} }
objectiveManager.DoCurrentObjective(deltaTime); if (objectiveManager.CurrentObjective == null) { return; }
bool run = objectiveManager.GetCurrentPriority() > AIObjectiveManager.OrderPriority; objectiveManager.DoCurrentObjective(deltaTime);
bool run = objectiveManager.CurrentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
{ {
if (Vector2.DistanceSquared(Character.SimPosition, goTo.Target.SimPosition) > 3 * 3) run = Vector2.DistanceSquared(Character.SimPosition, goTo.Target.SimPosition) > 3 * 3;
{
run = true;
}
}
if (!run)
{
run = objectiveManager.CurrentObjective.ForceRun;
} }
if (run) if (run)
{ {
@@ -162,7 +158,7 @@ namespace Barotrauma
{ {
bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool highPressure = Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0 && Character.PressureProtection <= 0; bool highPressure = Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0 && Character.PressureProtection <= 0;
bool shouldKeepTheGearOn = objectiveManager.CurrentObjective.KeepDivingGearOn; bool shouldKeepTheGearOn = !ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>();
bool removeDivingSuit = (oxygenLow && !highPressure) || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 1 && !Character.IsClimbing && steeringManager == insideSteering && !PathSteering.InStairs); bool removeDivingSuit = (oxygenLow && !highPressure) || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 1 && !Character.IsClimbing && steeringManager == insideSteering && !PathSteering.InStairs);
if (removeDivingSuit) if (removeDivingSuit)
@@ -188,7 +184,7 @@ namespace Barotrauma
} }
} }
} }
if (!(ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires) && !(ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire)) if (!ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() && !ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFire>())
{ {
var extinguisherItem = Character.Inventory.FindItemByIdentifier("extinguisher") ?? Character.Inventory.FindItemByTag("extinguisher"); var extinguisherItem = Character.Inventory.FindItemByIdentifier("extinguisher") ?? Character.Inventory.FindItemByTag("extinguisher");
if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem)) if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem))
@@ -197,6 +193,21 @@ namespace Barotrauma
extinguisherItem.Drop(Character); extinguisherItem.Drop(Character);
} }
} }
foreach (var item in Character.Inventory.Items)
{
if (item == null) { continue; }
if (ObjectiveManager.CurrentObjective is AIObjectiveIdle)
{
if (item.AllowedSlots.Contains(InvSlotType.RightHand | InvSlotType.LeftHand) && Character.HasEquippedItem(item))
{
// Try to put the weapon in an Any slot, and drop it if that fails
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
item.Drop(Character);
}
}
}
}
if (Character.IsKeyDown(InputType.Aim)) if (Character.IsKeyDown(InputType.Aim))
{ {
@@ -229,41 +240,59 @@ namespace Barotrauma
Order newOrder = null; Order newOrder = null;
if (Character.CurrentHull != null) if (Character.CurrentHull != null)
{ {
if (Character.CurrentHull.FireSources.Count > 0) if (AIObjectiveExtinguishFires.IsValidTarget(Character.CurrentHull, Character))
{ {
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire"); var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire");
newOrder = new Order(orderPrefab, Character.CurrentHull, null); newOrder = new Order(orderPrefab, Character.CurrentHull, null);
AddTargets<AIObjectiveExtinguishFires, Hull>(Character, Character.CurrentHull);
} }
if (Character.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor == null && g.Open > 0.0f)) foreach (var gap in Character.CurrentHull.ConnectedGaps)
{ {
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach"); if (AIObjectiveFixLeaks.IsValidTarget(gap, Character))
newOrder = new Order(orderPrefab, Character.CurrentHull, null); {
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap);
}
}
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != Character.CurrentHull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.Condition > r.ShowRepairUIThreshold)) { continue; }
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbrokendevices");
newOrder = new Order(orderPrefab, Character.CurrentHull, item.Repairables?.FirstOrDefault());
AddTargets<AIObjectiveRepairItems, Item>(Character, item);
}
} }
foreach (Character c in Character.CharacterList) foreach (Character c in Character.CharacterList)
{ {
if (c.CurrentHull == Character.CurrentHull && !c.IsDead && if (c.CurrentHull != Character.CurrentHull) { continue; }
(c.AIController is EnemyAIController || (c.TeamID != Character.TeamID && Character.TeamID != Character.TeamType.FriendlyNPC && c.TeamID != Character.TeamType.FriendlyNPC))) if (AIObjectiveFightIntruders.IsValidTarget(c, Character))
{ {
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders"); var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders");
newOrder = new Order(orderPrefab, Character.CurrentHull, null); newOrder = new Order(orderPrefab, Character.CurrentHull, null);
AddTargets<AIObjectiveFightIntruders, Character>(Character, c);
} }
} }
}
if (Character.CurrentHull != null && (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f)) if (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f)
{ {
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid"); var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid");
newOrder = new Order(orderPrefab, Character.CurrentHull, null); newOrder = new Order(orderPrefab, Character.CurrentHull, null);
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
}
} }
if (newOrder != null) if (newOrder != null)
{ {
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{ {
Character.Speak( Character.Speak(newOrder.GetChatMessage("", Character.CurrentHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
newOrder.GetChatMessage("", Character.CurrentHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
} }
} }
} }
@@ -290,6 +319,7 @@ namespace Barotrauma
{ {
float damage = attackResult.Damage; float damage = attackResult.Damage;
if (damage <= 0) { return; } if (damage <= 0) { return; }
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker == null || attacker.IsDead || attacker.Removed) if (attacker == null || attacker.IsDead || attacker.Removed)
{ {
// Ignore damage from falling etc that we shouldn't react to. // Ignore damage from falling etc that we shouldn't react to.
@@ -337,18 +367,18 @@ namespace Barotrauma
{ {
// Replace the old objective with the new. // Replace the old objective with the new.
ObjectiveManager.Objectives.Remove(combatObjective); ObjectiveManager.Objectives.Remove(combatObjective);
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode)); objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager));
} }
} }
else else
{ {
if (delay > 0) if (delay > 0)
{ {
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode), delay); objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager), delay);
} }
else else
{ {
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode)); objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager));
} }
} }
} }
@@ -359,8 +389,10 @@ namespace Barotrauma
CurrentOrderOption = option; CurrentOrderOption = option;
CurrentOrder = order; CurrentOrder = order;
objectiveManager.SetOrder(order, option, orderGiver); objectiveManager.SetOrder(order, option, orderGiver);
if (speak && Character.SpeechImpediment < 100.0f) Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f); if (speak && Character.SpeechImpediment < 100.0f)
{
Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
}
SetOrderProjSpecific(order); SetOrderProjSpecific(order);
} }
@@ -397,7 +429,7 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen. /// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary> /// </summary>
public static bool HasDivingGear(Character character) => HasItem(character, "diving", "oxygensource"); public static bool HasDivingMask(Character character) => HasItem(character, "diving", "oxygensource");
public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0) public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0)
{ {
@@ -417,12 +449,9 @@ namespace Barotrauma
{ {
foreach (var c in Character.CharacterList) foreach (var c in Character.CharacterList)
{ {
if (c.TeamID == character.TeamID) if (c.AIController is HumanAIController humanAi && humanAi.IsFriendly(character))
{ {
if (c.AIController is HumanAIController humanAi) humanAi.RefreshHullSafety(hull);
{
humanAi.RefreshHullSafety(hull);
}
} }
} }
} }
@@ -439,13 +468,79 @@ namespace Barotrauma
} }
} }
public static void RefreshTargets(Character character, Order order, Hull hull)
{
switch (order.AITag)
{
case "reportfire":
AddTargets<AIObjectiveExtinguishFires, Hull>(character, hull);
break;
case "reportbreach":
foreach (var gap in hull.ConnectedGaps)
{
if (AIObjectiveFixLeaks.IsValidTarget(gap, character))
{
AddTargets<AIObjectiveFixLeaks, Gap>(character, gap);
}
}
break;
case "reportbrokendevices":
foreach (var item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.Condition > r.ShowRepairUIThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
break;
case "reportintruders":
foreach (var enemy in Character.CharacterList)
{
if (enemy.CurrentHull != hull) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character))
{
AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
}
}
break;
case "requestfirstaid":
foreach (var c in Character.CharacterList)
{
if (c.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(c, character))
{
AddTargets<AIObjectiveRescueAll, Character>(character, c);
}
}
break;
default:
#if DEBUG
DebugConsole.ThrowError(order.AITag + " not implemented!");
#endif
break;
}
}
private static void AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
foreach (var c in Character.CharacterList)
{
if (IsFriendly(caller, c) && c.AIController is HumanAIController humanAI)
{
humanAI.ObjectiveManager.GetObjective<T1>()?.AddTarget(target);
}
}
}
public float GetHullSafety(Hull hull) public float GetHullSafety(Hull hull)
{ {
if (hull == null) { return 0; } if (hull == null) { return 0; }
bool ignoreFire = ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire || ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires; bool ignoreFire = ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() || ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(Character); bool ignoreWater = HasDivingSuit(Character);
bool ignoreOxygen = ignoreWater || HasDivingGear(Character); bool ignoreOxygen = ignoreWater || HasDivingMask(Character);
bool ignoreEnemies = ObjectiveManager.CurrentObjective is AIObjectiveCombat || ObjectiveManager.CurrentOrder is AIObjectiveCombat; bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
return GetHullSafety(hull, Character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); return GetHullSafety(hull, Character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
} }
@@ -463,7 +558,7 @@ namespace Barotrauma
// Even the smallest fire reduces the safety by 50% // Even the smallest fire reduces the safety by 50%
float fire = hull.FireSources.Count * 0.5f + hull.FireSources.Sum(fs => fs.DamageRange) / hull.Size.X; float fire = hull.FireSources.Count * 0.5f + hull.FireSources.Sum(fs => fs.DamageRange) / hull.Size.X;
float fireFactor = ignoreFire ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1)); float fireFactor = ignoreFire ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
int enemyCount = Character.CharacterList.Count(e => int enemyCount = Character.CharacterList.Count(e =>
e.CurrentHull == hull && !e.IsDead && !e.IsUnconscious && e.CurrentHull == hull && !e.IsDead && !e.IsUnconscious &&
(e.AIController is EnemyAIController || (e.TeamID != character.TeamID && character.TeamID != Character.TeamType.FriendlyNPC && e.TeamID != Character.TeamType.FriendlyNPC))); (e.AIController is EnemyAIController || (e.TeamID != character.TeamID && character.TeamID != Character.TeamType.FriendlyNPC && e.TeamID != Character.TeamType.FriendlyNPC)));
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages) // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
@@ -472,7 +567,8 @@ namespace Barotrauma
return MathHelper.Clamp(safety * 100, 0, 100); return MathHelper.Clamp(safety * 100, 0, 100);
} }
public bool IsFriendly(Character other) => IsFriendly(Character, other);
// TODO: If the aliens are quaranteed to be in another team than the player, we wouldn't need to check the species. // TODO: If the aliens are quaranteed to be in another team than the player, we wouldn't need to check the species.
public bool IsFriendly(Character other) => other.TeamID == Character.TeamID && other.SpeciesName == Character.SpeciesName; public static bool IsFriendly(Character me, Character other) => other.TeamID == me.TeamID && other.SpeciesName == me.SpeciesName;
} }
} }
@@ -287,7 +287,8 @@ namespace Barotrauma
character.AnimController.Anim = AnimController.Animation.None; character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null; character.SelectedConstruction = null;
} }
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(collider.radius * 3, 2)) float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(collider.radius * 2 * multiplier, 2))
{ {
currentPath.SkipToNextNode(); currentPath.SkipToNextNode();
} }
@@ -296,19 +297,62 @@ namespace Barotrauma
{ {
Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize(); Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character // Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = colliderSize.Y + character.AnimController.ColliderHeightFromFloor; float characterHeight = colliderSize.Y + character.AnimController.ColliderHeightFromFloor;
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
if (InStairs)
if (horizontalDistance < collider.radius * 3 && isAboveFeet && isNotTooHigh)
{ {
currentPath.SkipToNextNode(); float multiplierX = MathHelper.Lerp(1, 10, MathHelper.Clamp(velocity.X / 10, 0, 1));
float multiplierY = MathHelper.Lerp(1, 10, MathHelper.Clamp(velocity.Y / 10, 0, 1));
float verticalDistance = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float targetDistX = collider.radius * multiplierX;
float targetDistY = collider.radius * multiplierY;
if (horizontalDistance < targetDistX && verticalDistance < targetDistY && isAboveFeet && isNotTooHigh)
{
currentPath.SkipToNextNode();
}
} }
else
{
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(velocity.X / 10, 0, 1));
float targetDistance = collider.radius * margin;
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh)
{
currentPath.SkipToNextNode();
}
}
}
if (currentPath.CurrentNode == null) return Vector2.Zero;
return currentPath.CurrentNode.SimPosition - pos;
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen) { return true; }
if (canBreakDoors) { return true; }
if (door.IsStuck) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (door.HasIntegratedButtons)
{
return door.HasRequiredItems(character, false);
} }
else else
{ {
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasRequiredItems(character, false) && (buttonFilter == null || buttonFilter(b)));
}
}
private void CheckDoorsInPath()
{
for (int i = 0; i < 2; i++)
{
WayPoint currentWaypoint = null;
WayPoint nextWaypoint = null;
Door door = null; Door door = null;
bool shouldBeOpen = false; bool shouldBeOpen = false;
@@ -319,21 +363,19 @@ namespace Barotrauma
} }
else else
{ {
WayPoint node = null;
WayPoint nextNode = null;
if (i == 0) if (i == 0)
{ {
node = currentPath.CurrentNode; currentWaypoint = currentPath.CurrentNode;
nextNode = currentPath.NextNode; nextWaypoint = currentPath.NextNode;
} }
else else
{ {
node = currentPath.PrevNode; currentWaypoint = currentPath.PrevNode;
nextNode = currentPath.CurrentNode; nextWaypoint = currentPath.CurrentNode;
} }
if (node?.ConnectedDoor == null) continue; if (currentWaypoint?.ConnectedDoor == null) { continue; }
if (nextNode == null) if (nextWaypoint == null)
{ {
//the node we're heading towards is the last one in the path, and at a door //the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node //the door needs to be open for the character to reach the node
@@ -341,21 +383,21 @@ namespace Barotrauma
} }
else else
{ {
door = node.ConnectedGap.ConnectedDoor; door = currentWaypoint.ConnectedGap.ConnectedDoor;
if (door.LinkedGap.IsHorizontal) if (door.LinkedGap.IsHorizontal)
{ {
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X); int currentDir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f; shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
} }
else else
{ {
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y); int currentDir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f; shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f;
} }
} }
} }
if (door == null) return; if (door == null) { return; }
//toggle the door if it's the previous node and open, or if it's current node and closed //toggle the door if it's the previous node and open, or if it's current node and closed
if (door.IsOpen != shouldBeOpen) if (door.IsOpen != shouldBeOpen)
@@ -381,70 +423,29 @@ namespace Barotrauma
buttonPressCooldown = ButtonPressInterval; buttonPressCooldown = ButtonPressInterval;
break; break;
} }
else if (closestButton != null)
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
if (!door.HasRequiredItems(character, false) && shouldBeOpen)
{ {
currentPath.Unreachable = true; if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2))
return; {
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
// Can't reach the button closest to the door.
// It's possible that we could reach another buttons.
// If this becomes an issue, we could go through them here and check if any of them are reachable
// (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
currentPath.Unreachable = true;
return;
}
} }
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
} }
else else if (shouldBeOpen)
{ {
if (!door.HasRequiredItems(character, false) && shouldBeOpen) currentPath.Unreachable = true;
{ return;
currentPath.Unreachable = true;
return;
}
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
if (!door.HasRequiredItems(character, false) && shouldBeOpen)
{
currentPath.Unreachable = true;
return;
}
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
if (!door.HasRequiredItems(character, false) && shouldBeOpen)
{
currentPath.Unreachable = true;
return;
}
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
if (!door.HasRequiredItems(character, false) && shouldBeOpen)
{
currentPath.Unreachable = true;
return;
}
door.Item.TryInteract(character, false, true, true);
buttonPressCooldown = ButtonPressInterval;
break;
} }
} }
} }
@@ -453,31 +454,37 @@ namespace Barotrauma
private float? GetNodePenalty(PathNode node, PathNode nextNode) private float? GetNodePenalty(PathNode node, PathNode nextNode)
{ {
if (character == null) { return 0.0f; } if (character == null) { return 0.0f; }
float penalty = 0.0f; float penalty = 0.0f;
if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f) if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f)
{ {
if (nextNode.Waypoint.ConnectedDoor == null) var door = nextNode.Waypoint.ConnectedDoor;
if (door == null)
{ {
penalty = 100.0f; penalty = 100.0f;
} }
else if (!canBreakDoors) else
{ {
//door closed and the character can't open doors -> node can't be traversed if (!CanAccessDoor(door, button =>
if (!canOpenDoors || character.LockHands) { return null; } {
// Ignore buttons that are on the wrong side of the door
var doorButtons = nextNode.Waypoint.ConnectedDoor.Item.GetConnectedComponents<Controller>(); if (door.IsHorizontal)
if (!doorButtons.Any()) {
if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
{
return false;
}
}
else
{
if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
{
return false;
}
}
return true;
}))
{ {
if (!nextNode.Waypoint.ConnectedDoor.HasRequiredItems(character, false)) { return null; } return null;
}
foreach (Controller button in doorButtons)
{
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
Math.Sign(node.Position.X - nextNode.Position.X)) { continue; }
if (!button.HasRequiredItems(character, false)) { return null; }
} }
} }
} }
@@ -12,37 +12,6 @@ namespace Barotrauma
public abstract string DebugTag { get; } public abstract string DebugTag { get; }
public virtual bool ForceRun => false; public virtual bool ForceRun => false;
public virtual bool KeepDivingGearOn => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
public float Priority { get; set; }
protected readonly Character character;
protected string option;
protected bool abandon;
public virtual bool CanBeCompleted => !abandon && subObjectives.All(so => so.CanBeCompleted);
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective { get; private set; }
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
/// <summary> /// <summary>
/// Run the main objective with all subobjectives concurrently? /// Run the main objective with all subobjectives concurrently?
@@ -78,6 +47,7 @@ namespace Barotrauma
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null) public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
{ {
this.objectiveManager = objectiveManager;
this.character = character; this.character = character;
Option = option ?? string.Empty; Option = option ?? string.Empty;
@@ -110,18 +80,6 @@ namespace Barotrauma
#endif #endif
subObjectives.Remove(subObjective); subObjectives.Remove(subObjective);
} }
else if (subObjective.ShouldInterruptSubObjective(subObjective))
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is interrupted.");
#endif
subObjectives.Remove(subObjective);
}
}
if (!subObjectives.Contains(CurrentSubObjective))
{
CurrentSubObjective = null;
} }
foreach (AIObjective objective in subObjectives) foreach (AIObjective objective in subObjectives)
@@ -148,29 +106,40 @@ namespace Barotrauma
subObjectives.Add(objective); subObjectives.Add(objective);
} }
public void SortSubObjectives(AIObjectiveManager objectiveManager) public void SortSubObjectives()
{ {
if (subObjectives.None()) { return; } if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager))); subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
CurrentSubObjective = SubObjectives.First(); if (ConcurrentObjectives)
CurrentSubObjective.SortSubObjectives(objectiveManager); {
subObjectives.ForEach(so => so.SortSubObjectives());
}
else
{
subObjectives.First().SortSubObjectives();
}
} }
public virtual float GetPriority(AIObjectiveManager objectiveManager) => Priority; public virtual float GetPriority() => Priority * PriorityModifier;
public virtual void Update(AIObjectiveManager objectiveManager, float deltaTime) public virtual void Update(float deltaTime)
{ {
var subObjective = objectiveManager.CurrentObjective?.CurrentSubObjective;
if (objectiveManager.CurrentOrder == this) if (objectiveManager.CurrentOrder == this)
{ {
Priority = AIObjectiveManager.OrderPriority; Priority = AIObjectiveManager.OrderPriority;
} }
else if (objectiveManager.CurrentObjective == this || subObjective == this) else if (objectiveManager.WaitTimer <= 0)
{ {
Priority += Devotion * deltaTime; if (objectiveManager.CurrentObjective != null)
{
if (objectiveManager.CurrentObjective == this || objectiveManager.CurrentObjective.subObjectives.Any(so => so == this))
{
Priority += Devotion * PriorityModifier * deltaTime;
}
}
Priority = MathHelper.Clamp(Priority, 0, 100);
subObjectives.ForEach(so => so.Update(deltaTime));
} }
Priority = MathHelper.Clamp(Priority, 0, 100);
subObjectives.ForEach(so => so.Update(objectiveManager, deltaTime));
} }
/// <summary> /// <summary>
@@ -190,13 +159,54 @@ namespace Barotrauma
} }
} }
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective) => false; /// <summary>
public virtual void OnSelected() { } /// Checks if the objective already is created and added in subobjectives. If not, creates it.
/// Handles objectives that cannot be completed. If the objective has been removed form the subobjectives, a null value is assigned to the reference.
/// Returns true if the objective was created.
/// </summary>
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onAbandon = null) where T : AIObjective
{
if (objective != null)
{
// Sub objective already found, no need to do anything if it remains in the subobjectives
// If the sub objective is removed -> it's either completed or impossible to complete.
if (!subObjectives.Contains(objective))
{
if (!objective.CanBeCompleted)
{
abandon = true;
onAbandon?.Invoke();
}
objective = null;
}
return false;
}
else
{
objective = constructor();
if (!subObjectives.Contains(objective))
{
AddSubObjective(objective);
}
return true;
}
}
public virtual void OnSelected()
{
// Should we reset steering here?
//if (!ConcurrentObjectives)
//{
// SteeringManager.Reset();
//}
}
public virtual void Reset() { } public virtual void Reset() { }
protected abstract void Act(float deltaTime); protected abstract void Act(float deltaTime);
public abstract bool IsCompleted(); public abstract bool IsCompleted();
public abstract bool IsDuplicate(AIObjective otherObjective); public abstract bool IsDuplicate(AIObjective otherObjective);
} }
} }
@@ -1,19 +1,19 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer> class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{ {
public override string DebugTag => "charge batteries"; public override string DebugTag => "charge batteries";
private readonly IEnumerable<PowerContainer> batteryList; private IEnumerable<PowerContainer> batteryList;
public AIObjectiveChargeBatteries(Character character, string option) : base(character, option) public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
{ : base(character, objectiveManager, priorityModifier, option) { }
batteryList = Item.ItemList.Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
}
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
@@ -22,37 +22,57 @@ namespace Barotrauma
protected override void FindTargets() protected override void FindTargets()
{ {
foreach (Item item in Item.ItemList) base.FindTargets();
{ if (targets.None() && objectiveManager.CurrentOrder == this)
if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) { continue; }
if (item.Submarine == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
var battery = item.GetComponent<PowerContainer>();
if (battery != null)
{
if (!ignoreList.Contains(battery))
{
if (!targets.Contains(battery))
{
targets.Add(battery);
}
}
}
}
if (targets.None())
{ {
character.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f); character.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
} }
}
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
var item = battery.Item;
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.ConditionPercentage <= 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Option == "charge")
{
if (battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio - 0.01f) { return false; }
}
else else
{ {
targets.Sort((x, y) => x.ChargePercentage.CompareTo(y.ChargePercentage)); if (battery.RechargeRatio <= 0) { return false; }
}
return true;
}
protected override float TargetEvaluation()
{
if (Option == "charge")
{
return targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio)));
}
else
{
return targets.Max(t => MathHelper.Lerp(0, 100, t.RechargeRatio));
} }
} }
protected override bool Filter(PowerContainer battery) => true; protected override IEnumerable<PowerContainer> GetList()
protected override float Average(PowerContainer battery) => 100 - battery.ChargePercentage; {
protected override IEnumerable<PowerContainer> GetList() => batteryList; if (batteryList == null)
protected override AIObjective ObjectiveConstructor(PowerContainer battery) => new AIObjectiveOperateItem(battery, character, Option, false); {
batteryList = Item.ItemList.Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
}
return batteryList;
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery)
=> new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false };
} }
} }
@@ -1,15 +1,17 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveCombat : AIObjective class AIObjectiveCombat : AIObjective
{ {
public override string DebugTag => "combat"; public override string DebugTag => "combat";
public override bool KeepDivingGearOn => true; public bool useCoolDown = true;
const float CoolDown = 10.0f; const float CoolDown = 10.0f;
@@ -41,29 +43,39 @@ namespace Barotrauma
return _weaponComponent; return _weaponComponent;
} }
} }
private AIObjectiveContainItem reloadWeaponObjective;
private Hull retreatTarget;
private AIObjectiveGoTo retreatObjective;
private AIObjectiveFindSafety findSafety;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<RangedWeapon> rangedWeapons = new HashSet<RangedWeapon>();
private readonly HashSet<MeleeWeapon> meleeWeapons = new HashSet<MeleeWeapon>();
private readonly HashSet<Item> adHocWeapons = new HashSet<Item>();
private AIObjectiveContainItem reloadWeaponObjective;
private AIObjectiveGoTo retreatObjective;
private AIObjectiveGoTo followTargetObjective;
private Hull retreatTarget;
private float coolDownTimer; private float coolDownTimer;
public enum CombatMode public enum CombatMode
{ {
Defensive, Defensive,
Offensive, // Not implemented Offensive,
Retreat Retreat
} }
public CombatMode Mode { get; private set; } public CombatMode Mode { get; private set; }
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode) : base(character, "") public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{ {
Enemy = enemy; Enemy = enemy;
coolDownTimer = CoolDown; coolDownTimer = CoolDown;
findSafety = HumanAIController.ObjectiveManager.GetObjective<AIObjectiveFindSafety>(); findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
findSafety.Priority = 0; if (findSafety != null)
findSafety.unreachable.Clear(); {
findSafety.Priority = 0;
findSafety.unreachable.Clear();
}
Mode = mode; Mode = mode;
if (Enemy == null) if (Enemy == null)
{ {
@@ -73,12 +85,22 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
coolDownTimer -= deltaTime; if (useCoolDown)
{
coolDownTimer -= deltaTime;
}
if (abandon) { return; } if (abandon) { return; }
Arm(deltaTime);
Move(deltaTime);
}
private void Arm(float deltaTime)
{
switch (Mode) switch (Mode)
{ {
case CombatMode.Offensive:
case CombatMode.Defensive: case CombatMode.Defensive:
if (Weapon != null && character.Inventory.Items.Contains(_weapon)) if (Weapon != null && !character.Inventory.Items.Contains(_weapon) || _weaponComponent != null && !_weaponComponent.HasRequiredContainedItems(false))
{ {
Weapon = null; Weapon = null;
} }
@@ -90,40 +112,65 @@ namespace Barotrauma
{ {
Mode = CombatMode.Retreat; Mode = CombatMode.Retreat;
} }
else if (Equip(deltaTime)) if (Equip())
{ {
if (Reload(deltaTime)) if (Reload(deltaTime))
{ {
Attack(deltaTime); Attack(deltaTime);
} }
} }
// When defensive, try to retreat to safety. TODO: in offsensive mode, engage the target
Retreat(deltaTime);
break; break;
case CombatMode.Retreat:
break;
default:
throw new NotImplementedException();
}
}
private void Move(float deltaTime)
{
switch (Mode)
{
case CombatMode.Offensive:
Engage(deltaTime);
break;
case CombatMode.Defensive:
case CombatMode.Retreat: case CombatMode.Retreat:
Retreat(deltaTime); Retreat(deltaTime);
break; break;
case CombatMode.Offensive:
default: default:
throw new System.NotImplementedException(); throw new NotImplementedException();
} }
} }
private Item GetWeapon() private Item GetWeapon()
{ {
rangedWeapons.Clear();
meleeWeapons.Clear();
adHocWeapons.Clear();
Item weapon = null;
_weaponComponent = null; _weaponComponent = null;
var weapon = character.Inventory.FindItemByTag("weapon"); foreach (var item in character.Inventory.Items)
if (weapon == null)
{ {
foreach (var item in character.Inventory.Items) if (item == null) { continue; }
foreach (var component in item.Components)
{ {
if (item == null) { continue; } if (component is RangedWeapon rw)
foreach (var component in item.Components)
{ {
if (component is MeleeWeapon || component is RangedWeapon) if (rw.HasRequiredContainedItems(false))
{ {
return item; rangedWeapons.Add(rw);
} }
}
else if (component is MeleeWeapon mw)
{
if (mw.HasRequiredContainedItems(false))
{
meleeWeapons.Add(mw);
}
}
else
{
var effects = component.statusEffectLists; var effects = component.statusEffectLists;
if (effects != null) if (effects != null)
{ {
@@ -133,7 +180,10 @@ namespace Barotrauma
{ {
if (statusEffect.Afflictions.Any()) if (statusEffect.Afflictions.Any())
{ {
return item; if (component.HasRequiredContainedItems(false))
{
adHocWeapons.Add(item);
}
} }
} }
} }
@@ -141,6 +191,20 @@ namespace Barotrauma
} }
} }
} }
var rangedWeapon = rangedWeapons.OrderByDescending(w => w.CombatPriority).FirstOrDefault();
var meleeWeapon = meleeWeapons.OrderByDescending(w => w.CombatPriority).FirstOrDefault();
if (rangedWeapon != null)
{
weapon = rangedWeapon.Item;
}
else if (meleeWeapon != null)
{
weapon = meleeWeapon.Item;
}
if (weapon == null)
{
weapon = adHocWeapons.GetRandom(Rand.RandSync.Server);
}
return weapon; return weapon;
} }
@@ -155,7 +219,7 @@ namespace Barotrauma
} }
} }
private bool Equip(float deltaTime) private bool Equip()
{ {
if (!character.SelectedItems.Contains(Weapon)) if (!character.SelectedItems.Contains(Weapon))
{ {
@@ -166,8 +230,7 @@ namespace Barotrauma
} }
else else
{ {
//couldn't equip the item, escape Mode = CombatMode.Retreat;
//Abandon(deltaTime);
return false; return false;
} }
} }
@@ -176,6 +239,7 @@ namespace Barotrauma
private void Retreat(float deltaTime) private void Retreat(float deltaTime)
{ {
followTargetObjective = null;
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{ {
retreatTarget = findSafety.FindBestHull(new List<Hull>() { character.CurrentHull }); retreatTarget = findSafety.FindBestHull(new List<Hull>() { character.CurrentHull });
@@ -184,12 +248,45 @@ namespace Barotrauma
{ {
if (retreatObjective == null || retreatObjective.Target != retreatTarget) if (retreatObjective == null || retreatObjective.Target != retreatTarget)
{ {
retreatObjective = new AIObjectiveGoTo(retreatTarget, character, false, true); retreatObjective = new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true);
} }
retreatObjective.TryComplete(deltaTime); retreatObjective.TryComplete(deltaTime);
} }
} }
private void Engage(float deltaTime)
{
retreatTarget = null;
retreatObjective = null;
if (followTargetObjective == null)
{
followTargetObjective = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
{
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
CheckVisibility = true
};
}
if (WeaponComponent is RangedWeapon)
{
followTargetObjective.CloseEnough = 3;
}
else if (WeaponComponent is MeleeWeapon mw)
{
followTargetObjective.CloseEnough = ConvertUnits.ToSimUnits(mw.Range);
}
else if (WeaponComponent is RepairTool rt)
{
followTargetObjective.CloseEnough = ConvertUnits.ToSimUnits(rt.Range);
}
if (retreatTarget != null)
{
SteeringManager.Reset();
Mode = CombatMode.Retreat;
}
followTargetObjective.TryComplete(deltaTime);
}
private bool Reload(float deltaTime) private bool Reload(float deltaTime)
{ {
if (WeaponComponent != null && WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) if (WeaponComponent != null && WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
@@ -202,7 +299,7 @@ namespace Barotrauma
{ {
if (reloadWeaponObjective == null) if (reloadWeaponObjective == null)
{ {
reloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, Weapon.GetComponent<ItemContainer>()); reloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager);
} }
} }
} }
@@ -215,6 +312,7 @@ namespace Barotrauma
} }
else if (!reloadWeaponObjective.CanBeCompleted) else if (!reloadWeaponObjective.CanBeCompleted)
{ {
SteeringManager.Reset();
Mode = CombatMode.Retreat; Mode = CombatMode.Retreat;
} }
else else
@@ -229,14 +327,31 @@ namespace Barotrauma
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies; private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
private void Attack(float deltaTime) private void Attack(float deltaTime)
{ {
float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position);
character.CursorPosition = Enemy.Position; character.CursorPosition = Enemy.Position;
float engageDistance = 500;
if (squaredDistance > engageDistance * engageDistance) { return; }
bool canSeeTarget = character.CanSeeCharacter(Enemy);
if (!canSeeTarget && character.CurrentHull != Enemy.CurrentHull) { return; }
if (Weapon.RequireAimToUse) if (Weapon.RequireAimToUse)
{ {
character.SetInput(InputType.Aim, false, true); bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons && character.SelectedConstruction == null)
{
character.SetInput(InputType.Aim, false, true);
}
} }
if (WeaponComponent is MeleeWeapon meleeWeapon) if (WeaponComponent is MeleeWeapon meleeWeapon)
{ {
if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range) if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range)
{ {
character.SetInput(InputType.Shoot, false, true); character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character); Weapon.Use(deltaTime, character);
@@ -246,7 +361,7 @@ namespace Barotrauma
{ {
if (WeaponComponent is RepairTool repairTool) if (WeaponComponent is RepairTool repairTool)
{ {
if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; } if (squaredDistance > repairTool.Range * repairTool.Range) { return; }
} }
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - character.Position) < MathHelper.PiOver4) if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - character.Position) < MathHelper.PiOver4)
{ {
@@ -254,7 +369,8 @@ namespace Barotrauma
{ {
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
} }
var pickedBody = Submarine.PickBody(character.SimPosition, Enemy.SimPosition, myBodies); var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall;
var pickedBody = Submarine.PickBody(character.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null) if (pickedBody != null)
{ {
Character target = null; Character target = null;
@@ -276,17 +392,15 @@ namespace Barotrauma
} }
} }
private void Abandon(float deltaTime)
{
abandon = true;
SteeringManager.Reset();
}
public override bool IsCompleted() public override bool IsCompleted()
{ {
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || coolDownTimer <= 0; bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (useCoolDown && coolDownTimer <= 0);
if (completed) if (completed)
{ {
if (objectiveManager.CurrentOrder == this && Enemy != null && Enemy.IsDead)
{
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
if (Weapon != null) if (Weapon != null)
{ {
Unequip(); Unequip();
@@ -295,8 +409,12 @@ namespace Barotrauma
return completed; return completed;
} }
public override bool CanBeCompleted => !abandon && (reloadWeaponObjective == null || reloadWeaponObjective.CanBeCompleted) && (retreatObjective == null || retreatObjective.CanBeCompleted); public override bool CanBeCompleted => !abandon &&
public override float GetPriority(AIObjectiveManager objectiveManager) => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : 100; (reloadWeaponObjective == null || reloadWeaponObjective.CanBeCompleted) &&
(retreatObjective == null || retreatObjective.CanBeCompleted) &&
(followTargetObjective == null || followTargetObjective.CanBeCompleted);
public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
@@ -9,15 +9,9 @@ namespace Barotrauma
{ {
public override string DebugTag => "contain item"; public override string DebugTag => "contain item";
public int MinContainedAmount = 1;
//can either be a tag or an identifier
private string[] itemIdentifiers;
private ItemContainer container;
public Func<Item, float> GetItemPriority; public Func<Item, float> GetItemPriority;
public int MinContainedAmount = 1;
public string[] ignoredContainerIdentifiers; public string[] ignoredContainerIdentifiers;
//can either be a tag or an identifier //can either be a tag or an identifier
@@ -28,13 +22,11 @@ namespace Barotrauma
private AIObjectiveGetItem getItemObjective; private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container) public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container) : this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
{
}
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container) public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base (character, "") : base (character, objectiveManager, priorityModifier)
{ {
this.itemIdentifiers = itemIdentifiers; this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++) for (int i = 0; i < itemIdentifiers.Length; i++)
@@ -51,22 +43,10 @@ namespace Barotrauma
int containedItemCount = 0; int containedItemCount = 0;
foreach (Item item in container.Inventory.Items) foreach (Item item in container.Inventory.Items)
{ {
if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) containedItemCount++; if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id)))
}
return containedItemCount >= MinContainedAmount;
}
public override bool CanBeCompleted
{
get
{
if (goToObjective != null)
{ {
containedItemCount++; containedItemCount++;
} }
return getItemObjective == null || getItemObjective.CanBeCompleted;
} }
return containedItemCount >= MinContainedAmount; return containedItemCount >= MinContainedAmount;
} }
@@ -88,17 +68,16 @@ namespace Barotrauma
foreach (string identifier in itemIdentifiers) foreach (string identifier in itemIdentifiers)
{ {
itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier); itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
if (itemToContain != null && itemToContain.Condition > 0.0f) break; if (itemToContain != null && itemToContain.Condition > 0.0f) { break; }
} }
if (itemToContain == null) if (itemToContain == null)
{ {
getItemObjective = new AIObjectiveGetItem(character, itemIdentifiers) TryAddSubObjective(ref getItemObjective, () =>
{ new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager)
GetItemPriority = GetItemPriority, {
ignoredContainerIdentifiers = ignoredContainerIdentifiers GetItemPriority = GetItemPriority,
}; ignoredContainerIdentifiers = ignoredContainerIdentifiers
AddSubObjective(getItemObjective); });
return; return;
} }
if (container.Item.ParentInventory == character.Inventory) if (container.Item.ParentInventory == character.Inventory)
@@ -115,7 +94,8 @@ namespace Barotrauma
} }
else else
{ {
if (container.Item.CurrentHull != character.CurrentHull || (Vector2.Distance(character.Position, container.Item.Position) > container.Item.InteractDistance && !container.Item.IsInsideTrigger(character.WorldPosition))) if (container.Item.CurrentHull != character.CurrentHull ||
(Vector2.DistanceSquared(character.Position, container.Item.Position) > Math.Pow(container.Item.InteractDistance, 2) && !container.Item.IsInsideTrigger(character.WorldPosition)))
{ {
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager)); TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
return; return;
@@ -127,14 +107,15 @@ namespace Barotrauma
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
AIObjectiveContainItem objective = otherObjective as AIObjectiveContainItem; if (!(otherObjective is AIObjectiveContainItem objective)) { return false; }
if (objective == null) return false; if (objective.container != container) { return false; }
if (objective.container != container) return false; if (objective.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
if (objective.itemIdentifiers.Length != itemIdentifiers.Length) return false;
for (int i = 0; i < itemIdentifiers.Length; i++) for (int i = 0; i < itemIdentifiers.Length; i++)
{ {
if (objective.itemIdentifiers[i] != itemIdentifiers[i]) return false; if (objective.itemIdentifiers[i] != itemIdentifiers[i])
{
return false;
}
} }
return true; return true;
} }
@@ -8,78 +8,53 @@ namespace Barotrauma
{ {
public override string DebugTag => "decontain item"; public override string DebugTag => "decontain item";
//can either be a tag or an identifier
private string[] itemIdentifiers;
private ItemContainer container;
private bool isCompleted;
public Func<Item, float> GetItemPriority; public Func<Item, float> GetItemPriority;
private AIObjectiveGetItem getItemObjective; //can either be a tag or an identifier
private AIObjectiveGoTo goToObjective; private readonly string[] itemIdentifiers;
private Item targetItem; private readonly ItemContainer container;
private readonly Item targetItem;
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer container) private AIObjectiveGoTo goToObjective;
: base(character, "") private bool isCompleted;
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{ {
this.targetItem = targetItem; this.targetItem = targetItem;
this.container = container; this.container = container;
} }
public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer container) public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container) : this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
{
}
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer container) public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, "") : base(character, objectiveManager, priorityModifier)
{ {
this.itemIdentifiers = itemIdentifiers; this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++) for (int i = 0; i < itemIdentifiers.Length; i++)
{ {
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
} }
this.container = container; this.container = container;
} }
public override bool IsCompleted() public override bool IsCompleted() => isCompleted;
{
return isCompleted;
}
public override bool CanBeCompleted public override float GetPriority()
{
get
{
if (goToObjective != null)
{
return goToObjective.CanBeCompleted;
}
return getItemObjective == null || getItemObjective.CanBeCompleted;
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (objectiveManager.CurrentOrder == this) if (objectiveManager.CurrentOrder == this)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
} }
return 1.0f; return 1.0f;
} }
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (isCompleted) return; if (isCompleted) { return; }
Item itemToDecontain = null; Item itemToDecontain = null;
//get the item that should be de-contained //get the item that should be de-contained
if (targetItem == null) if (targetItem == null)
{ {
@@ -88,7 +63,7 @@ namespace Barotrauma
foreach (string identifier in itemIdentifiers) foreach (string identifier in itemIdentifiers)
{ {
itemToDecontain = container.Inventory.FindItemByIdentifier(identifier) ?? container.Inventory.FindItemByTag(identifier); itemToDecontain = container.Inventory.FindItemByIdentifier(identifier) ?? container.Inventory.FindItemByTag(identifier);
if (itemToDecontain != null) break; if (itemToDecontain != null) { break; }
} }
} }
} }
@@ -96,38 +71,32 @@ namespace Barotrauma
{ {
itemToDecontain = targetItem; itemToDecontain = targetItem;
} }
if (itemToDecontain == null || itemToDecontain.Container != container.Item) // Item not found or already de-contained, consider complete if (itemToDecontain == null || itemToDecontain.Container != container.Item) // Item not found or already de-contained, consider complete
{ {
isCompleted = true; isCompleted = true;
return; return;
} }
if (itemToDecontain.OwnInventory != character.Inventory && itemToDecontain.ParentInventory != character.Inventory) if (itemToDecontain.OwnInventory != character.Inventory && itemToDecontain.ParentInventory != character.Inventory)
{ {
if (Vector2.Distance(character.Position, container.Item.Position) > container.Item.InteractDistance if (Vector2.DistanceSquared(character.Position, container.Item.Position) > MathUtils.Pow(container.Item.InteractDistance, 2) && !container.Item.IsInsideTrigger(character.WorldPosition))
&& !container.Item.IsInsideTrigger(character.WorldPosition))
{ {
goToObjective = new AIObjectiveGoTo(container.Item, character); TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
AddSubObjective(goToObjective);
return; return;
} }
} }
itemToDecontain.Drop(character); itemToDecontain.Drop(character);
isCompleted = true; isCompleted = true;
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
AIObjectiveDecontainItem decontainItem = otherObjective as AIObjectiveDecontainItem; if (!(otherObjective is AIObjectiveDecontainItem decontainItem)) { return false; }
if (decontainItem == null) return false;
if (decontainItem.itemIdentifiers != null && itemIdentifiers != null) if (decontainItem.itemIdentifiers != null && itemIdentifiers != null)
{ {
if (decontainItem.itemIdentifiers.Length != itemIdentifiers.Length) return false; if (decontainItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < decontainItem.itemIdentifiers.Length; i++) for (int i = 0; i < decontainItem.itemIdentifiers.Length; i++)
{ {
if (decontainItem.itemIdentifiers[i] != itemIdentifiers[i]) return false; if (decontainItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; }
} }
return true; return true;
} }
@@ -135,7 +104,6 @@ namespace Barotrauma
{ {
return decontainItem.targetItem == targetItem; return decontainItem.targetItem == targetItem;
} }
return false; return false;
} }
} }
@@ -3,6 +3,7 @@ using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -10,125 +11,117 @@ namespace Barotrauma
{ {
public override string DebugTag => "extinguish fire"; public override string DebugTag => "extinguish fire";
public override bool ForceRun => true; public override bool ForceRun => true;
public override bool KeepDivingGearOn => true; public override bool ConcurrentObjectives => true;
private Hull targetHull; private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective; private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective; private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer; private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull) : base(character, "") public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{ {
this.targetHull = targetHull; this.targetHull = targetHull;
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority()
{ {
if (gotoObjective != null && !gotoObjective.CanBeCompleted) { return 0; } if (Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c))) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f; float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist)); float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist));
float severityFactor = MathHelper.Lerp(0, 1, MathHelper.Clamp(targetHull.FireSources.Sum(fs => fs.Size.X) / targetHull.Size.X, 0, 1)); float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
return MathHelper.Clamp(Priority * severityFactor * distanceFactor, 0, 100); float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = Math.Max(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1));
} }
public override bool IsCompleted() public override bool IsCompleted() => targetHull.FireSources.None();
{
return targetHull.FireSources.Count == 0;
}
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFire otherExtinguishFire && otherExtinguishFire.targetHull == targetHull;
{
var otherExtinguishFire = otherObjective as AIObjectiveExtinguishFire;
return otherExtinguishFire != null && otherExtinguishFire.targetHull == targetHull;
}
public override bool CanBeCompleted
{
get { return getExtinguisherObjective == null || getExtinguisherObjective.IsCompleted() || getExtinguisherObjective.CanBeCompleted; }
}
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher"); var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem)) if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{ {
if (getExtinguisherObjective == null) TryAddSubObjective(ref getExtinguisherObjective, () =>
{ {
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
getExtinguisherObjective = new AIObjectiveGetItem(character, "extinguisher", true); return new AIObjectiveGetItem(character, "extinguisher", objectiveManager, equip: true);
} });
else
{
getExtinguisherObjective.TryComplete(deltaTime);
}
return;
} }
else
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
if (extinguisher == null)
{ {
DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher"); var extinguisher = extinguisherItem.GetComponent<RepairTool>();
return; if (extinguisher == null)
}
foreach (FireSource fs in targetHull.FireSources)
{
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
bool move = !inRange;
if (inRange || useExtinquisherTimer > 0.0f)
{ {
useExtinquisherTimer += deltaTime; #if DEBUG
if (useExtinquisherTimer > 2.0f) DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
#endif
abandon = true;
return;
}
foreach (FireSource fs in targetHull.FireSources)
{
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
bool move = !inRange;
if (inRange || useExtinquisherTimer > 0.0f)
{ {
useExtinquisherTimer = 0.0f; useExtinquisherTimer += deltaTime;
} if (useExtinquisherTimer > 2.0f)
character.AIController.SteeringManager.Reset();
character.CursorPosition = fs.Position;
if (extinguisher.Item.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
}
Limb sightLimb = null;
if (character.Inventory.IsInLimbSlot(extinguisherItem, InvSlotType.RightHand))
{
sightLimb = character.AnimController.GetLimb(LimbType.RightHand);
}
else if (character.Inventory.IsInLimbSlot(extinguisherItem, InvSlotType.LeftHand))
{
sightLimb = character.AnimController.GetLimb(LimbType.LeftHand);
}
if (!character.CanSeeTarget(fs, sightLimb))
{
move = true;
}
else
{
move = false;
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{ {
character.Speak(TextManager.Get("DialogPutOutFire").Replace("[roomname]", targetHull.Name), null, 0, "putoutfire", 10.0f); useExtinquisherTimer = 0.0f;
}
character.AIController.SteeringManager.Reset();
character.CursorPosition = fs.Position;
if (extinguisher.Item.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
}
Limb sightLimb = null;
if (character.Inventory.IsInLimbSlot(extinguisherItem, InvSlotType.RightHand))
{
sightLimb = character.AnimController.GetLimb(LimbType.RightHand);
}
else if (character.Inventory.IsInLimbSlot(extinguisherItem, InvSlotType.LeftHand))
{
sightLimb = character.AnimController.GetLimb(LimbType.LeftHand);
}
if (!character.CanSeeTarget(fs, sightLimb))
{
move = true;
}
else
{
move = false;
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.Get("DialogPutOutFire").Replace("[roomname]", targetHull.Name), null, 0, "putoutfire", 10.0f);
}
} }
} }
} if (move)
if (move)
{
//go to the first firesource
if (gotoObjective == null || !gotoObjective.CanBeCompleted || gotoObjective.IsCompleted())
{ {
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character); //go to the first firesource
} TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character, objectiveManager));
else
{
gotoObjective.TryComplete(deltaTime);
} }
break;
} }
break;
} }
} }
} }
@@ -1,62 +1,44 @@
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveExtinguishFires : AIObjective class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
{ {
public override string DebugTag => "extinguish fires"; public override string DebugTag => "extinguish fires";
public override bool ForceRun => true; public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private Dictionary<Hull, AIObjectiveExtinguishFire> extinguishObjectives = new Dictionary<Hull, AIObjectiveExtinguishFire>(); public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public AIObjectiveExtinguishFires(Character character) : base(character, "") { } protected override void FindTargets()
public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (character.Submarine == null) { return 0; } base.FindTargets();
int fireCount = character.Submarine.GetHulls(true).Sum(h => h.FireSources.Count); if (targets.None() && objectiveManager.CurrentOrder == this)
if (objectiveManager.CurrentOrder == this && fireCount > 0)
{ {
return AIObjectiveManager.OrderPriority; character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
} }
return MathHelper.Clamp(fireCount * 20, 0, 100);
} }
public override bool IsCompleted() => false; protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
public override bool CanBeCompleted => true;
public override bool IsDuplicate(AIObjective otherObjective) protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : targets.Sum(t => GetFireSeverity(t));
{
return otherObjective is AIObjectiveExtinguishFires;
}
protected override void Act(float deltaTime) public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFires;
protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override AIObjective ObjectiveConstructor(Hull target) => new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier);
public static bool IsValidTarget(Hull hull, Character character)
{ {
SyncRemovedObjectives(extinguishObjectives, Hull.hullList); if (hull == null) { return false; }
if (character.Submarine == null) { return; } if (hull.FireSources.None()) { return false; }
foreach (Hull hull in Hull.hullList) if (hull.Submarine == null) { return false; }
{ if (hull.Submarine.TeamID != character.TeamID) { return false; }
if (hull.FireSources.None()) { continue; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
if (hull.Submarine == null) { continue; } return true;
if (hull.Submarine.TeamID != character.TeamID) { continue; }
// If the character is inside, only take connected hulls into account.
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (!extinguishObjectives.TryGetValue(hull, out AIObjectiveExtinguishFire objective))
{
objective = new AIObjectiveExtinguishFire(character, hull);
extinguishObjectives.Add(hull, objective);
AddSubObjective(objective);
}
}
if (extinguishObjectives.None())
{
character?.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
}
} }
} }
} }
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -9,8 +10,10 @@ namespace Barotrauma
public override string DebugTag => "find diving gear"; public override string DebugTag => "find diving gear";
public override bool ForceRun => true; public override bool ForceRun => true;
private AIObjective subObjective; private readonly string gearTag;
private string gearTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
public override bool IsCompleted() public override bool IsCompleted()
{ {
@@ -21,15 +24,16 @@ namespace Barotrauma
{ {
var containedItems = character.Inventory.Items[i].ContainedItems; var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems == null) { continue; } if (containedItems == null) { continue; }
return containedItems.Any(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
var oxygenTank = containedItems.FirstOrDefault(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
if (oxygenTank != null) { return true; }
} }
} }
return false; return false;
} }
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit) : base(character, "") public override float GetPriority() => MathHelper.Clamp(100 - character.OxygenAvailable, 0, 100);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindDivingGear;
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{ {
gearTag = needDivingSuit ? "divingsuit" : "diving"; gearTag = needDivingSuit ? "divingsuit" : "diving";
} }
@@ -42,16 +46,23 @@ namespace Barotrauma
TryAddSubObjective(ref getDivingGear, () => TryAddSubObjective(ref getDivingGear, () =>
{ {
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
subObjective = new AIObjectiveGetItem(character, gearTag, true); return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true);
} });
} }
if (getDivingGear != null) { return; } if (getDivingGear != null) { return; }
var containedItems = item.ContainedItems; var containedItems = item.ContainedItems;
if (containedItems == null) if (containedItems == null)
{ {
var containedItems = item.ContainedItems; var containedItems = item.ContainedItems;
if (containedItems == null) { return; } if (containedItems == null)
//check if there's an oxygen tank in the mask/suit {
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
#endif
abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems) foreach (Item containedItem in containedItems)
{ {
if (containedItem == null) { continue; } if (containedItem == null) { continue; }
@@ -59,26 +70,16 @@ namespace Barotrauma
{ {
containedItem.Drop(character); containedItem.Drop(character);
} }
else if (containedItem.Prefab.Identifier == "oxygentank" || containedItem.HasTag("oxygensource"))
{
//we've got an oxygen source inside the mask/suit, all good
return;
}
} }
if (!(subObjective is AIObjectiveContainItem) || subObjective.IsCompleted()) if (containedItems.None(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f))
{ {
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); TryAddSubObjective(ref getOxygen, () =>
subObjective = new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>()); {
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager);
});
} }
} }
if (subObjective != null)
{
subObjective.TryComplete(deltaTime);
}
} }
public override bool CanBeCompleted => subObjective == null || subObjective.CanBeCompleted;
public override float GetPriority(AIObjectiveManager objectiveManager) => MathHelper.Clamp(100 - character.OxygenAvailable, 0, 100);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindDivingGear;
} }
} }
@@ -10,7 +10,6 @@ namespace Barotrauma
{ {
public override string DebugTag => "find safety"; public override string DebugTag => "find safety";
public override bool ForceRun => true; public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
// TODO: expose? // TODO: expose?
const float priorityIncrease = 25; const float priorityIncrease = 25;
@@ -18,7 +17,7 @@ namespace Barotrauma
const float SearchHullInterval = 3.0f; const float SearchHullInterval = 3.0f;
const float clearUnreachableInterval = 30; const float clearUnreachableInterval = 30;
public readonly List<Hull> unreachable = new List<Hull>(); public readonly HashSet<Hull> unreachable = new HashSet<Hull>();
private float currenthullSafety; private float currenthullSafety;
private float unreachableClearTimer; private float unreachableClearTimer;
@@ -27,49 +26,15 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
private AIObjectiveFindDivingGear divingGearObjective; private AIObjectiveFindDivingGear divingGearObjective;
public AIObjectiveFindSafety(Character character) : base(character, "") { } public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public override bool IsCompleted() => false; public override bool IsCompleted() => false;
public override bool CanBeCompleted => true; public override bool CanBeCompleted => true;
protected override void Act(float deltaTime) public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindSafety;
public override void Update(float deltaTime)
{ {
var currentHull = character.AnimController.CurrentHull;
if (HumanAIController.NeedsDivingGear(currentHull) && divingGearObjective == null)
{
bool needsDivingSuit = currentHull == null || currentHull.WaterPercentage > 90;
bool hasEquipment = needsDivingSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingGear(character);
if (!hasEquipment)
{
divingGearObjective = new AIObjectiveFindDivingGear(character, needsDivingSuit);
}
}
if (divingGearObjective != null)
{
divingGearObjective.TryComplete(deltaTime);
if (divingGearObjective.IsCompleted())
{
divingGearObjective = null;
Priority = 0;
}
else if (divingGearObjective.CanBeCompleted)
{
// If diving gear objective is active and can be completed, wait for it to complete.
return;
}
else
{
divingGearObjective = null;
// Reduce the timer so that we get a safe hull target faster.
searchHullTimer = Math.Min(1, searchHullTimer);
}
}
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
}
if (unreachableClearTimer > 0) if (unreachableClearTimer > 0)
{ {
unreachableClearTimer -= deltaTime; unreachableClearTimer -= deltaTime;
@@ -79,86 +44,86 @@ namespace Barotrauma
unreachableClearTimer = clearUnreachableInterval; unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear(); unreachable.Clear();
} }
if (unreachableClearTimer > 0)
{
unreachableClearTimer -= deltaTime;
}
else
{
unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear();
}
if (unreachableClearTimer > 0)
{
unreachableClearTimer -= deltaTime;
}
else
{
unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear();
}
if (unreachableClearTimer > 0)
{
unreachableClearTimer -= deltaTime;
}
else
{
unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear();
}
if (searchHullTimer > 0.0f)
{
unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear();
}
if (character.CurrentHull == null) if (character.CurrentHull == null)
{ {
var bestHull = FindBestHull(); currenthullSafety = 0;
if (bestHull != null && bestHull != currentHull) Priority = 100;
{ return;
if (goToObjective != null)
{
if (goToObjective.Target != bestHull)
{
// If we need diving gear, we should already have it, if possible.
goToObjective = new AIObjectiveGoTo(bestHull, character, getDivingGearIfNeeded: false)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character)
};
}
}
else
{
goToObjective = new AIObjectiveGoTo(bestHull, character, getDivingGearIfNeeded: false)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character)
};
}
}
searchHullTimer = SearchHullInterval;
} }
if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; } if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; }
currenthullSafety = HumanAIController.GetHullSafety(character.CurrentHull); currenthullSafety = HumanAIController.GetHullSafety(character.CurrentHull);
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{ {
goToObjective.TryComplete(deltaTime); Priority -= priorityDecrease * deltaTime;
if (!goToObjective.CanBeCompleted)
{
if (!unreachable.Contains(goToObjective.Target))
{
unreachable.Add(goToObjective.Target as Hull);
}
goToObjective = null;
HumanAIController.ObjectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
//SteeringManager.SteeringWander();
}
} }
else if (currentHull != null) else
{ {
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted)
{
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
}
}
protected override void Act(float deltaTime)
{
var currentHull = character.AnimController.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull);
bool needsDivingSuit = needsDivingGear && (currentHull == null || currentHull.WaterPercentage > 90);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingMask(character);
}
if (needsEquipment)
{
TryAddSubObjective(ref divingGearObjective,
() => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => searchHullTimer = Math.Min(1, searchHullTimer));
}
else
{
if (divingGearObjective != null && divingGearObjective.IsCompleted())
{
// Reset the devotion.
Priority = 0;
divingGearObjective = null;
}
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
}
if (searchHullTimer > 0.0f)
{
searchHullTimer -= deltaTime;
}
else
{
searchHullTimer = SearchHullInterval;
var bestHull = FindBestHull();
if (bestHull != null && bestHull != currentHull)
{
if (goToObjective?.Target != bestHull)
{
goToObjective = null;
}
TryAddSubObjective(ref goToObjective, () =>
new AIObjectiveGoTo(bestHull, character, objectiveManager, getDivingGearIfNeeded: false)
{
// If we need diving gear, we should already have it, if possible.
AllowGoingOutside = HumanAIController.HasDivingSuit(character)
}, onAbandon: () => unreachable.Add(goToObjective.Target as Hull));
}
}
if (goToObjective != null) { return; }
if (currentHull == null) { return; }
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found) //goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards // -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero; Vector2 escapeVel = Vector2.Zero;
@@ -168,14 +133,12 @@ namespace Barotrauma
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
} }
foreach (Character enemy in Character.CharacterList) foreach (Character enemy in Character.CharacterList)
{ {
//don't run from friendly NPCs //don't run from friendly NPCs
if (enemy.TeamID == Character.TeamType.FriendlyNPC) { continue; } if (enemy.TeamID == Character.TeamType.FriendlyNPC) { continue; }
//friendly NPCs don't run away from anything but characters controlled by EnemyAIController (= monsters) //friendly NPCs don't run away from anything but characters controlled by EnemyAIController (= monsters)
if (character.TeamID == Character.TeamType.FriendlyNPC && !(enemy.AIController is EnemyAIController)) { continue; } if (character.TeamID == Character.TeamType.FriendlyNPC && !(enemy.AIController is EnemyAIController)) { continue; }
if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious && if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID)) (enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{ {
@@ -184,7 +147,6 @@ namespace Barotrauma
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
} }
} }
if (escapeVel != Vector2.Zero) if (escapeVel != Vector2.Zero)
{ {
//only move if we haven't reached the edge of the room //only move if we haven't reached the edge of the room
@@ -281,36 +243,5 @@ namespace Barotrauma
} }
return bestHull; return bestHull;
} }
public override bool IsDuplicate(AIObjective otherObjective)
{
return (otherObjective is AIObjectiveFindSafety);
}
public override void Update(AIObjectiveManager objectiveManager, float deltaTime)
{
if (character.CurrentHull == null)
{
currenthullSafety = 0;
Priority = 5;
return;
}
if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; }
currenthullSafety = HumanAIController.GetHullSafety(character.CurrentHull);
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
}
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted)
{
Priority = Math.Max(Priority, AIObjectiveManager.OrderPriority + 10);
}
}
} }
} }
@@ -3,28 +3,24 @@ using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveFixLeak : AIObjective class AIObjectiveFixLeak : AIObjective
{ {
public override string DebugTag => "fix leak"; public override string DebugTag => "fix leak";
public override bool KeepDivingGearOn => true;
public override bool ForceRun => true; public override bool ForceRun => true;
private readonly Gap leak; public Gap Leak { get; private set; }
private AIObjectiveFindDivingGear findDivingGear; private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveGetItem getWeldingTool;
private AIObjectiveContainItem refuelObjective;
private AIObjectiveGoTo gotoObjective; private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective; private AIObjectiveOperateItem operateObjective;
public Gap Leak public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier)
{
get { return leak; }
}
public AIObjectiveFixLeak(Gap leak, Character character) : base (character, "")
{ {
Leak = leak; Leak = leak;
} }
@@ -34,17 +30,16 @@ namespace Barotrauma
return Leak.Open <= 0.0f || Leak.Removed; return Leak.Open <= 0.0f || Leak.Removed;
} }
public override bool CanBeCompleted => !abandon && base.CanBeCompleted; public override float GetPriority()
public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (leak.Open == 0.0f) { return 0.0f; } if (Leak.Open == 0.0f) { return 0.0f; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float leakSize = (leak.IsHorizontal ? leak.Rect.Height : leak.Rect.Width) * Math.Max(leak.Open, 0.1f); float dist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 10000, dist));
float dist = Vector2.DistanceSquared(character.SimPosition, leak.SimPosition); float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak);
dist = Math.Max(dist / 100.0f, 1.0f); float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
return Math.Min(leakSize / dist, 40.0f); float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1));
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
@@ -55,59 +50,44 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (!leak.IsRoomToRoom) if (!Leak.IsRoomToRoom)
{ {
if (findDivingGear == null) if (!HumanAIController.HasDivingSuit(character))
{ {
findDivingGear = new AIObjectiveFindDivingGear(character, true); TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, true, objectiveManager));
AddSubObjective(findDivingGear);
}
else if (!findDivingGear.CanBeCompleted)
{
abandon = true;
return; return;
} }
} }
var weldingTool = character.Inventory.FindItemByTag("weldingtool"); var weldingTool = character.Inventory.FindItemByTag("weldingtool");
if (weldingTool == null) if (weldingTool == null)
{ {
AddSubObjective(new AIObjectiveGetItem(character, "weldingtool", true)); TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true));
return; return;
} }
else else
{ {
var containedItems = weldingTool.ContainedItems; var containedItems = weldingTool.ContainedItems;
if (containedItems == null) return; if (containedItems == null)
var fuelTank = containedItems.FirstOrDefault(i => i.HasTag("weldingfueltank") && i.Condition > 0.0f);
if (fuelTank == null)
{ {
AddSubObjective(new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>())); #if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
#endif
abandon = true;
return; return;
} }
} // Drop empty tanks
foreach (Item containedItem in containedItems)
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null) { return; }
Vector2 gapDiff = leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
float reach = ConvertUnits.ToSimUnits(repairTool.Range);
bool canReach = ConvertUnits.ToSimUnits(gapDiff.Length()) < reach;
if (canReach)
{
Limb sightLimb = null;
if (character.Inventory.IsInLimbSlot(repairTool.Item, InvSlotType.RightHand))
{ {
sightLimb = character.AnimController.GetLimb(LimbType.RightHand); if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
if (containedItems.None(i => i.HasTag("weldingfueltank") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>(), objectiveManager));
return;
} }
else if (character.Inventory.IsInLimbSlot(repairTool.Item, InvSlotType.LeftHand)) else if (character.Inventory.IsInLimbSlot(repairTool.Item, InvSlotType.LeftHand))
{ {
@@ -115,44 +95,30 @@ namespace Barotrauma
} }
canReach = character.CanSeeTarget(leak, sightLimb); canReach = character.CanSeeTarget(leak, sightLimb);
} }
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
abandon = true;
return;
}
Vector2 gapDiff = Leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
float reach = ConvertUnits.ToSimUnits(repairTool.Range);
bool canOperate = ConvertUnits.ToSimUnits(gapDiff.Length()) < reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak));
}
else else
{ {
if (gotoObjective != null) TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character, objectiveManager) { CloseEnough = reach * 0.75f });
{
// Check if the objective is already removed -> completed/impossible
if (!subObjectives.Contains(gotoObjective))
{
if (!gotoObjective.CanBeCompleted)
{
abandon = true;
}
gotoObjective = null;
return;
}
}
else
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character)
{
CloseEnough = reach
};
if (!subObjectives.Contains(gotoObjective))
{
AddSubObjective(gotoObjective);
}
}
}
if (gotoObjective == null || gotoObjective.IsCompleted())
{
if (operateObjective == null)
{
operateObjective = new AIObjectiveOperateItem(repairTool, character, "", true, leak);
AddSubObjective(operateObjective);
}
else if (!subObjectives.Contains(operateObjective))
{
operateObjective = null;
}
} }
} }
@@ -9,60 +9,43 @@ namespace Barotrauma
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap> class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{ {
public override string DebugTag => "fix leaks"; public override string DebugTag => "fix leaks";
public override bool KeepDivingGearOn => true;
public override bool ForceRun => true; public override bool ForceRun => true;
public AIObjectiveFixLeaks(Character character) : base (character, "") { } public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override void FindTargets()
{
if (character.Submarine == null) { return 0; }
if (targets.None()) { return 0; }
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return MathHelper.Lerp(0, AIObjectiveManager.OrderPriority, targets.Average(t => Average(t)));
}
protected override void FindTargets() protected override void FindTargets()
{ {
base.FindTargets(); base.FindTargets();
targets.Sort((x, y) => GetGapFixPriority(y).CompareTo(GetGapFixPriority(x))); if (targets.None() && objectiveManager.CurrentOrder == this)
}
protected override bool Filter(Gap gap)
{
bool ignore = ignoreList.Contains(gap) || gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null);
if (!ignore)
{ {
if (gap.Submarine == null) { ignore = true; } character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks", 30.0f);
else if (gap.Submarine.TeamID != character.TeamID) { ignore = true; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { ignore = true; }
} }
return ignore;
} }
private float GetGapFixPriority(Gap gap) protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
public static float GetLeakSeverity(Gap leak)
{ {
if (gap == null) return 0.0f; if (leak == null) { return 0; }
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, (leak.IsHorizontal ? leak.Rect.Width : leak.Rect.Height)));
//larger gap -> higher priority float severity = sizeFactor * leak.Open;
float gapPriority = (gap.IsHorizontal ? gap.Rect.Width : gap.Rect.Height) * gap.Open; if (!leak.IsRoomToRoom) { severity *= 50; }
return MathHelper.Min(severity, 100);
//prioritize gaps that are close
gapPriority /= Math.Max(Vector2.Distance(character.WorldPosition, gap.WorldPosition), 1.0f);
//gaps to outside are much higher priority
if (!gap.IsRoomToRoom) gapPriority *= 10.0f;
return gapPriority;
} }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks; public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks;
protected override float Average(Gap gap) => gap.Open; protected override float TargetEvaluation() => targets.Max(t => GetLeakSeverity(t));
protected override IEnumerable<Gap> GetList() => Gap.GapList; protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap) => new AIObjectiveFixLeak(gap, character); protected override AIObjective ObjectiveConstructor(Gap gap) => new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
public static bool IsValidTarget(Gap gap, Character character)
{
if (gap == null) { return false; }
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null) { return false; }
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
return true;
}
} }
} }
@@ -11,6 +11,9 @@ namespace Barotrauma
{ {
public override string DebugTag => "get item"; public override string DebugTag => "get item";
private readonly bool equip;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
public Func<Item, float> GetItemPriority; public Func<Item, float> GetItemPriority;
//can be either tags or identifiers //can be either tags or identifiers
@@ -20,14 +23,8 @@ namespace Barotrauma
public string[] ignoredContainerIdentifiers; public string[] ignoredContainerIdentifiers;
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
private float currItemPriority; private float currItemPriority;
private bool equip;
private HashSet<Item> ignoredItems = new HashSet<Item>(); public override float GetPriority()
private bool canBeCompleted = true;
public override bool CanBeCompleted => canBeCompleted;
public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (objectiveManager.CurrentOrder == this) if (objectiveManager.CurrentOrder == this)
{ {
@@ -36,16 +33,19 @@ namespace Barotrauma
return 1.0f; return 1.0f;
} }
public AIObjectiveGetItem(Character character, Item targetItem, bool equip = false) : base(character, "") public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = false, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{ {
currSearchIndex = -1; currSearchIndex = -1;
this.equip = equip; this.equip = equip;
this.targetItem = targetItem; this.targetItem = targetItem;
} }
public AIObjectiveGetItem(Character character, string itemIdentifier, bool equip = false) : this(character, new string[] { itemIdentifier }, equip) { } public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = false, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, priorityModifier) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false) : base(character, "") public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = false, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{ {
currSearchIndex = -1; currSearchIndex = -1;
this.equip = equip; this.equip = equip;
@@ -54,20 +54,15 @@ namespace Barotrauma
{ {
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
} }
CheckInventory(); CheckInventory();
} }
private void CheckInventory() private void CheckInventory()
{ {
if (itemIdentifiers == null) if (itemIdentifiers == null) { return; }
{
return;
}
for (int i = 0; i < character.Inventory.Items.Length; i++) for (int i = 0; i < character.Inventory.Items.Length; i++)
{ {
if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) continue; if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) { continue; }
if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id))) if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id)))
{ {
targetItem = character.Inventory.Items[i]; targetItem = character.Inventory.Items[i];
@@ -81,7 +76,7 @@ namespace Barotrauma
{ {
foreach (Item containedItem in containedItems) foreach (Item containedItem in containedItems)
{ {
if (containedItem == null || containedItem.Condition <= 0.0f) continue; if (containedItem == null || containedItem.Condition <= 0.0f) { continue; }
if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id))) if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id)))
{ {
targetItem = containedItem; targetItem = containedItem;
@@ -99,13 +94,11 @@ namespace Barotrauma
FindTargetItem(); FindTargetItem();
if (targetItem == null || moveToTarget == null) if (targetItem == null || moveToTarget == null)
{ {
HumanAIController.ObjectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime); objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
//SteeringManager.SteeringWander();
return; return;
} }
if (moveToTarget.CurrentHull == character.CurrentHull && if (moveToTarget.CurrentHull == character.CurrentHull &&
Vector2.DistanceSquared(character.Position, moveToTarget.Position) < MathUtils.Pow(targetItem.InteractDistance * 2, 2)) Vector2.DistanceSquared(character.Position, moveToTarget.Position) < MathUtils.Pow(targetItem.InteractDistance, 2))
{ {
int targetSlot = -1; int targetSlot = -1;
if (equip) if (equip)
@@ -123,8 +116,7 @@ namespace Barotrauma
for (int i = 0; i < character.Inventory.Items.Length; i++) for (int i = 0; i < character.Inventory.Items.Length; i++)
{ {
//slot not needed by the item, continue //slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) continue; if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i; targetSlot = i;
//slot free, continue //slot free, continue
if (character.Inventory.Items[i] == null) { continue; } if (character.Inventory.Items[i] == null) { continue; }
@@ -136,7 +128,6 @@ namespace Barotrauma
} }
} }
targetItem.TryInteract(character, false, true); targetItem.TryInteract(character, false, true);
if (targetSlot > -1 && !character.HasEquippedItem(targetItem)) if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
{ {
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character); character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
@@ -144,23 +135,20 @@ namespace Barotrauma
} }
else else
{ {
if (goToObjective == null || moveToTarget != goToObjective.Target) TryAddSubObjective(ref goToObjective,
{ constructor: () =>
//check if we're already looking for a diving gear {
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) || //check if we're already looking for a diving gear
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit"))); bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) ||
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit")));
//don't attempt to get diving gear to reach the destination if the item we're trying to get is diving gear return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: !gettingDivingGear);
goToObjective = new AIObjectiveGoTo(moveToTarget, character, false, !gettingDivingGear); },
} onAbandon: () =>
{
goToObjective.TryComplete(deltaTime); targetItem = null;
if (!goToObjective.CanBeCompleted) moveToTarget = null;
{ ignoredItems.Add(targetItem);
targetItem = null; });
moveToTarget = null;
ignoredItems.Add(targetItem);
}
} }
} }
@@ -176,11 +164,10 @@ namespace Barotrauma
#if DEBUG #if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined."); DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined.");
#endif #endif
canBeCompleted = false; abandon = true;
} }
return; return;
} }
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++) for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
{ {
currSearchIndex++; currSearchIndex++;
@@ -189,15 +176,18 @@ namespace Barotrauma
if (item.Submarine == null) { continue; } if (item.Submarine == null) { continue; }
else if (item.Submarine.TeamID != character.TeamID) { continue; } else if (item.Submarine.TeamID != character.TeamID) { continue; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.CurrentHull == null || item.Condition <= 0.0f) { continue; } if (item.CurrentHull == null || item.Condition <= 0.0f) { continue; }
if (itemIdentifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; } if (itemIdentifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null) if (ignoredContainerIdentifiers != null && item.Container != null)
{ {
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
} }
// TODO: need to exclude items that already are in the inventory?
//// Don't allow to get items in rooms that have a fire (except fire extinguishers) or an enemy inside (except weapons)
//if (!itemIdentifiers.Contains("extinguisher") && item.CurrentHull.FireSources.Count > 0 ||
// item.GetComponent<MeleeWeapon>() == null && item.GetComponent<RangedWeapon>() == null && Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { continue; }
//if the item is inside a character's inventory, don't steal it unless the character is dead //if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory) if (item.ParentInventory is CharacterInventory)
{ {
@@ -219,13 +209,10 @@ namespace Barotrauma
itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f; itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f;
//ignore if the item has a lower priority than the currently selected one //ignore if the item has a lower priority than the currently selected one
if (moveToTarget != null && itemPriority < currItemPriority) { continue; } if (moveToTarget != null && itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority; currItemPriority = itemPriority;
targetItem = item; targetItem = item;
moveToTarget = rootContainer ?? item; moveToTarget = rootContainer ?? item;
}
} }
//if searched through all the items and a target wasn't found, can't be completed //if searched through all the items and a target wasn't found, can't be completed
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null) if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null)
@@ -233,21 +220,21 @@ namespace Barotrauma
#if DEBUG #if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}"); DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}");
#endif #endif
canBeCompleted = false; abandon = true;
} }
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
AIObjectiveGetItem getItem = otherObjective as AIObjectiveGetItem; AIObjectiveGetItem getItem = otherObjective as AIObjectiveGetItem;
if (getItem == null) return false; if (getItem == null) { return false; }
if (getItem.equip != equip) return false; if (getItem.equip != equip) { return false; }
if (getItem.itemIdentifiers != null && itemIdentifiers != null) if (getItem.itemIdentifiers != null && itemIdentifiers != null)
{ {
if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) return false; if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < getItem.itemIdentifiers.Length; i++) for (int i = 0; i < getItem.itemIdentifiers.Length; i++)
{ {
if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) return false; if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; }
} }
return true; return true;
} }
@@ -265,7 +252,10 @@ namespace Barotrauma
foreach (string itemName in itemIdentifiers) foreach (string itemName in itemIdentifiers)
{ {
var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName); var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName);
if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem))) return true; if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem)))
{
return true;
}
} }
return false; return false;
} }
@@ -9,37 +9,23 @@ namespace Barotrauma
{ {
public override string DebugTag => "go to"; public override string DebugTag => "go to";
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveFindDivingGear findDivingGear; private AIObjectiveFindDivingGear findDivingGear;
private Vector2 targetPos; private Vector2 targetPos;
private bool repeat; private bool repeat;
private bool cannotReach;
//how long until the path to the target is declared unreachable //how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable; private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded; private bool getDivingGearIfNeeded;
public float CloseEnough = 0.5f; public float CloseEnough { get; set; } = 0.5f;
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead; public override float GetPriority()
public bool AllowGoingOutside = false;
public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (FollowControlledCharacter && Character.Controlled == null) { return 0.0f; } if (FollowControlledCharacter && Character.Controlled == null) { return 0.0f; }
if (Target != null && Target.Removed) { return 0.0f; } if (Target != null && Target.Removed) { return 0.0f; }
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { return 0.0f; } if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { return 0.0f; }
if (objectiveManager.CurrentOrder == this) if (objectiveManager.CurrentOrder == this)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
@@ -47,44 +33,6 @@ namespace Barotrauma
return 1.0f; return 1.0f;
} }
public override bool CanBeCompleted
{
get
{
bool canComplete = !cannotReach && !abandon;
if (canComplete)
{
if (FollowControlledCharacter && Character.Controlled == null)
{
canComplete = false;
}
else if (Target != null && Target.Removed)
{
canComplete = false;
}
else if (!repeat && waitUntilPathUnreachable < 0)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null)
{
canComplete = !PathSteering.CurrentPath.Unreachable;
}
}
}
if (!canComplete)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {(Target != null ? Target.ToString() : TargetPos.ToString())}", Color.Yellow);
#endif
if (HumanAIController.ObjectiveManager.CurrentOrder != null)
{
character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f);
}
character.AIController.SteeringManager.Reset();
}
return canComplete;
}
}
public Entity Target { get; private set; } public Entity Target { get; private set; }
public Vector2 TargetPos => Target != null ? Target.SimPosition : targetPos; public Vector2 TargetPos => Target != null ? Target.SimPosition : targetPos;
@@ -97,7 +45,7 @@ namespace Barotrauma
this.Target = target; this.Target = target;
this.repeat = repeat; this.repeat = repeat;
waitUntilPathUnreachable = 1.0f; waitUntilPathUnreachable = 2.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded; this.getDivingGearIfNeeded = getDivingGearIfNeeded;
CalculateCloseEnough(); CalculateCloseEnough();
} }
@@ -118,27 +66,36 @@ namespace Barotrauma
{ {
if (FollowControlledCharacter) if (FollowControlledCharacter)
{ {
if (Character.Controlled == null) { return; } if (Character.Controlled == null)
{
abandon = true;
return;
}
Target = Character.Controlled; Target = Character.Controlled;
} }
if (Target == character) if (Target == character)
{ {
character.AIController.SteeringManager.Reset(); character.AIController.SteeringManager.Reset();
abandon = true;
return; return;
} }
waitUntilPathUnreachable -= deltaTime; waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing) if (!character.IsClimbing)
{ {
character.SelectedConstruction = null; character.SelectedConstruction = null;
} }
if (Target != null)
if (Target != null) { character.AIController.SelectTarget(Target.AiTarget); } {
if (Target.Removed)
{
abandon = true;
}
else
{
character.AIController.SelectTarget(Target.AiTarget);
}
}
Vector2 currTargetPos = Vector2.Zero; Vector2 currTargetPos = Vector2.Zero;
if (Target == null) if (Target == null)
{ {
currTargetPos = targetPos; currTargetPos = targetPos;
@@ -153,8 +110,12 @@ namespace Barotrauma
currTargetPos -= character.Submarine.SimPosition; currTargetPos -= character.Submarine.SimPosition;
} }
} }
bool sightCheck = true;
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < CloseEnough * CloseEnough) if (CheckVisibility && Target != null)
{
sightCheck = Target is Character ch ? character.CanSeeCharacter(ch) : character.CanSeeTarget(Target);
}
if (sightCheck && Vector2.DistanceSquared(currTargetPos, character.SimPosition) < CloseEnough * CloseEnough)
{ {
character.AIController.SteeringManager.Reset(); character.AIController.SteeringManager.Reset();
character.AnimController.TargetDir = currTargetPos.X > character.SimPosition.X ? Direction.Right : Direction.Left; character.AnimController.TargetDir = currTargetPos.X > character.SimPosition.X ? Direction.Right : Direction.Left;
@@ -162,31 +123,50 @@ namespace Barotrauma
else else
{ {
bool isInside = character.CurrentHull != null; bool isInside = character.CurrentHull != null;
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null; bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool targetIsOutside = (Target != null && Target.Submarine == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes); bool targetIsOutside = (Target != null && Target.Submarine == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside) if (isInside && targetIsOutside && !AllowGoingOutside)
{ {
cannotReach = true; abandon = true;
}
else if (!repeat && waitUntilPathUnreachable < 0)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null)
{
abandon = PathSteering.CurrentPath.Unreachable;
}
}
if (abandon)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {(Target != null ? Target.ToString() : TargetPos.ToString())}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null)
{
character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f);
}
character.AIController.SteeringManager.Reset();
} }
else else
{ {
character.AIController.SteeringManager.SteeringSeek(currTargetPos); character.AIController.SteeringManager.SteeringSeek(currTargetPos);
if (getDivingGearIfNeeded) if (getDivingGearIfNeeded)
{ {
if (targetIsOutside || var targetHull = Target is Hull h ? h : Target is Item i ? i.CurrentHull : Target is Character c ? c.CurrentHull : character.CurrentHull;
Target is Hull h && HumanAIController.NeedsDivingGear(h) || bool needsDivingGear = HumanAIController.NeedsDivingGear(targetHull);
Target is Item i && HumanAIController.NeedsDivingGear(i.CurrentHull) || bool needsDivingSuit = needsDivingGear && (targetIsOutside || targetHull.WaterPercentage > 90);
Target is Character c && HumanAIController.NeedsDivingGear(c.CurrentHull)) bool needsEquipment = false;
if (needsDivingSuit)
{ {
if (findDivingGear == null) needsEquipment = !HumanAIController.HasDivingSuit(character);
{ }
findDivingGear = new AIObjectiveFindDivingGear(character, true); else if (needsDivingGear)
AddSubObjective(findDivingGear); {
} needsEquipment = !HumanAIController.HasDivingMask(character);
else if (!findDivingGear.CanBeCompleted) }
{ if (needsEquipment)
abandon = true; {
} TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager));
} }
} }
} }
@@ -197,45 +177,29 @@ namespace Barotrauma
{ {
if (repeat) { return false; } if (repeat) { return false; }
bool completed = false; bool completed = false;
if (Target is Item item) if (Target is Item item)
{ {
if (item.IsInsideTrigger(character.WorldPosition)) completed = true; if (item.IsInsideTrigger(character.WorldPosition)) { completed = true; }
} }
else if (Target is Character targetCharacter) else if (Target is Character targetCharacter)
{ {
if (character.CanInteractWith(targetCharacter)) completed = true; if (character.CanInteractWith(targetCharacter)) { completed = true; }
} }
completed = completed || Vector2.DistanceSquared(Target != null ? Target.SimPosition : targetPos, character.SimPosition) < CloseEnough * CloseEnough; completed = completed || Vector2.DistanceSquared(Target != null ? Target.SimPosition : targetPos, character.SimPosition) < CloseEnough * CloseEnough;
if (completed) { character.AIController.SteeringManager.Reset(); }
if (completed) character.AIController.SteeringManager.Reset();
return completed; return completed;
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
AIObjectiveGoTo objective = otherObjective as AIObjectiveGoTo; if (!(otherObjective is AIObjectiveGoTo objective)) { return false; }
if (objective == null) return false; if (objective.Target == Target) { return true; }
return objective.targetPos == targetPos;
if (objective.Target == Target) return true;
private void CalculateCloseEnough()
{
float interactionDistance = Target is Item i ? ConvertUnits.ToSimUnits(i.InteractDistance) : 0;
CloseEnough = Math.Max(interactionDistance, CloseEnough);
} }
private void CalculateCloseEnough() private void CalculateCloseEnough()
{ {
float interactionDistance = Target is Item i ? ConvertUnits.ToSimUnits(i.InteractDistance) : 0; float interactionDistance = Target is Item i ? ConvertUnits.ToSimUnits(i.InteractDistance * 0.9f) : 0;
CloseEnough = Math.Max(interactionDistance, CloseEnough);
}
private void CalculateCloseEnough()
{
float interactionDistance = Target is Item i ? ConvertUnits.ToSimUnits(i.InteractDistance) : 0;
CloseEnough = Math.Max(interactionDistance, CloseEnough); CloseEnough = Math.Max(interactionDistance, CloseEnough);
} }
} }
@@ -29,7 +29,7 @@ namespace Barotrauma
private readonly List<Hull> targetHulls = new List<Hull>(20); private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20); private readonly List<float> hullWeights = new List<float>(20);
public AIObjectiveIdle(Character character) : base(character, "") public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{ {
standStillTimer = Rand.Range(-10.0f, 10.0f); standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f); walkDuration = Rand.Range(0.0f, 10.0f);
@@ -38,6 +38,27 @@ namespace Barotrauma
public override bool IsCompleted() => false; public override bool IsCompleted() => false;
public override bool CanBeCompleted => true; public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void SetRandom()
{
Random = Rand.Range(0.5f, 1.5f);
randomTimer = randomUpdateInterval;
}
public override float GetPriority()
{
return 1;
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
float initiative = character.GetSkillLevel("initiative");
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
return Priority;
}
public override void Update(float deltaTime) public override void Update(float deltaTime)
{ {
if (objectiveManager.CurrentObjective == this) if (objectiveManager.CurrentObjective == this)
@@ -63,7 +84,7 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (PathSteering == null) return; if (PathSteering == null) { return; }
//don't keep dragging others when idling //don't keep dragging others when idling
if (character.SelectedCharacter != null) if (character.SelectedCharacter != null)
@@ -75,24 +96,10 @@ namespace Barotrauma
character.SelectedConstruction = null; character.SelectedConstruction = null;
} }
bool currentHullForbidden = IsForbidden(character.CurrentHull);
if (!currentHullForbidden && !character.AnimController.InWater && !character.IsClimbing && HumanAIController.ObjectiveManager.WaitTimer > 0)
{
SteeringManager.Reset();
return;
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || if (currentTargetIsInvalid || currentTarget == null && IsForbidden(character.CurrentHull))
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTargetIsInvalid || (currentTarget == null && currentHullForbidden))
{ {
newTargetTimer = 0; newTargetTimer = 0;
standStillTimer = 0; standStillTimer = 0;
@@ -129,7 +136,6 @@ namespace Barotrauma
{ {
//choose a random available hull //choose a random available hull
var randomHull = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); var randomHull = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull); bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull);
if (isCurrentHullOK) if (isCurrentHullOK)
{ {
@@ -137,7 +143,8 @@ namespace Barotrauma
// Only do this when the current hull is ok, because otherwise would block all paths from the current hull to the target hull. // Only do this when the current hull is ok, because otherwise would block all paths from the current hull to the target hull.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, randomHull.SimPosition); var path = PathSteering.PathFinder.FindPath(character.SimPosition, randomHull.SimPosition);
if (path.Unreachable || if (path.Unreachable ||
path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) ||
IsForbidden(n.CurrentHull)))
{ {
//can't go to this room, remove it from the list and try another room next frame //can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(randomHull); int index = targetHulls.IndexOf(randomHull);
@@ -260,7 +267,6 @@ namespace Barotrauma
private void FindTargetHulls() private void FindTargetHulls()
{ {
bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull);
targetHulls.Clear(); targetHulls.Clear();
hullWeights.Clear(); hullWeights.Clear();
foreach (var hull in Hull.hullList) foreach (var hull in Hull.hullList)
@@ -292,7 +298,7 @@ namespace Barotrauma
} }
} }
private bool IsForbidden(Hull hull) public static bool IsForbidden(Hull hull)
{ {
if (hull == null) { return true; } if (hull == null) { return true; }
string hullName = hull.RoomName?.ToLowerInvariant(); string hullName = hull.RoomName?.ToLowerInvariant();
@@ -7,7 +7,7 @@ namespace Barotrauma
{ {
abstract class AIObjectiveLoop<T> : AIObjective abstract class AIObjectiveLoop<T> : AIObjective
{ {
protected List<T> targets = new List<T>(); protected HashSet<T> targets = new HashSet<T>();
protected Dictionary<T, AIObjective> objectives = new Dictionary<T, AIObjective>(); protected Dictionary<T, AIObjective> objectives = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>(); protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer; private float ignoreListTimer;
@@ -17,7 +17,24 @@ namespace Barotrauma
protected virtual float IgnoreListClearInterval => 0; protected virtual float IgnoreListClearInterval => 0;
protected virtual float TargetUpdateInterval => 2; protected virtual float TargetUpdateInterval => 2;
public AIObjectiveLoop(Character character, string option) : base(character, option) public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
public bool AddTarget(T target)
{
if (ReportedTargets.Contains(target))
{
return false;
}
if (Filter(target))
{
ReportedTargets.Add(target);
return true;
}
return false;
}
public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
: base(character, objectiveManager, priorityModifier, option)
{ {
Reset(); Reset();
} }
@@ -26,9 +43,11 @@ namespace Barotrauma
public override bool IsCompleted() => false; public override bool IsCompleted() => false;
public override bool CanBeCompleted => true; public override bool CanBeCompleted => true;
public override void Update(AIObjectiveManager objectiveManager, float deltaTime) public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override void Update(float deltaTime)
{ {
base.Update(objectiveManager, deltaTime); base.Update(deltaTime);
if (IgnoreListClearInterval > 0) if (IgnoreListClearInterval > 0)
{ {
if (ignoreListTimer > IgnoreListClearInterval) if (ignoreListTimer > IgnoreListClearInterval)
@@ -79,15 +98,29 @@ namespace Barotrauma
public override void OnSelected() public override void OnSelected()
{ {
Reset(); base.OnSelected();
if (HumanAIController.ObjectiveManager.CurrentOrder == this)
{
Reset();
}
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority()
{ {
if (character.Submarine == null) { return 0; } if (character.Submarine == null) { return 0; }
if (targets.None()) { return 0; } if (targets.None()) { return 0; }
float avg = targets.Average(t => Average(t)); // Allow the target value to be more than 100.
return MathHelper.Lerp(0, AIObjectiveManager.OrderPriority + 20, avg / 100); float targetValue = TargetEvaluation();
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1) { return 0; }
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float devotion = MathHelper.Min(10, Priority);
float value = MathHelper.Clamp((devotion + targetValue * PriorityModifier) / 100, 0, 1);
return MathHelper.Lerp(0, max, value);
} }
protected void UpdateTargets() protected void UpdateTargets()
@@ -99,12 +132,19 @@ namespace Barotrauma
protected virtual void FindTargets() protected virtual void FindTargets()
{ {
foreach (T item in GetList()) foreach (T target in GetList())
{ {
if (Filter(item)) { continue; } // The bots always find targets when the objective is an order.
if (!targets.Contains(item)) if (objectiveManager.CurrentOrder != this)
{ {
targets.Add(item); // Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
if (!ignoreList.Contains(target))
{
targets.Add(target);
} }
} }
} }
@@ -116,6 +156,7 @@ namespace Barotrauma
if (!objectives.TryGetValue(target, out AIObjective objective)) if (!objectives.TryGetValue(target, out AIObjective objective))
{ {
objective = ObjectiveConstructor(target); objective = ObjectiveConstructor(target);
objective.Completed += () => ReportedTargets.Remove(target);
objectives.Add(target, objective); objectives.Add(target, objective);
AddSubObjective(objective); AddSubObjective(objective);
} }
@@ -126,7 +167,9 @@ namespace Barotrauma
/// List of all possible items of the specified type. Used for filtering the removed objectives. /// List of all possible items of the specified type. Used for filtering the removed objectives.
/// </summary> /// </summary>
protected abstract IEnumerable<T> GetList(); protected abstract IEnumerable<T> GetList();
protected abstract float Average(T target);
protected abstract float TargetEvaluation();
protected abstract AIObjective ObjectiveConstructor(T target); protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target); protected abstract bool Filter(T target);
} }
@@ -9,27 +9,29 @@ namespace Barotrauma
class AIObjectiveManager class AIObjectiveManager
{ {
// TODO: expose // TODO: expose
public const float OrderPriority = 50.0f; public const float OrderPriority = 70;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden // Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 2; public const float baseDevotion = 2;
public List<AIObjective> Objectives { get; private set; } public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
private Character character; private Character character;
/// <summary> /// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out. Only affects idling. /// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders.
/// </summary> /// </summary>
public float WaitTimer; public float WaitTimer;
public AIObjective CurrentOrder { get; private set; } public AIObjective CurrentOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; } public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public AIObjectiveManager(Character character) public AIObjectiveManager(Character character)
{ {
this.character = character; this.character = character;
CreateAutonomousObjectives();
Objectives = new List<AIObjective>();
} }
public void AddObjective(AIObjective objective) public void AddObjective(AIObjective objective)
@@ -46,6 +48,30 @@ namespace Barotrauma
} }
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>(); public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void CreateAutonomousObjectives()
{
Objectives.Clear();
AddObjective(new AIObjectiveFindSafety(character, this), delay: Rand.Value() / 2);
AddObjective(new AIObjectiveIdle(character, this), delay: Rand.Value() / 2);
int objectiveCount = Objectives.Count;
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == automaticOrder.aiTag);
if (orderPrefab == null) { throw new Exception("Could not find a matching prefab by ai tag: " + automaticOrder.aiTag); }
// TODO: Similar code is used in CrewManager:815-> DRY
var matchingItems = orderPrefab.ItemIdentifiers.Any() ?
Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType));
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
var item = matchingItems.GetRandom();
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType));
AddObjective(CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier), delay: Rand.Value() / 2);
objectiveCount++;
}
WaitTimer = Math.Max(WaitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
public void AddObjective(AIObjective objective, float delay, Action callback = null) public void AddObjective(AIObjective objective, float delay, Action callback = null)
{ {
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine)) if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
@@ -75,7 +101,7 @@ namespace Barotrauma
{ {
var previousObjective = CurrentObjective; var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault(); var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority(this) > firstObjective.GetPriority(this)) if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority() > firstObjective.GetPriority())
{ {
CurrentObjective = CurrentOrder; CurrentObjective = CurrentOrder;
} }
@@ -86,18 +112,24 @@ namespace Barotrauma
if (previousObjective != CurrentObjective) if (previousObjective != CurrentObjective)
{ {
CurrentObjective?.OnSelected(); CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>()?.SetRandom();
} }
return CurrentObjective; return CurrentObjective;
} }
public float GetCurrentPriority() public float GetCurrentPriority()
{ {
return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority(this); return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority();
} }
public void UpdateObjectives(float deltaTime) public void UpdateObjectives(float deltaTime)
{ {
CurrentOrder?.Update(this, deltaTime); CurrentOrder?.Update(deltaTime);
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
return;
}
for (int i = 0; i < Objectives.Count; i++) for (int i = 0; i < Objectives.Count; i++)
{ {
var objective = Objectives[i]; var objective = Objectives[i];
@@ -115,9 +147,9 @@ namespace Barotrauma
#endif #endif
Objectives.Remove(objective); Objectives.Remove(objective);
} }
else else if (objective != CurrentOrder)
{ {
objective.Update(this, deltaTime); objective.Update(deltaTime);
} }
} }
GetCurrentObjective(); GetCurrentObjective();
@@ -127,15 +159,41 @@ namespace Barotrauma
{ {
if (Objectives.Any()) if (Objectives.Any())
{ {
Objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this))); Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
} }
CurrentObjective?.SortSubObjectives(this); CurrentObjective?.SortSubObjectives();
} }
public void DoCurrentObjective(float deltaTime) public void DoCurrentObjective(float deltaTime)
{ {
if (WaitTimer > 0.0f) { WaitTimer -= deltaTime; } if (WaitTimer <= 0)
CurrentObjective?.TryComplete(deltaTime); {
CurrentObjective?.TryComplete(deltaTime);
}
else
{
if (CurrentOrder != null)
{
CurrentOrder.TryComplete(deltaTime);
}
else
{
if (character.AIController is HumanAIController humanAI && humanAI.SteeringManager != null)
{
if (!character.AnimController.InWater &&
!character.IsClimbing &&
!humanAI.UnsafeHulls.Contains(character.CurrentHull) &&
!AIObjectiveIdle.IsForbidden(character.CurrentHull))
{
humanAI.SteeringManager.Reset();
}
else
{
CurrentObjective?.TryComplete(deltaTime);
}
}
}
}
} }
public void SetOrder(AIObjective objective) public void SetOrder(AIObjective objective)
@@ -145,13 +203,22 @@ namespace Barotrauma
public void SetOrder(Order order, string option, Character orderGiver) public void SetOrder(Order order, string option, Character orderGiver)
{ {
CurrentOrder = null; CurrentOrder = CreateObjective(order, option, orderGiver);
if (order == null) return; if (CurrentOrder == null)
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
{
if (order == null) { return null; }
AIObjective newObjective;
switch (order.AITag.ToLowerInvariant()) switch (order.AITag.ToLowerInvariant())
{ {
case "follow": case "follow":
CurrentOrder = new AIObjectiveGoTo(orderGiver, character, true) newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{ {
CloseEnough = 1.5f, CloseEnough = 1.5f,
AllowGoingOutside = true, AllowGoingOutside = true,
@@ -160,38 +227,41 @@ namespace Barotrauma
}; };
break; break;
case "wait": case "wait":
CurrentOrder = new AIObjectiveGoTo(character, character, true) newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier)
{ {
AllowGoingOutside = true AllowGoingOutside = true
}; };
break; break;
case "fixleaks": case "fixleaks":
CurrentOrder = new AIObjectiveFixLeaks(character); newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier);
break; break;
case "chargebatteries": case "chargebatteries":
CurrentOrder = new AIObjectiveChargeBatteries(character, option); newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier);
break; break;
case "rescue": case "rescue":
CurrentOrder = new AIObjectiveRescueAll(character); newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break; break;
case "repairsystems": case "repairsystems":
CurrentOrder = new AIObjectiveRepairItems(character) { RequireAdequateSkills = option != "all" }; newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) { RequireAdequateSkills = option != "all" };
break; break;
case "pumpwater": case "pumpwater":
CurrentOrder = new AIObjectivePumpWater(character, option); newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
break; break;
case "extinguishfires": case "extinguishfires":
CurrentOrder = new AIObjectiveExtinguishFires(character); newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier);
break;
case "fightintruders":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
break; break;
case "steer": case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>(); var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (order.TargetItemComponent == null) return; if (order.TargetItemComponent == null) { return null; }
CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController); newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true };
break; break;
default: default:
if (order.TargetItemComponent == null) return; if (order.TargetItemComponent == null) { return null; }
CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController); newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true };
break; break;
} }
return newObjective; return newObjective;
@@ -10,8 +10,6 @@ namespace Barotrauma
{ {
public override string DebugTag => "operate item"; public override string DebugTag => "operate item";
private ItemComponent component, controller;
private ItemComponent component, controller; private ItemComponent component, controller;
private Entity operateTarget; private Entity operateTarget;
private bool isCompleted; private bool isCompleted;
@@ -20,59 +18,25 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective; private AIObjectiveGetItem getItemObjective;
private bool useController; public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
private AIObjectiveGoTo gotoObjective;
private bool useController;
private AIObjectiveGoTo gotoObjective;
private bool useController;
private AIObjectiveGoTo gotoObjective;
private bool useController;
private AIObjectiveGoTo gotoObjective;
private bool useController;
private AIObjectiveGoTo gotoObjective;
public override bool CanBeCompleted
{
get
{
if (gotoObjective != null && !gotoObjective.CanBeCompleted) return false;
if (useController && controller == null) return false;
return canBeCompleted;
}
}
public Entity OperateTarget => operateTarget; public Entity OperateTarget => operateTarget;
public ItemComponent Component => component; public ItemComponent Component => component;
public ItemComponent Component => component; public override float GetPriority()
public ItemComponent Component => component;
public ItemComponent Component => component;
public ItemComponent Component => component;
public ItemComponent Component => component;
public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (gotoObjective != null && !gotoObjective.CanBeCompleted) { return 0; } if (component.Item.ConditionPercentage <= 0) { return 0; }
if (objectiveManager.CurrentOrder == this) if (objectiveManager.CurrentOrder == this)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
} }
return base.GetPriority(objectiveManager); if (component.Item.CurrentHull == null) { return 0; }
if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return 0; }
float devotion = MathHelper.Min(10, Priority);
float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier;
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
return MathHelper.Clamp(value, 0, max);
} }
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1) public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
@@ -82,34 +46,31 @@ namespace Barotrauma
this.requireEquip = requireEquip; this.requireEquip = requireEquip;
this.operateTarget = operateTarget; this.operateTarget = operateTarget;
this.useController = useController; this.useController = useController;
if (useController) if (useController)
{ {
var controllers = component.Item.GetConnectedComponents<Controller>(); //try finding the controller with the simpler non-recursive method first
if (controllers.Any()) controller = controllers[0]; controller =
component.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
component.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
} }
canBeCompleted = true;
} }
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
ItemComponent target = useController ? controller : component; ItemComponent target = useController ? controller : component;
if (useController && controller == null) if (useController && controller == null)
{ {
character.Speak(TextManager.Get("DialogCantFindController").Replace("[item]", component.Item.Name), null, 2.0f, "cantfindcontroller", 30.0f); character.Speak(TextManager.Get("DialogCantFindController").Replace("[item]", component.Item.Name), null, 2.0f, "cantfindcontroller", 30.0f);
abandon = true;
return; return;
} }
if (target.CanBeSelected) if (target.CanBeSelected)
{ {
bool inSameRoom = character.CurrentHull == target.Item.CurrentHull; bool inSameRoom = character.CurrentHull == target.Item.CurrentHull;
bool withinReach = target.Item.IsInsideTrigger(character.WorldPosition) || Vector2.DistanceSquared(character.Position, target.Item.Position) < MathUtils.Pow(target.Item.InteractDistance, 2); bool withinReach = target.Item.IsInsideTrigger(character.WorldPosition) || Vector2.DistanceSquared(character.Position, target.Item.Position) < MathUtils.Pow(target.Item.InteractDistance, 2);
if (inSameRoom && withinReach) if (inSameRoom && withinReach)
{ {
if (character.CurrentHull == target.Item.CurrentHull) if (character.SelectedConstruction != target.Item)
{ {
if (character.SelectedConstruction != target.Item && target.CanBeSelected) if (character.SelectedConstruction != target.Item && target.CanBeSelected)
{ {
@@ -121,16 +82,22 @@ namespace Barotrauma
} }
return; return;
} }
if (component.AIOperate(deltaTime, character, this))
{
isCompleted = true;
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager));
} }
AddSubObjective(gotoObjective = new AIObjectiveGoTo(target.Item, character));
} }
else else
{ {
if (component.Item.GetComponent<Pickable>() == null) if (component.Item.GetComponent<Pickable>() == null)
{ {
//controller/target can't be selected and the item cannot be picked -> objective can't be completed //controller/target can't be selected and the item cannot be picked -> objective can't be completed
canBeCompleted = false; abandon = true;
return; return;
} }
else if (!character.Inventory.Items.Contains(component.Item)) else if (!character.Inventory.Items.Contains(component.Item))
@@ -150,11 +117,9 @@ namespace Barotrauma
#endif #endif
return; return;
} }
for (int i = 0; i < character.Inventory.Capacity; i++) for (int i = 0; i < character.Inventory.Capacity; i++)
{ {
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || if (character.Inventory.SlotTypes[i] == InvSlotType.Any || !holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
!holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
{ {
continue; continue;
} }
@@ -188,10 +153,8 @@ namespace Barotrauma
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
AIObjectiveOperateItem operateItem = otherObjective as AIObjectiveOperateItem; if (!(otherObjective is AIObjectiveOperateItem operateItem)) { return false; }
if (operateItem == null) return false; return (operateItem.component == component || otherObjective.Option == Option);
return (operateItem.component == component ||otherObjective.Option == Option);
} }
} }
} }
@@ -1,68 +1,74 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectivePumpWater : AIObjectiveLoop<Pump> class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{ {
public override string DebugTag => "pump water"; public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true; private IEnumerable<Pump> pumpList;
private readonly IEnumerable<Pump> pumpList;
public AIObjectivePumpWater(Character character, string option) : base(character, option) public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
{ : base(character, objectiveManager, priorityModifier, option) { }
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (character.Submarine == null) { return 0; }
if (objectiveManager.CurrentOrder == this && targets.Count > 0)
{
return AIObjectiveManager.OrderPriority;
}
return 0.0f;
}
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectivePumpWater && otherObjective.Option == Option; public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectivePumpWater && otherObjective.Option == Option;
//availablePumps = allPumps.Where(p => !p.Item.HasTag("ballast") && p.Item.Connections.None(c => c.IsPower && p.Item.GetConnectedComponentsRecursive<Steering>(c).None())).ToList();
protected override void FindTargets() protected override void FindTargets()
{ {
if (option == null) { return; } if (Option == null) { return; }
foreach (Item item in Item.ItemList) base.FindTargets();
if (targets.None() && objectiveManager.CurrentOrder == this)
{ {
if (item.HasTag("ballast")) { continue; } character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps", 30.0f);
if (item.Submarine == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
var pump = item.GetComponent<Pump>();
if (pump != null)
{
if (!ignoreList.Contains(pump))
{
if (option == "stoppumping")
{
if (!pump.IsActive || pump.FlowPercentage == 0.0f) { continue; }
}
else
{
if (!pump.Item.InWater) { continue; }
if (pump.IsActive && pump.FlowPercentage <= -90.0f) { continue; }
}
if (!targets.Contains(pump))
{
targets.Add(pump);
}
}
}
} }
} }
protected override bool Filter(Pump pump) => true; protected override bool Filter(Pump pump)
protected override IEnumerable<Pump> GetList() => pumpList; {
protected override AIObjective ObjectiveConstructor(Pump pump) => new AIObjectiveOperateItem(pump, character, Option, false); if (pump == null) { return false; }
protected override float Average(Pump target) => 0; if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Option == "stoppumping")
{
if (!pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0)) { return false; }
}
else
{
if (!pump.Item.InWater) { return false; }
if (pump.IsActive && pump.FlowPercentage <= -99.9f) { return false; }
}
return true;
}
protected override IEnumerable<Pump> GetList()
{
if (pumpList == null)
{
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
}
return pumpList;
}
protected override AIObjective ObjectiveConstructor(Pump pump) => new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { IsLoop = false };
protected override float TargetEvaluation()
{
if (Option == "stoppumping")
{
return targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100)));
}
else
{
return targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(-t.FlowPercentage / 100)));
}
}
} }
} }
@@ -11,37 +11,33 @@ namespace Barotrauma
{ {
public override string DebugTag => "repair item"; public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; } public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
private float previousCondition = -1; private float previousCondition = -1;
private RepairTool repairTool;
public AIObjectiveRepairItem(Character character, Item item) : base(character, "") public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{ {
Item = item; Item = item;
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority()
{ {
// TODO: priority list? // TODO: priority list?
if (Item.Repairables.None()) { return 0; }
// Ignore items that are being repaired by someone else. // Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; } if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y) * 2.0f; float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist)); float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist));
float damagePriority = MathHelper.Lerp(1, 0, (Item.Condition + 10) / Item.MaxCondition); float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = character.SelectedConstruction == Item ? 50 : 0; float isSelected = character.SelectedConstruction == Item ? 50 : 0;
float baseLevel = Math.Max(Priority + isSelected, 1); float devotion = (Math.Min(Priority, 10) + isSelected) / 100;
return MathHelper.Clamp(baseLevel * damagePriority * distanceFactor * successFactor, 0, 100); float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1));
} }
public override bool CanBeCompleted => !abandon;
public override bool IsCompleted() public override bool IsCompleted()
{ {
bool isCompleted = Item.IsFullCondition; bool isCompleted = Item.IsFullCondition;
@@ -59,15 +55,6 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (goToObjective != null && !subObjectives.Contains(goToObjective))
{
if (!goToObjective.IsCompleted() && !goToObjective.CanBeCompleted)
{
abandon = true;
character?.Speak(TextManager.Get("DialogCannotRepair").Replace("[itemname]", Item.Name), null, 0.0f, "cannotrepair", 10.0f);
}
goToObjective = null;
}
foreach (Repairable repairable in Item.Repairables) foreach (Repairable repairable in Item.Repairables)
{ {
if (!repairable.HasRequiredItems(character, false)) if (!repairable.HasRequiredItems(character, false))
@@ -77,12 +64,14 @@ namespace Barotrauma
{ {
foreach (RelatedItem requiredItem in kvp.Value) foreach (RelatedItem requiredItem in kvp.Value)
{ {
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true)); AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
} }
} }
return; return;
} }
} }
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any(so => so is AIObjectiveGetItem)) { return; }
if (repairTool == null) if (repairTool == null)
{ {
FindRepairTool(); FindRepairTool();
@@ -114,31 +103,31 @@ namespace Barotrauma
{ {
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
abandon = true; abandon = true;
character?.Speak(TextManager.Get("DialogRepairFailed").Replace("[itemname]", Item.Name), null, 0.0f, "repairfailed", 10.0f); character?.Speak(TextManager.Get("DialogCannotRepair").Replace("[itemname]", Item.Name), null, 0.0f, "cannotrepair", 10.0f);
} }
} }
repairable.CurrentFixer = abandon && repairable.CurrentFixer == character ? null : character; repairable.CurrentFixer = abandon && repairable.CurrentFixer == character ? null : character;
break; break;
} }
} }
else if (goToObjective == null || goToObjective.Target != Item) else
{ {
previousCondition = -1; // If cannot reach the item, approach it.
if (goToObjective != null) TryAddSubObjective(ref goToObjective,
{ constructor: () =>
subObjectives.Remove(goToObjective); {
} previousCondition = -1;
goToObjective = new AIObjectiveGoTo(Item, character); var objective = new AIObjectiveGoTo(Item, character, objectiveManager);
if (repairTool != null) if (repairTool != null)
{ {
//goToObjective.CloseEnough = (HumanAIController.AnimController.ArmLength + ConvertUnits.ToSimUnits(repairTool.Range)) * 0.75f; objective.CloseEnough = ConvertUnits.ToSimUnits(repairTool.Range) * 0.75f;
goToObjective.CloseEnough = ConvertUnits.ToSimUnits(repairTool.Range); }
} return objective;
AddSubObjective(goToObjective); },
onAbandon: () => character.Speak(TextManager.Get("DialogCannotRepair").Replace("[itemname]", Item.Name), null, 0.0f, "cannotrepair", 10.0f));
} }
} }
private RepairTool repairTool;
private void FindRepairTool() private void FindRepairTool()
{ {
foreach (Repairable repairable in Item.Repairables) foreach (Repairable repairable in Item.Repairables)
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Extensions; using Barotrauma.Extensions;
@@ -7,18 +8,25 @@ namespace Barotrauma
class AIObjectiveRepairItems : AIObjectiveLoop<Item> class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{ {
public override string DebugTag => "repair items"; public override string DebugTag => "repair items";
public override bool KeepDivingGearOn => true;
/// <summary> /// <summary>
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item /// Should the character only attempt to fix items they have the skills to fix, or any damaged item
/// </summary> /// </summary>
public bool RequireAdequateSkills; public bool RequireAdequateSkills;
public AIObjectiveRepairItems(Character character) : base(character, "") { } public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
// TODO: This can allow two active repair items objectives, if RequireAdequateSkills is not at the same value. We don't want that.
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills; public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
protected override void FindTargets()
{
base.FindTargets();
if (targets.None() && objectiveManager.CurrentOrder == this)
{
character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets", 30.0f);
}
}
protected override void CreateObjectives() protected override void CreateObjectives()
{ {
foreach (var item in targets) foreach (var item in targets)
@@ -38,37 +46,35 @@ namespace Barotrauma
protected override bool Filter(Item item) protected override bool Filter(Item item)
{ {
bool ignore = ignoreList.Contains(item) || item.IsFullCondition; if (!IsValidTarget(item, character)) { return false; }
if (!ignore) if (item.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (!objectives.ContainsKey(item))
{ {
if (item.Submarine == null) { ignore = true; } if (item.Repairables.All(r => item.Condition > r.ShowRepairUIThreshold)) { return false; }
else if (item.Submarine.TeamID != character.TeamID) { ignore = true; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { ignore = true; }
else
{
if (item.Repairables.None()) { ignore = true; }
else
{
foreach (Repairable repairable in item.Repairables)
{
if (!objectives.ContainsKey(item) && item.Condition > repairable.ShowRepairUIThreshold)
{
ignore = true;
}
else if (RequireAdequateSkills && !repairable.HasRequiredSkills(character))
{
ignore = true;
}
if (ignore) { break; }
}
}
}
} }
return ignore; if (RequireAdequateSkills)
{
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
}
return true;
} }
protected override float Average(Item item) => 100 - item.ConditionPercentage; protected override float TargetEvaluation() => targets.Max(t => 100 - t.ConditionPercentage);
protected override IEnumerable<Item> GetList() => Item.ItemList; protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item); protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.Repairables.None()) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
return true;
}
} }
} }
@@ -11,7 +11,6 @@ namespace Barotrauma
{ {
public override string DebugTag => "rescue"; public override string DebugTag => "rescue";
public override bool ForceRun => true; public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f; const float TreatmentDelay = 0.5f;
@@ -21,26 +20,11 @@ namespace Barotrauma
private float treatmentTimer; private float treatmentTimer;
public override bool CanBeCompleted public override bool CanBeCompleted => base.CanBeCompleted && AIObjectiveRescueAll.IsValidTarget(targetCharacter, character);
{
get
{
if (targetCharacter.Removed ||
targetCharacter.IsDead ||
targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.VitalityThreshold)
{
return false;
}
if (goToObjective != null && !goToObjective.CanBeCompleted) { return false; }
return true; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
} : base(character, objectiveManager, priorityModifier)
}
public AIObjectiveRescue(Character character, Character targetCharacter)
: base(character, "")
{ {
Debug.Assert(character != targetCharacter);
this.targetCharacter = targetCharacter; this.targetCharacter = targetCharacter;
} }
@@ -52,14 +36,19 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
//target in water -> move to a dry place first // Target is not in a safe place -> Move to a safe place first
if (targetCharacter.AnimController.InWater) if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{ {
if (character.SelectedCharacter != targetCharacter) if (character.SelectedCharacter != targetCharacter)
{ {
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter)) if (!character.CanInteractWith(targetCharacter))
{ {
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character)); if (goToObjective != null && goToObjective.Target != targetCharacter)
{
goToObjective = null;
}
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager));
} }
else else
{ {
@@ -68,15 +57,28 @@ namespace Barotrauma
} }
else else
{ {
AddSubObjective(new AIObjectiveFindSafety(character)); // Drag the character into safety
if (goToObjective != null && goToObjective.Target == targetCharacter)
{
goToObjective = null;
}
var findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
if (findSafety == null)
{
// Ensure that we have the find safety objective (should always be the case)
objectiveManager.AddObjective(new AIObjectiveFindSafety(character, objectiveManager));
}
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(findSafety.FindBestHull(), character, objectiveManager));
} }
return; return;
} }
//target not in water -> we can start applying treatment if (subObjectives.Any()) { return; }
// We can start applying treatment
if (!character.CanInteractWith(targetCharacter)) if (!character.CanInteractWith(targetCharacter))
{ {
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character)); TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager));
} }
else else
{ {
@@ -87,31 +89,12 @@ namespace Barotrauma
null, 1.0f, null, 1.0f,
"foundunconscioustarget" + targetCharacter.Name, 60.0f); "foundunconscioustarget" + targetCharacter.Name, 60.0f);
} }
character.SelectCharacter(targetCharacter); character.SelectCharacter(targetCharacter);
GiveTreatment(deltaTime); GiveTreatment(deltaTime);
} }
} }
protected override bool ShouldInterruptSubObjective(AIObjective subObjective) // TODO: consider optimizing a bit
{
if (subObjective is AIObjectiveFindSafety)
{
if (character.SelectedCharacter != targetCharacter) return true;
if (character.AnimController.InWater || targetCharacter.AnimController.InWater) return false;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (!Submarine.RectContains(targetCharacter.CurrentHull.WorldRect, limb.WorldPosition)) return false;
}
return !character.AnimController.InWater && !targetCharacter.AnimController.InWater &&
HumanAIController.GetHullSafety(character.CurrentHull, character) > HumanAIController.HULL_SAFETY_THRESHOLD;
}
return false;
}
private void GiveTreatment(float deltaTime) private void GiveTreatment(float deltaTime)
{ {
if (treatmentTimer > 0.0f) if (treatmentTimer > 0.0f)
@@ -128,7 +111,6 @@ namespace Barotrauma
{ {
return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth)); return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth));
}); });
//check if we already have a suitable treatment for any of the afflictions //check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions) foreach (Affliction affliction in allAfflictions)
{ {
@@ -143,7 +125,6 @@ namespace Barotrauma
} }
} }
} }
//didn't have any suitable treatments available, try to find some medical items //didn't have any suitable treatments available, try to find some medical items
HashSet<string> suitableItemIdentifiers = new HashSet<string>(); HashSet<string> suitableItemIdentifiers = new HashSet<string>();
foreach (Affliction affliction in allAfflictions) foreach (Affliction affliction in allAfflictions)
@@ -166,9 +147,8 @@ namespace Barotrauma
itemNameList.Add(itemPrefab.Name); itemNameList.Add(itemPrefab.Name);
} }
//only list the first 4 items //only list the first 4 items
if (itemNameList.Count >= 4) break; if (itemNameList.Count >= 4) { break; }
} }
if (itemNameList.Count > 0) if (itemNameList.Count > 0)
{ {
string itemListStr = ""; string itemListStr = "";
@@ -185,30 +165,29 @@ namespace Barotrauma
.Replace("[treatmentlist]", itemListStr), .Replace("[treatmentlist]", itemListStr),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f); null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
} }
character.DeselectCharacter(); character.DeselectCharacter();
AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), true)); AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true));
} }
character.AnimController.Anim = AnimController.Animation.CPR; character.AnimController.Anim = AnimController.Animation.CPR;
} }
private void ApplyTreatment(Affliction affliction, Item item) private void ApplyTreatment(Affliction affliction, Item item)
{ {
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction); var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
bool remove = false; bool remove = false;
foreach (ItemComponent ic in item.Components) foreach (ItemComponent ic in item.Components)
{ {
if (!ic.HasRequiredContainedItems(addMessage: false)) continue; if (!ic.HasRequiredContainedItems(addMessage: false)) { continue; }
#if CLIENT #if CLIENT
ic.PlaySound(ActionType.OnUse, character.WorldPosition, character); ic.PlaySound(ActionType.OnUse, character.WorldPosition, character);
#endif #endif
ic.WasUsed = true; ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb); ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
if (ic.DeleteOnUse) remove = true; if (ic.DeleteOnUse)
{
remove = true;
}
} }
if (remove) if (remove)
{ {
Entity.Spawner?.AddToRemoveQueue(item); Entity.Spawner?.AddToRemoveQueue(item);
@@ -217,28 +196,26 @@ namespace Barotrauma
public override bool IsCompleted() public override bool IsCompleted()
{ {
bool isCompleted = bool isCompleted = targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.VitalityThreshold;
targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.VitalityThreshold;
if (isCompleted) if (isCompleted)
{ {
character?.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name), character?.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
} }
return isCompleted || targetCharacter.Removed || targetCharacter.IsDead;
return isCompleted || targetCharacter.IsDead;
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority()
{ {
if (targetCharacter.AnimController.CurrentHull == null || targetCharacter.IsDead) { return 0.0f; } if (targetCharacter.CurrentHull == null || targetCharacter.IsDead) { return 0; ; }
// Don't go into rooms that have enemies
Vector2 diff = targetCharacter.WorldPosition - character.WorldPosition; if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(c))) { return 0; }
float distance = Math.Abs(diff.X) + Math.Abs(diff.Y); // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float vitalityFactor = (targetCharacter.MaxVitality - targetCharacter.Vitality) / targetCharacter.MaxVitality; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist));
float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter);
return 1000.0f * vitalityFactor / distance; float devotion = Math.Max(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1));
} }
} }
} }
@@ -4,21 +4,17 @@ using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveRescueAll : AIObjective class AIObjectiveRescueAll : AIObjectiveLoop<Character>
{ {
public override string DebugTag => "rescue all"; public override string DebugTag => "rescue all";
public override bool KeepDivingGearOn => true;
//only treat characters whose vitality is below this (0.8 = 80% of max vitality) //only treat characters whose vitality is below this (0.8 = 80% of max vitality)
public const float VitalityThreshold = 0.8f; public const float VitalityThreshold = 0.8f;
private List<Character> rescueTargets; public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
public AIObjectiveRescueAll(Character character) : base (character, "") public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRescueAll;
{
rescueTargets = new List<Character>();
}
protected override void FindTargets() protected override void FindTargets()
{ {
@@ -29,31 +25,13 @@ namespace Barotrauma
} }
} }
public override float GetPriority(AIObjectiveManager objectiveManager) protected override bool Filter(Character target) => IsValidTarget(target, character);
{
if (character.Submarine == null) { return 0; }
GetRescueTargets();
if (!rescueTargets.Any()) { return 0.0f; }
if (objectiveManager.CurrentOrder == this) protected override IEnumerable<Character> GetList() => Character.CharacterList;
{
return AIObjectiveManager.OrderPriority;
}
//if there are targets to rescue, the priority is slightly less protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
//than the priority of explicit orders given to the character
return AIObjectiveManager.OrderPriority - 5.0f;
}
private void GetRescueTargets() protected override float TargetEvaluation() => targets.Max(t => GetVitalityFactor(t)) * 100;
{
rescueTargets = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
c.TeamID == character.TeamID &&
c != character &&
!c.IsDead &&
c.Vitality / c.MaxVitality < VitalityThreshold);
}
public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality; public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality;
@@ -69,8 +47,5 @@ namespace Barotrauma
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
return true; return true;
} }
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
} }
} }
@@ -64,8 +64,7 @@ namespace Barotrauma
private Order(XElement orderElement) private Order(XElement orderElement)
{ {
AITag = orderElement.GetAttributeString("aitag", ""); AITag = orderElement.GetAttributeString("aitag", "");
Name = TextManager.Get("OrderName." + AITag, true) ?? orderElement.GetAttributeString("name", "Name not found"); Name = TextManager.Get("OrderName." + AITag, true) ?? "Name not found";
DoingText = TextManager.Get("OrderNameDoing." + AITag, true) ?? orderElement.GetAttributeString("doingtext", "");
string targetItemType = orderElement.GetAttributeString("targetitemtype", ""); string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType)) if (!string.IsNullOrWhiteSpace(targetItemType))
@@ -127,7 +126,6 @@ namespace Barotrauma
Name = prefab.Name; Name = prefab.Name;
AITag = prefab.AITag; AITag = prefab.AITag;
DoingText = prefab.DoingText;
ItemComponentType = prefab.ItemComponentType; ItemComponentType = prefab.ItemComponentType;
Options = prefab.Options; Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite; SymbolSprite = prefab.SymbolSprite;
@@ -142,8 +140,10 @@ namespace Barotrauma
{ {
if (UseController) if (UseController)
{ {
var controllers = targetItem.Item.GetConnectedComponents<Controller>(); //try finding the controller with the simpler non-recursive method first
if (controllers.Count > 0) ConnectedController = controllers[0]; ConnectedController =
targetItem.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetItem.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
} }
TargetEntity = targetItem.Item; TargetEntity = targetItem.Item;
TargetItemComponent = targetItem; TargetItemComponent = targetItem;
@@ -233,7 +233,7 @@ namespace Barotrauma
//} //}
var body = Submarine.PickBody(end, node.Waypoint.SimPosition, null, var body = Submarine.PickBody(end, node.Waypoint.SimPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs | Physics.CollisionPlatform); Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
if (body != null) if (body != null)
{ {
@@ -24,7 +24,10 @@ namespace Barotrauma
{ {
public static List<JobPrefab> List; public static List<JobPrefab> List;
public List<SkillPrefab> Skills; public readonly XElement Items;
public readonly List<string> ItemNames = new List<string>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutomaticOrders = new List<AutonomousObjective>();
[Serialize("1,1,1,1", false)] [Serialize("1,1,1,1", false)]
public Color UIColor public Color UIColor
@@ -118,14 +121,6 @@ namespace Barotrauma
public XElement ClothingElement { get; private set; } public XElement ClothingElement { get; private set; }
public JobPrefab(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier);
public XElement ClothingElement { get; private set; }
public JobPrefab(XElement element) public JobPrefab(XElement element)
{ {
SerializableProperty.DeserializeProperties(this, element); SerializableProperty.DeserializeProperties(this, element);
@@ -173,8 +173,8 @@ namespace Barotrauma
var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition, var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition,
Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end")); Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end"));
InitializeWatchman(spawnedCharacter); InitializeWatchman(spawnedCharacter);
(spawnedCharacter.AIController as HumanAIController)?.ObjectiveManager.SetOrder( var objectiveManager = (spawnedCharacter.AIController as HumanAIController)?.ObjectiveManager;
new AIObjectiveGoTo(watchmanSpawnpoint, spawnedCharacter, repeat: true, getDivingGearIfNeeded: false)); objectiveManager?.SetOrder(new AIObjectiveGoTo(watchmanSpawnpoint, spawnedCharacter, objectiveManager, repeat: true, getDivingGearIfNeeded: false));
if (watchmanJob != null) if (watchmanJob != null)
{ {
spawnedCharacter.GiveJobItems(); spawnedCharacter.GiveJobItems();
@@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components
private bool autoOrientGap; private bool autoOrientGap;
private bool isStuck; private bool isStuck;
public bool IsStuck => isStuck;
private float resetPredictionTimer; private float resetPredictionTimer;
private Rectangle doorRect; private Rectangle doorRect;
@@ -227,8 +229,7 @@ namespace Barotrauma.Items.Components
{ {
msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText); msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText);
} }
if (isBroken) { return true; } return isBroken || base.HasRequiredItems(character, addMessage, msg);
return base.HasRequiredItems(character, addMessage, msg);
} }
public override bool Pick(Character picker) public override bool Pick(Character picker)
@@ -281,7 +281,7 @@ namespace Barotrauma.Items.Components
float dist = fromItemToLeak.Length(); float dist = fromItemToLeak.Length();
//too far away -> consider this done and hope the AI is smart enough to move closer //too far away -> consider this done and hope the AI is smart enough to move closer
if (dist > Range * 5.0f) return true; if (dist > Range * 3.0f) { return true; }
// TODO: use the collider size? // TODO: use the collider size?
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
@@ -344,7 +344,19 @@ namespace Barotrauma.Items.Components
character.CursorPosition = leak.Position + VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime), dist); character.CursorPosition = leak.Position + VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime), dist);
if (item.RequireAimToUse) if (item.RequireAimToUse)
{ {
character.SetInput(InputType.Aim, false, true); bool isOperatingButtons = false;
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
{
var door = indoorSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
} }
// Press the trigger only when the tool is approximately facing the target. // Press the trigger only when the tool is approximately facing the target.
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
@@ -400,7 +412,8 @@ namespace Barotrauma.Items.Components
object value = property.GetValue(target); object value = property.GetValue(target);
if (value.GetType() == typeof(float)) if (value.GetType() == typeof(float))
{ {
user.UpdateHUDProgressBar(door, door.Item.WorldPosition, (float)value / 100, Color.DarkGray * 0.5f, Color.White); var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, (float)value / 100, Color.DarkGray * 0.5f, Color.White);
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
} }
} }
} }
@@ -734,6 +734,25 @@ namespace Barotrauma.Items.Components
} }
} }
if (targetItem.Prefab.DeconstructItems.Any())
{
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddToRemoveQueue(targetItem);
MoveInputQueue();
PutItemsToLinkedContainer();
}
else
{
if (outputContainer.Inventory.Items.All(i => i != null))
{
targetItem.Drop(dropper: null);
}
else
{
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
}
}
if (targetItem.Prefab.DeconstructItems.Any()) if (targetItem.Prefab.DeconstructItems.Any())
{ {
inputContainer.Inventory.RemoveItem(targetItem); inputContainer.Inventory.RemoveItem(targetItem);
@@ -499,7 +499,7 @@ namespace Barotrauma.Items.Components
//load more fuel if the current maximum output is only 50% of the current load //load more fuel if the current maximum output is only 50% of the current load
if (NeedMoreFuel(minimumOutputRatio: 0.5f)) if (NeedMoreFuel(minimumOutputRatio: 0.5f))
{ {
var containFuelObjective = new AIObjectiveContainItem(character, new string[] { "fuelrod", "reactorfuel" }, item.GetComponent<ItemContainer>()) var containFuelObjective = new AIObjectiveContainItem(character, new string[] { "fuelrod", "reactorfuel" }, item.GetComponent<ItemContainer>(), objective.objectiveManager)
{ {
MinContainedAmount = item.ContainedItems.Count(i => i != null && i.Prefab.Identifier == "fuelrod" || i.HasTag("reactorfuel")) + 1, MinContainedAmount = item.ContainedItems.Count(i => i != null && i.Prefab.Identifier == "fuelrod" || i.HasTag("reactorfuel")) + 1,
GetItemPriority = (Item fuelItem) => GetItemPriority = (Item fuelItem) =>
@@ -212,6 +212,33 @@ namespace Barotrauma.Items.Components
} }
} }
public Vector2? PosToMaintain
{
get { return posToMaintain; }
set { posToMaintain = value; }
}
struct ObstacleDebugInfo
{
public Vector2 Point1;
public Vector2 Point2;
public Vector2? Intersection;
public float Dot;
public Vector2 AvoidStrength;
public ObstacleDebugInfo(GraphEdge edge, Vector2? intersection, float dot, Vector2 avoidStrength)
{
Point1 = edge.Point1;
Point2 = edge.Point2;
Intersection = intersection;
Dot = dot;
AvoidStrength = avoidStrength;
}
}
//edge point 1, edge point 2, avoid strength //edge point 1, edge point 2, avoid strength
private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>(); private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>();
@@ -235,12 +235,12 @@ namespace Barotrauma.Items.Components
if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge") if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge")
{ {
if (Math.Abs(rechargeSpeed - maxRechargeSpeed * 0.5f) > 0.05f) if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f)
{ {
#if SERVER #if SERVER
item.CreateServerEvent(this); item.CreateServerEvent(this);
#endif #endif
RechargeSpeed = maxRechargeSpeed * 0.5f; RechargeSpeed = maxRechargeSpeed * aiRechargeTargetRatio;
#if CLIENT #if CLIENT
rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f);
#endif #endif
@@ -415,7 +415,7 @@ namespace Barotrauma.Items.Components
if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f) if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f)
{ {
objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, "", false)); objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: "", requireEquip: false));
return false; return false;
} }
} }
@@ -454,11 +454,11 @@ namespace Barotrauma.Items.Components
if (container.Inventory.Items[0] != null && container.Inventory.Items[0].Condition <= 0.0f) if (container.Inventory.Items[0] != null && container.Inventory.Items[0].Condition <= 0.0f)
{ {
var removeShellObjective = new AIObjectiveDecontainItem(character, container.Inventory.Items[0], container); var removeShellObjective = new AIObjectiveDecontainItem(character, container.Inventory.Items[0], container, objective.objectiveManager);
objective.AddSubObjective(removeShellObjective); objective.AddSubObjective(removeShellObjective);
} }
var containShellObjective = new AIObjectiveContainItem(character, container.ContainableItems[0].Identifiers[0], container); var containShellObjective = new AIObjectiveContainItem(character, container.ContainableItems[0].Identifiers[0], container, objective.objectiveManager);
character?.Speak(TextManager.Get("DialogLoadTurret").Replace("[itemname]", item.Name), null, 0.0f, "loadturret", 30.0f); character?.Speak(TextManager.Get("DialogLoadTurret").Replace("[itemname]", item.Name), null, 0.0f, "loadturret", 30.0f);
containShellObjective.MinContainedAmount = usableProjectileCount + 1; containShellObjective.MinContainedAmount = usableProjectileCount + 1;
containShellObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; containShellObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier };
@@ -1668,6 +1668,10 @@ namespace Barotrauma
{ {
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
} }
if (!broken)
{
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
}
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { return; } if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { return; }
@@ -1804,6 +1808,9 @@ namespace Barotrauma
} }
} }
/// <summary>
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
/// </summary>
public List<T> GetConnectedComponents<T>(bool recursive = false) where T : ItemComponent public List<T> GetConnectedComponents<T>(bool recursive = false) where T : ItemComponent
{ {
List<T> connectedComponents = new List<T>(); List<T> connectedComponents = new List<T>();
@@ -1812,7 +1819,6 @@ namespace Barotrauma
{ {
HashSet<Connection> alreadySearched = new HashSet<Connection>(); HashSet<Connection> alreadySearched = new HashSet<Connection>();
GetConnectedComponentsRecursive(alreadySearched, connectedComponents); GetConnectedComponentsRecursive(alreadySearched, connectedComponents);
return connectedComponents; return connectedComponents;
} }
@@ -1841,27 +1847,13 @@ namespace Barotrauma
{ {
if (alreadySearched.Contains(c)) { continue; } if (alreadySearched.Contains(c)) { continue; }
alreadySearched.Add(c); alreadySearched.Add(c);
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents);
var recipients = c.Recipients;
foreach (Connection recipient in recipients)
{
if (alreadySearched.Contains(recipient)) { continue; }
var component = recipient.Item.GetComponent<T>();
if (component != null)
{
var receiverConnections = wifiReceiver.Item.Connections;
if (receiverConnections == null) { continue; }
foreach (Connection wifiOutput in receiverConnections)
{
if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; }
GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents);
}
}
recipient.Item.GetConnectedComponentsRecursive<T>(alreadySearched, connectedComponents);
}
} }
} }
/// <summary>
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
/// </summary>
public List<T> GetConnectedComponentsRecursive<T>(Connection c) where T : ItemComponent public List<T> GetConnectedComponentsRecursive<T>(Connection c) where T : ItemComponent
{ {
List<T> connectedComponents = new List<T>(); List<T> connectedComponents = new List<T>();
@@ -1891,13 +1883,28 @@ namespace Barotrauma
foreach (Connection recipient in recipients) foreach (Connection recipient in recipients)
{ {
if (alreadySearched.Contains(recipient)) { continue; } if (alreadySearched.Contains(recipient)) { continue; }
var component = recipient.Item.GetComponent<T>(); var component = recipient.Item.GetComponent<T>();
if (component != null) if (component != null)
{ {
connectedComponents.Add(component); connectedComponents.Add(component);
} }
//connected to a wifi component -> see which other wifi components it can communicate with
var wifiComponent = recipient.Item.GetComponent<WifiComponent>();
if (wifiComponent != null && wifiComponent.CanTransmit())
{
foreach (var wifiReceiver in wifiComponent.GetReceiversInRange())
{
var receiverConnections = wifiReceiver.Item.Connections;
if (receiverConnections == null) { continue; }
foreach (Connection wifiOutput in receiverConnections)
{
if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; }
GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents);
}
}
}
recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents); recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents);
} }
@@ -2200,29 +2207,6 @@ namespace Barotrauma
if (remove) { Spawner?.AddToRemoveQueue(this); } if (remove) { Spawner?.AddToRemoveQueue(this); }
} }
List<ColoredText> texts = new List<ColoredText>();
public List<ColoredText> GetHUDTexts(Character character)
{
texts.Clear();
foreach (ItemComponent ic in components)
{
if (string.IsNullOrEmpty(ic.DisplayMsg)) continue;
if (!ic.CanBePicked && !ic.CanBeSelected) continue;
if (ic is Holdable holdable && !holdable.CanBeDeattached()) continue;
Color color = Color.Gray;
bool hasRequiredSkillsAndItems = ic.HasRequiredSkills(character) && ic.HasRequiredItems(character, false);
if (hasRequiredSkillsAndItems)
{
color = Color.Cyan;
}
texts.Add(new ColoredText(ic.DisplayMsg, color, false));
}
if (remove) { Spawner?.AddToRemoveQueue(this); }
}
public bool Combine(Item item) public bool Combine(Item item)
{ {
bool isCombined = false; bool isCombined = false;
@@ -360,6 +360,25 @@ namespace Barotrauma
} }
} }
public string DisplayName
{
get;
private set;
}
private string roomName;
[Editable, Serialize("", true, translationTextTag: "RoomName.")]
public string RoomName
{
get { return roomName; }
set
{
if (roomName == value) { return; }
roomName = value;
DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName;
}
}
public override Rectangle Rect public override Rectangle Rect
{ {
get get