409d4d9...aeafa16 (merge human-ai)

This commit is contained in:
Joonas Rikkonen
2019-03-18 22:52:17 +02:00
parent 80ab27df22
commit 3301bed442
88 changed files with 2334 additions and 1657 deletions
@@ -15,7 +15,6 @@
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\AITarget.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\AITarget.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\EnemyAIController.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\EnemyAIController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\HumanAIController.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\HumanAIController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\HumanoidAnimParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Ragdoll.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Ragdoll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Attack.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Attack.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Character.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Character.cs" />
@@ -65,10 +65,11 @@ namespace Barotrauma
{ {
GUI.DrawLine(spriteBatch, pos, GUI.DrawLine(spriteBatch, pos,
ConvertUnits.ToDisplayUnits(new Vector2(latchOntoAI.WallAttachPos.Value.X, -latchOntoAI.WallAttachPos.Value.Y)), Color.Orange * 0.6f, 0, 3); ConvertUnits.ToDisplayUnits(new Vector2(latchOntoAI.WallAttachPos.Value.X, -latchOntoAI.WallAttachPos.Value.Y)), Color.Orange * 0.6f, 0, 3);
} }
} }
GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3);
IndoorsSteeringManager pathSteering = steeringManager as IndoorsSteeringManager; IndoorsSteeringManager pathSteering = steeringManager as IndoorsSteeringManager;
if (pathSteering == null || pathSteering.CurrentPath == null || pathSteering.CurrentPath.CurrentNode == null) return; if (pathSteering == null || pathSteering.CurrentPath == null || pathSteering.CurrentPath.CurrentNode == null) return;
@@ -1,6 +1,5 @@
using Barotrauma.Networking; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework; using FarseerPhysics;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -23,34 +22,70 @@ namespace Barotrauma
public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{ {
Vector2 pos = Character.WorldPosition;
pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -120);
if (SelectedAiTarget?.Entity != null) if (SelectedAiTarget?.Entity != null)
{ {
GUI.DrawLine(spriteBatch, //GUI.DrawLine(spriteBatch, pos, new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), Color.Red);
new Vector2(Character.DrawPosition.X, -Character.DrawPosition.Y), //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black);
new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), Color.Red);
} }
IndoorsSteeringManager pathSteering = steeringManager as IndoorsSteeringManager; if (ObjectiveManager != null)
if (pathSteering == null || pathSteering.CurrentPath == null || pathSteering.CurrentPath.CurrentNode == null) return;
GUI.DrawLine(spriteBatch,
new Vector2(Character.DrawPosition.X, -Character.DrawPosition.Y),
new Vector2(pathSteering.CurrentPath.CurrentNode.DrawPosition.X, -pathSteering.CurrentPath.CurrentNode.DrawPosition.Y),
Color.LightGreen * 0.5f, 0, 3);
for (int i = 1; i < pathSteering.CurrentPath.Nodes.Count; i++)
{ {
GUI.DrawLine(spriteBatch, var currentOrder = ObjectiveManager.CurrentOrder;
new Vector2(pathSteering.CurrentPath.Nodes[i].DrawPosition.X, -pathSteering.CurrentPath.Nodes[i].DrawPosition.Y), if (currentOrder != null)
new Vector2(pathSteering.CurrentPath.Nodes[i - 1].DrawPosition.X, -pathSteering.CurrentPath.Nodes[i - 1].DrawPosition.Y), {
Color.LightGreen * 0.5f, 0, 3); GUI.DrawString(spriteBatch, pos + textOffset, $"ORDER: {currentOrder.DebugTag} ({currentOrder.GetPriority(ObjectiveManager).FormatZeroDecimal()})", Color.White, Color.Black);
}
GUI.SmallFont.DrawString(spriteBatch, var currentObjective = ObjectiveManager.CurrentObjective;
pathSteering.CurrentPath.Nodes[i].ID.ToString(), if (currentObjective != null)
new Vector2(pathSteering.CurrentPath.Nodes[i].DrawPosition.X, -pathSteering.CurrentPath.Nodes[i].DrawPosition.Y - 10), {
Color.LightGreen); GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.GetPriority(ObjectiveManager).FormatZeroDecimal()})", Color.White, Color.Black);
var subObjective = currentObjective.CurrentSubObjective;
if (subObjective != null)
{
GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.GetPriority(ObjectiveManager).FormatZeroDecimal()})", Color.White, Color.Black);
}
}
} }
if (steeringManager is IndoorsSteeringManager pathSteering)
{
var path = pathSteering.CurrentPath;
if (path != null)
{
if (path.CurrentNode != null)
{
GUI.DrawLine(spriteBatch, pos,
new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y),
Color.BlueViolet, 0, 3);
GUI.DrawString(spriteBatch, pos + textOffset - new Vector2(0, 20), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f);
}
for (int i = 1; i < path.Nodes.Count; i++)
{
var previousNode = path.Nodes[i - 1];
var currentNode = path.Nodes[i];
GUI.DrawLine(spriteBatch,
new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y),
new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y),
Color.Blue * 0.5f, 0, 3);
GUI.SmallFont.DrawString(spriteBatch,
currentNode.ID.ToString(),
new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y - 10),
Color.LightGreen);
}
}
}
GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3);
//if (Character.IsKeyDown(InputType.Aim))
//{
// GUI.DrawLine(spriteBatch, pos, new Vector2(Character.CursorWorldPosition.X, -Character.CursorWorldPosition.Y), Color.Yellow, width: 4);
//}
} }
//TODO: move this to the shared project, otherwise bots won't be able to report things in multiplayer //TODO: move this to the shared project, otherwise bots won't be able to report things in multiplayer
@@ -441,7 +441,7 @@ namespace Barotrauma
} }
Collider.DebugDraw(spriteBatch, frozen ? Color.Red : (inWater ? Color.SkyBlue : Color.Gray)); Collider.DebugDraw(spriteBatch, frozen ? Color.Red : (inWater ? Color.SkyBlue : Color.Gray));
GUI.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.ToString(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); GUI.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange);
foreach (RevoluteJoint joint in LimbJoints) foreach (RevoluteJoint joint in LimbJoints)
{ {
@@ -171,7 +171,7 @@ namespace Barotrauma
float alpha = Math.Min((1000.0f - dist) / 1000.0f * 2.0f, 1.0f); float alpha = Math.Min((1000.0f - dist) / 1000.0f * 2.0f, 1.0f);
if (alpha <= 0.0f) continue; if (alpha <= 0.0f) continue;
GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, GUI.BrokenIcon, GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, GUI.BrokenIcon,
Color.Lerp(Color.DarkRed, Color.Orange * 0.5f, brokenItem.Condition / 100.0f) * alpha); Color.Lerp(Color.DarkRed, Color.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha);
} }
if (!character.IsUnconscious && character.Stun <= 0.0f) if (!character.IsUnconscious && character.Stun <= 0.0f)
@@ -492,16 +492,12 @@ namespace Barotrauma
} }
foreach (var modifier in damageModifiers) foreach (var modifier in damageModifiers)
{ {
//float midAngle = MathUtils.GetMidAngle(modifier.ArmorSector.X, modifier.ArmorSector.Y); float rotation = -body.TransformedRotation + GetArmorSectorRotationOffset(modifier.ArmorSector) * Dir;
//float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation); Vector2 forward = VectorExtensions.Forward(rotation);
//float offset = -body.Rotation - (midAngle + spritesheetOrientation) * Dir;
float offset = GetArmorSectorRotationOffset(modifier.ArmorSector, -body.Rotation);
Vector2 forward = Vector2.Transform(-Vector2.UnitY, Matrix.CreateRotationZ(offset));
float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2);
color = modifier.DamageMultiplier > 1 ? Color.Red : Color.GreenYellow; color = modifier.DamageMultiplier > 1 ? Color.Red : Color.GreenYellow;
GUI.DrawLine(spriteBatch, bodyDrawPos, bodyDrawPos + Vector2.Normalize(forward) * size, color, width: (int)Math.Round(4 / cam.Zoom)); GUI.DrawLine(spriteBatch, bodyDrawPos, bodyDrawPos + Vector2.Normalize(forward) * size, color, width: (int)Math.Round(4 / cam.Zoom));
if (Dir == -1) { offset += MathHelper.Pi; } ShapeExtensions.DrawSector(spriteBatch, bodyDrawPos, size, GetArmorSectorSize(modifier.ArmorSector) * Dir, 40, color, rotation + MathHelper.Pi, thickness: 2 / cam.Zoom);
ShapeExtensions.DrawSector(spriteBatch, bodyDrawPos, size, GetArmorSectorSize(modifier.ArmorSector) * Dir, 40, color, offset + MathHelper.Pi, thickness: 2 / cam.Zoom);
} }
} }
} }
@@ -3063,6 +3063,189 @@ namespace Barotrauma
TextManager.WriteToCSV(); TextManager.WriteToCSV();
NPCConversation.WriteToCSV(); NPCConversation.WriteToCSV();
})); }));
#endif
commands.Add(new Command("cleanbuild", "", (string[] args) =>
{
GameMain.Config.MusicVolume = 0.5f;
GameMain.Config.SoundVolume = 0.5f;
NewMessage("Music and sound volume set to 0.5", Color.Green);
commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) =>
{
float defaultZoom = Screen.Selected.Cam.DefaultZoom;
if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out defaultZoom);
float zoomSmoothness = Screen.Selected.Cam.ZoomSmoothness;
if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out zoomSmoothness);
float moveSmoothness = Screen.Selected.Cam.MoveSmoothness;
if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out moveSmoothness);
float minZoom = Screen.Selected.Cam.MinZoom;
if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out minZoom);
float maxZoom = Screen.Selected.Cam.MaxZoom;
if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out maxZoom);
Screen.Selected.Cam.DefaultZoom = defaultZoom;
Screen.Selected.Cam.ZoomSmoothness = zoomSmoothness;
Screen.Selected.Cam.MoveSmoothness = moveSmoothness;
Screen.Selected.Cam.MinZoom = minZoom;
Screen.Selected.Cam.MaxZoom = maxZoom;
}));
commands.Add(new Command("waterparams", "waterparams [distortionscalex] [distortionscaley] [distortionstrengthx] [distortionstrengthy] [bluramount]: default 0.5 0.5 0.5 0.5 1", (string[] args) =>
{
float distortScaleX = 0.5f, distortScaleY = 0.5f;
float distortStrengthX = 0.5f, distortStrengthY = 0.5f;
float blurAmount = 0.0f;
if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out distortScaleX);
if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out distortScaleY);
if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out distortStrengthX);
if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out distortStrengthY);
if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out blurAmount);
WaterRenderer.DistortionScale = new Vector2(distortScaleX, distortScaleY);
WaterRenderer.DistortionStrength = new Vector2(distortStrengthX, distortStrengthY);
WaterRenderer.BlurAmount = blurAmount;
}));
commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) =>
{
//TODO: maybe do this automatically during loading when possible?
if (Screen.Selected == GameMain.SubEditorScreen)
{
if (!MapEntity.SelectedAny)
{
ThrowError("You have to select item(s) first!");
}
else
{
foreach (var mapEntity in MapEntity.SelectedList)
{
if (mapEntity is Item item)
{
item.Rect = new Rectangle(item.Rect.X, item.Rect.Y,
(int)(item.Prefab.sprite.size.X * item.Prefab.Scale),
(int)(item.Prefab.sprite.size.Y * item.Prefab.Scale));
}
else if (mapEntity is Structure structure)
{
if (!structure.ResizeHorizontal)
{
structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y,
(int)structure.Prefab.ScaledSize.X,
structure.Rect.Height);
}
if (!structure.ResizeVertical)
{
structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y,
structure.Rect.Width,
(int)structure.Prefab.ScaledSize.Y);
}
}
}
}
}
}, isCheat: false));
#endif
GameMain.Config.SaveNewPlayerConfig();
commands.Add(new Command("loadtexts", "loadtexts [sourcefile] [destinationfile]: Loads all lines of text from a given .txt file and inserts them sequientially into the elements of an xml file. If the file paths are omitted, EnglishVanilla.txt and EnglishVanilla.xml are used.", (string[] args) =>
{
string sourcePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.txt";
string destinationPath = args.Length > 1 ? args[1] : "Content/Texts/EnglishVanilla.xml";
string[] lines;
try
{
lines = File.ReadAllLines(sourcePath);
}
catch (Exception e)
{
ThrowError("Reading the file \"" + sourcePath + "\" failed.", e);
return;
}
var doc = XMLExtensions.TryLoadXml(destinationPath);
int i = 0;
foreach (XElement element in doc.Root.Elements())
{
if (i >= lines.Length)
{
ThrowError("Error while loading texts to the xml file. The xml has more elements than the number of lines in the text file.");
return;
}
element.Value = lines[i];
i++;
}
doc.Save(destinationPath);
},
() =>
{
var files = TextManager.GetTextFiles().Select(f => f.Replace("\\", "/"));
return new string[][]
{
files.Where(f => Path.GetExtension(f)==".txt").ToArray(),
files.Where(f => Path.GetExtension(f)==".xml").ToArray()
};
}));
commands.Add(new Command("updatetextfile", "updatetextfile [sourcefile] [destinationfile]: Inserts all the xml elements that are only present in the source file into the destination file. Can be used to update outdated translation files more easily.", (string[] args) =>
{
if (args.Length < 2) return;
string sourcePath = args[0];
string destinationPath = args[1];
var sourceDoc = XMLExtensions.TryLoadXml(sourcePath);
var destinationDoc = XMLExtensions.TryLoadXml(destinationPath);
XElement destinationElement = destinationDoc.Root.Elements().First();
foreach (XElement element in sourceDoc.Root.Elements())
{
if (destinationDoc.Root.Element(element.Name) == null)
{
element.Value = "!!!!!!!!!!!!!" + element.Value;
destinationElement.AddAfterSelf(element);
}
XNode nextNode = destinationElement.NextNode;
while ((!(nextNode is XElement) || nextNode == element) && nextNode != null) nextNode = nextNode.NextNode;
destinationElement = nextNode as XElement;
}
destinationDoc.Save(destinationPath);
},
() =>
{
var files = TextManager.GetTextFiles().Where(f => Path.GetExtension(f) == ".xml").Select(f => f.Replace("\\", "/")).ToArray();
return new string[][]
{
files,
files
};
}));
commands.Add(new Command("dumpentitytexts", "dumpentitytexts [filepath]: gets the names and descriptions of all entity prefabs and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EntityTexts.txt", (string[] args) =>
{
string filePath = args.Length > 0 ? args[0] : "Content/Texts/EntityTexts.txt";
List<string> lines = new List<string>();
foreach (MapEntityPrefab me in MapEntityPrefab.List)
{
lines.Add("<EntityName." + me.Identifier + ">" + me.Name + "</" + me.Identifier + ".Name>");
lines.Add("<EntityDescription." + me.Identifier + ">" + me.Description + "</" + me.Identifier + ".Description>");
}
File.WriteAllLines(filePath, lines);
}));
#if DEBUG
commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) =>
{
if (args.Length != 1) return;
TextManager.CheckForDuplicates(args[0]);
}));
commands.Add(new Command("writetocsv", "Writes the default language (English) to a .csv file.", (string[] args) =>
{
TextManager.WriteToCSV();
NPCConversation.WriteToCSV();
}));
commands.Add(new Command("loadtexts", "loadtexts [sourcefile] [destinationfile]: Loads all lines of text from a given .txt file and inserts them sequientially into the elements of an xml file. If the file paths are omitted, EnglishVanilla.txt and EnglishVanilla.xml are used.", (string[] args) => commands.Add(new Command("loadtexts", "loadtexts [sourcefile] [destinationfile]: Loads all lines of text from a given .txt file and inserts them sequientially into the elements of an xml file. If the file paths are omitted, EnglishVanilla.txt and EnglishVanilla.xml are used.", (string[] args) =>
{ {
@@ -178,7 +178,7 @@ namespace Barotrauma
if (GUI.LargeFont != null) if (GUI.LargeFont != null)
{ {
GUI.LargeFont.DrawString(spriteBatch, loadText, GUI.LargeFont.DrawString(spriteBatch, loadText.ToUpper(),
new Vector2(GameMain.GraphicsWidth / 2.0f - GUI.LargeFont.MeasureString(loadText).X / 2.0f, GameMain.GraphicsHeight * 0.7f), new Vector2(GameMain.GraphicsWidth / 2.0f - GUI.LargeFont.MeasureString(loadText).X / 2.0f, GameMain.GraphicsHeight * 0.7f),
Color.White); Color.White);
} }
@@ -85,6 +85,28 @@ namespace Barotrauma
return true; return true;
}; };
characterListBox = new GUIListBox(new RectTransform(new Point(100, (int)(crewArea.Rect.Height - scrollButtonSize.Y * 1.6f)), crewArea.RectTransform, Anchor.CenterLeft), false, Color.Transparent, null)
{
//Spacing = (int)(3 * GUI.Scale),
ScrollBarEnabled = false,
ScrollBarVisible = false,
CanBeFocused = false
};
scrollButtonUp = new GUIButton(new RectTransform(scrollButtonSize, crewArea.RectTransform, Anchor.TopLeft, Pivot.TopLeft), "", Alignment.Center, "GUIButtonVerticalArrow")
{
Visible = false,
UserData = -1,
OnClicked = ScrollCharacterList
};
scrollButtonDown = new GUIButton(new RectTransform(scrollButtonSize, crewArea.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), "", Alignment.Center, "GUIButtonVerticalArrow")
{
Visible = false,
UserData = 1,
OnClicked = ScrollCharacterList
};
scrollButtonDown.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipVertically);
var characterInfo = new CharacterInfo(subElement); var characterInfo = new CharacterInfo(subElement);
characterInfos.Add(characterInfo); characterInfos.Add(characterInfo);
foreach (XElement invElement in subElement.Elements()) foreach (XElement invElement in subElement.Elements())
@@ -143,6 +165,16 @@ namespace Barotrauma
prevUIScale = GUI.Scale; prevUIScale = GUI.Scale;
} }
#endregion
#region Character list management
public Rectangle GetCharacterListArea()
{
return characterListBox.Rect;
}
partial void InitProjectSpecific() partial void InitProjectSpecific()
{ {
guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), null, Color.Transparent) guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), null, Color.Transparent)
@@ -243,6 +275,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);
return true; return true;
}, },
UserData = order, UserData = order,
@@ -333,8 +366,6 @@ namespace Barotrauma
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace); DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace);
return; return;
} }
if (characterInfos.Contains(revivedCharacter.Info)) AddCharacter(revivedCharacter);
}
characterInfos.Add(characterInfo); characterInfos.Add(characterInfo);
} }
@@ -1216,9 +1247,7 @@ namespace Barotrauma
characters.Contains(Character.Controlled)) characters.Contains(Character.Controlled))
{ {
//deselect construction unless it's the ladders the character is climbing //deselect construction unless it's the ladders the character is climbing
if (Character.Controlled != null && if (Character.Controlled != null && !Character.Controlled.IsClimbing)
Character.Controlled.SelectedConstruction != null &&
Character.Controlled.SelectedConstruction.GetComponent<Items.Components.Ladder>() == null)
{ {
Character.Controlled.SelectedConstruction = null; Character.Controlled.SelectedConstruction = null;
} }
@@ -1329,31 +1358,6 @@ namespace Barotrauma
if (GameMain.NetworkMember != null) GameMain.Client.SelectCrewCharacter(character, previewPlayer); if (GameMain.NetworkMember != null) GameMain.Client.SelectCrewCharacter(character, previewPlayer);
ToggleReportButton("reportintruders", hasIntruders);
foreach (GUIComponent reportButton in reportButtonFrame.Children)
{
var highlight = reportButton.GetChildByUserData("highlighted");
if (highlight.Visible)
{
highlight.RectTransform.LocalScale = new Vector2(1.25f + (float)Math.Sin(Timing.TotalTime * 5.0f) * 0.25f);
}
}
}
else
{
reportButtonFrame.Visible = false;
}
}
/// <summary>
/// Should report buttons be visible on the screen atm?
/// </summary>
private bool ReportButtonsVisible()
{
return CharacterHealth.OpenHealthWindow == null;
}
private bool ReportButtonClicked(GUIButton button, object userData) private bool ReportButtonClicked(GUIButton button, object userData)
{ {
//order targeted to all characters //order targeted to all characters
@@ -1425,18 +1429,20 @@ namespace Barotrauma
return CharacterHealth.OpenHealthWindow == null; return CharacterHealth.OpenHealthWindow == null;
} }
private bool ReportButtonClicked(GUIButton button, object userData) // TODO: remove? not used at all
{ //private bool ReportButtonClicked(GUIButton button, object userData)
//order targeted to all characters //{
Order order = userData as Order; // //order targeted to all characters
if (order.TargetAllCharacters) // Order order = userData as Order;
{ // if (order.TargetAllCharacters)
if (Character.Controlled == null || Character.Controlled.CurrentHull == null) return false; // {
AddOrder(new Order(order.Prefab, Character.Controlled.CurrentHull, null), order.Prefab.FadeOutTime); // if (Character.Controlled == null || Character.Controlled.CurrentHull == null) return false;
SetCharacterOrder(null, order, "", Character.Controlled); // AddOrder(new Order(order.Prefab, Character.Controlled.CurrentHull, null), order.Prefab.FadeOutTime);
} // HumanAIController.PropagateHullSafety(Character.Controlled, Character.Controlled.CurrentHull);
return true; // SetCharacterOrder(null, order, "", Character.Controlled);
} // }
// return true;
//}
private void ToggleReportButton(string orderAiTag, bool enabled) private void ToggleReportButton(string orderAiTag, bool enabled)
{ {
@@ -584,7 +584,7 @@ namespace Barotrauma.Tutorials
} }
} }
if (brokenBox != null && brokenBox.Condition > brokenBox.Prefab.Health / 2.0f && pump.Voltage < pump.MinVoltage) if (brokenBox != null && brokenBox.ConditionPercentage > 50.0f && pump.Voltage < pump.MinVoltage)
{ {
yield return new WaitForSeconds(1.0f); yield return new WaitForSeconds(1.0f);
@@ -670,7 +670,7 @@ namespace Barotrauma.Tutorials
Vector2 steering = targetPos - enemy.WorldPosition; Vector2 steering = targetPos - enemy.WorldPosition;
if (steering != Vector2.Zero) steering = Vector2.Normalize(steering); if (steering != Vector2.Zero) steering = Vector2.Normalize(steering);
enemy.AIController.Steering = steering * enemy.AnimController.GetCurrentSpeed(true); enemy.AIController.Steering = steering;
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
} while (capacitors.FirstOrDefault(c => c.Charge > 0.4f) == null); } while (capacitors.FirstOrDefault(c => c.Charge > 0.4f) == null);
@@ -325,7 +325,7 @@ namespace Barotrauma.Tutorials
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
if (!item.Repairables.Any() || item.Condition > item.Prefab.Health / 2.0f) continue; if (!item.Repairables.Any() || item.ConditionPercentage > 50) continue;
degradedEquipmentFound = true; degradedEquipmentFound = true;
break; break;
} }
@@ -626,6 +626,28 @@ namespace Barotrauma
new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredLanguage")); new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredLanguage"));
return true;
};
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), generalLayoutGroup.RectTransform), style: null);
new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft),
TextManager.Get("Cancel"))
{
IgnoreLayoutGroups = true,
OnClicked = (x, y) =>
{
if (UnsavedSettings)
{
LoadPlayerConfig();
}
if (Screen.Selected == GameMain.MainMenuScreen) GameMain.MainMenuScreen.ReturnToMainMenu(null, null);
GUI.SettingsMenuOpen = false;
return true;
}
};
//spacing //spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), generalLayoutGroup.RectTransform), style: null); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), generalLayoutGroup.RectTransform), style: null);
@@ -184,6 +184,63 @@ namespace Barotrauma.Items.Components
} }
} }
} }
public void ApplyTo(RectTransform target)
{
if (RelativeOffset.HasValue)
{
target.RelativeOffset = RelativeOffset.Value;
}
else if (AbsoluteOffset.HasValue)
{
target.AbsoluteOffset = AbsoluteOffset.Value;
}
if (RelativeSize.HasValue)
{
target.RelativeSize = RelativeSize.Value;
}
else if (AbsoluteSize.HasValue)
{
target.NonScaledSize = AbsoluteSize.Value;
}
if (Anchor.HasValue)
{
target.Anchor = Anchor.Value;
}
if (Pivot.HasValue)
{
target.Pivot = Pivot.Value;
}
else
{
target.Pivot = RectTransform.MatchPivotToAnchor(target.Anchor);
}
target.RecalculateChildren(true, true);
}
}
public GUIFrame GuiFrame { get; protected set; }
[Serialize(false, false)]
public bool AllowUIOverlap
{
get;
set;
}
private ItemComponent linkToUIComponent;
[Serialize("", false)]
public string LinkUIToComponent
{
get;
set;
}
[Serialize(0, false)]
public int HudPriority
{
get;
private set;
} }
private bool shouldMuffleLooping; private bool shouldMuffleLooping;
@@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components
CreateHUD(); CreateHUD();
} }
float distort = 1.0f - item.Condition / item.Prefab.Health; float distort = 1.0f - item.Condition / item.MaxCondition;
foreach (HullData hullData in hullDatas.Values) foreach (HullData hullData in hullDatas.Values)
{ {
hullData.DistortionTimer -= deltaTime; hullData.DistortionTimer -= deltaTime;
@@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components
} }
} }
float distort = 1.0f - item.Condition / item.Prefab.Health; float distort = 1.0f - item.Condition / item.MaxCondition;
for (int i = sonarBlips.Count - 1; i >= 0; i--) for (int i = sonarBlips.Count - 1; i >= 0; i--)
{ {
sonarBlips[i].FadeTimer -= deltaTime * MathHelper.Lerp(0.5f, 2.0f, distort); sonarBlips[i].FadeTimer -= deltaTime * MathHelper.Lerp(0.5f, 2.0f, distort);
@@ -898,7 +898,7 @@ namespace Barotrauma.Items.Components
{ {
strength = MathHelper.Clamp(strength, 0.0f, 1.0f); strength = MathHelper.Clamp(strength, 0.0f, 1.0f);
float distort = 1.0f - item.Condition / item.Prefab.Health; float distort = 1.0f - item.Condition / item.MaxCondition;
Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom;
pos.Y = -pos.Y; pos.Y = -pos.Y;
@@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components
var progressBar = user.UpdateHUDProgressBar( var progressBar = user.UpdateHUDProgressBar(
targetItem, targetItem,
progressBarPos, progressBarPos,
targetItem.Prefab.Health <= 0.0f ? 0.0f : targetItem.Condition / targetItem.Prefab.Health, targetItem.Condition / item.MaxCondition,
Color.Red, Color.Green); Color.Red, Color.Green);
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
} }
@@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components
public override bool ShouldDrawHUD(Character character) public override bool ShouldDrawHUD(Character character)
{ {
if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false; if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false;
return (item.Condition < ShowRepairUIThreshold || (currentFixer == character && item.Condition < item.Prefab.Health)); return (item.Condition < ShowRepairUIThreshold || (currentFixer == character && !item.IsFullCondition));
} }
partial void InitProjSpecific(XElement element) partial void InitProjSpecific(XElement element)
@@ -90,7 +90,7 @@ namespace Barotrauma.Items.Components
{ {
for (int i = 0; i < particleEmitters.Count; i++) for (int i = 0; i < particleEmitters.Count; i++)
{ {
if (item.Condition >= particleEmitterConditionRanges[i].X && item.Condition <= particleEmitterConditionRanges[i].Y) if (item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y)
{ {
particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull);
} }
@@ -101,8 +101,8 @@ namespace Barotrauma.Items.Components
{ {
IsActive = true; IsActive = true;
progressBar.BarSize = item.Condition / item.Prefab.Health; progressBar.BarSize = item.Condition / item.MaxCondition;
progressBar.Color = ToolBox.GradientLerp(item.Condition / item.Prefab.Health, Color.Red, Color.Orange, Color.Green); progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, Color.Red, Color.Orange, Color.Green);
repairButton.Enabled = currentFixer == null; repairButton.Enabled = currentFixer == null;
repairButton.Text = currentFixer == null ? repairButton.Text = currentFixer == null ?
@@ -784,12 +784,12 @@ namespace Barotrauma
if (item != null && drawItem) if (item != null && drawItem)
{ {
if (item.Condition < item.Prefab.Health && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator)) if (!item.IsFullCondition && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator))
{ {
GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Bottom - 8, rect.Width, 8), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Bottom - 8, rect.Width, 8), Color.Black * 0.8f, true);
GUI.DrawRectangle(spriteBatch, GUI.DrawRectangle(spriteBatch,
new Rectangle(rect.X, rect.Bottom - 8, (int)(rect.Width * item.Condition / item.Prefab.Health), 8), new Rectangle(rect.X, rect.Bottom - 8, (int)(rect.Width * item.Condition / item.MaxCondition), 8),
Color.Lerp(Color.Red, Color.Green, item.Condition / item.Prefab.Health) * 0.8f, true); Color.Lerp(Color.Red, Color.Green, item.Condition / item.MaxCondition) * 0.8f, true);
} }
if (itemContainer != null) if (itemContainer != null)
@@ -797,12 +797,12 @@ namespace Barotrauma
float containedState = 0.0f; float containedState = 0.0f;
if (itemContainer.ShowConditionInContainedStateIndicator) if (itemContainer.ShowConditionInContainedStateIndicator)
{ {
containedState = item.Condition / item.Prefab.Health; containedState = item.Condition / item.MaxCondition;
} }
else else
{ {
containedState = itemContainer.Inventory.Capacity == 1 ? containedState = itemContainer.Inventory.Capacity == 1 ?
(itemContainer.Inventory.Items[0] == null ? 0.0f : itemContainer.Inventory.Items[0].Condition / item.Prefab.Health) : (itemContainer.Inventory.Items[0] == null ? 0.0f : itemContainer.Inventory.Items[0].Condition / itemContainer.Inventory.Items[0].MaxCondition) :
itemContainer.Inventory.Items.Count(i => i != null) / (float)itemContainer.Inventory.capacity; itemContainer.Inventory.Items.Count(i => i != null) / (float)itemContainer.Inventory.capacity;
} }
@@ -240,22 +240,23 @@ namespace Barotrauma
var holdable = GetComponent<Holdable>(); var holdable = GetComponent<Holdable>();
if (holdable != null && holdable.Picker?.AnimController != null) if (holdable != null && holdable.Picker?.AnimController != null)
{ {
float depthStep = 0.000001f;
if (holdable.Picker.SelectedItems[0] == this) if (holdable.Picker.SelectedItems[0] == this)
{ {
Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightHand); Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightHand);
depth = holdLimb.ActiveSprite.Depth + 0.000001f; depth = holdLimb.ActiveSprite.Depth + depthStep * 2;
foreach (WearableSprite wearableSprite in holdLimb.WearingItems) foreach (WearableSprite wearableSprite in holdLimb.WearingItems)
{ {
if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) depth = Math.Min(wearableSprite.Sprite.Depth, depth); if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) depth = Math.Max(wearableSprite.Sprite.Depth + depthStep, depth);
} }
} }
else if (holdable.Picker.SelectedItems[1] == this) else if (holdable.Picker.SelectedItems[1] == this)
{ {
Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftHand); Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftHand);
depth = holdLimb.ActiveSprite.Depth - 0.000001f; depth = holdLimb.ActiveSprite.Depth - depthStep * 2;
foreach (WearableSprite wearableSprite in holdLimb.WearingItems) foreach (WearableSprite wearableSprite in holdLimb.WearingItems)
{ {
if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) depth = Math.Max(wearableSprite.Sprite.Depth, depth); if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) depth = Math.Min(wearableSprite.Sprite.Depth - depthStep, depth);
} }
} }
} }
@@ -844,7 +845,7 @@ namespace Barotrauma
msg.WritePadBits(); msg.WritePadBits();
} }
partial void UpdateNetPosition() partial void UpdateNetPosition(float deltaTime)
{ {
if (GameMain.Client == null) { return; } if (GameMain.Client == null) { return; }
@@ -401,6 +401,43 @@ namespace Barotrauma
UpdateSourceRect(limb, newRect); UpdateSourceRect(limb, newRect);
} }
} }
UpdateJointCreation();
if (PlayerInput.KeyHit(Keys.Left))
{
foreach (var limb in selectedLimbs)
{
var newRect = limb.ActiveSprite.SourceRect;
newRect.X--;
UpdateSourceRect(limb, newRect);
}
}
if (PlayerInput.KeyHit(Keys.Right))
{
foreach (var limb in selectedLimbs)
{
var newRect = limb.ActiveSprite.SourceRect;
newRect.X++;
UpdateSourceRect(limb, newRect);
}
}
if (PlayerInput.KeyHit(Keys.Down))
{
foreach (var limb in selectedLimbs)
{
var newRect = limb.ActiveSprite.SourceRect;
newRect.Y++;
UpdateSourceRect(limb, newRect);
}
}
if (PlayerInput.KeyHit(Keys.Up))
{
foreach (var limb in selectedLimbs)
{
var newRect = limb.ActiveSprite.SourceRect;
newRect.Y--;
UpdateSourceRect(limb, newRect);
}
}
} }
if (!isFreezed) if (!isFreezed)
{ {
@@ -2551,7 +2588,7 @@ namespace Barotrauma
{ {
var sourceRect = targetLimb.ActiveSprite.SourceRect; var sourceRect = targetLimb.ActiveSprite.SourceRect;
Vector2 size = sourceRect.Size.ToVector2() * Cam.Zoom * targetLimb.Scale * targetLimb.TextureScale; Vector2 size = sourceRect.Size.ToVector2() * Cam.Zoom * targetLimb.Scale * targetLimb.TextureScale;
Vector2 up = VectorExtensions.Backward(targetLimb.Rotation); Vector2 up = VectorExtensions.BackwardFlipped(targetLimb.Rotation);
Vector2 left = up.Right(); Vector2 left = up.Right();
Vector2 limbScreenPos = SimToScreen(targetLimb.SimPosition); Vector2 limbScreenPos = SimToScreen(targetLimb.SimPosition);
var offset = targetLimb.ActiveSprite.RelativeOrigin.X * left + targetLimb.ActiveSprite.RelativeOrigin.Y * up; var offset = targetLimb.ActiveSprite.RelativeOrigin.X * left + targetLimb.ActiveSprite.RelativeOrigin.Y * up;
@@ -2656,12 +2693,12 @@ namespace Barotrauma
//Vector2 centerOfMass = character.AnimController.GetCenterOfMass(); //Vector2 centerOfMass = character.AnimController.GetCenterOfMass();
Vector2 simSpaceForward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); Vector2 simSpaceForward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation));
//Vector2 simSpaceLeft = Vector2.Transform(-Vector2.UnitX, Matrix.CreateRotationZ(collider.Rotation)); //Vector2 simSpaceLeft = Vector2.Transform(-Vector2.UnitX, Matrix.CreateRotationZ(collider.Rotation));
Vector2 screenSpaceForward = VectorExtensions.Backward(collider.Rotation, 1); Vector2 screenSpaceForward = VectorExtensions.BackwardFlipped(collider.Rotation, 1);
Vector2 screenSpaceLeft = screenSpaceForward.Right(); Vector2 screenSpaceLeft = screenSpaceForward.Right();
// The forward vector is left or right in screen space when the unit is not swimming. Cannot rely on the collider here, because the rotation may vary on ground. // The forward vector is left or right in screen space when the unit is not swimming. Cannot rely on the collider here, because the rotation may vary on ground.
Vector2 forward = animParams.IsSwimAnimation ? screenSpaceForward : Vector2.UnitX * dir; Vector2 forward = animParams.IsSwimAnimation ? screenSpaceForward : Vector2.UnitX * dir;
Vector2 GetSimSpaceForward() => animParams.IsSwimAnimation ? Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)) : Vector2.UnitX * character.AnimController.Dir; Vector2 GetSimSpaceForward() => animParams.IsSwimAnimation ? Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)) : Vector2.UnitX * character.AnimController.Dir;
Vector2 GetScreenSpaceForward() => animParams.IsSwimAnimation ? VectorExtensions.Backward(collider.Rotation, 1) : Vector2.UnitX * character.AnimController.Dir; Vector2 GetScreenSpaceForward() => animParams.IsSwimAnimation ? VectorExtensions.BackwardFlipped(collider.Rotation, 1) : Vector2.UnitX * character.AnimController.Dir;
bool ShowCycleWidget() => PlayerInput.KeyDown(Keys.LeftAlt) && (CurrentAnimation is IHumanAnimation || CurrentAnimation is GroundedMovementParams); bool ShowCycleWidget() => PlayerInput.KeyDown(Keys.LeftAlt) && (CurrentAnimation is IHumanAnimation || CurrentAnimation is GroundedMovementParams);
bool altDown = PlayerInput.KeyDown(Keys.LeftAlt); bool altDown = PlayerInput.KeyDown(Keys.LeftAlt);
@@ -3196,7 +3233,7 @@ namespace Barotrauma
private Vector2[] GetLimbPhysicRect(Limb limb) private Vector2[] GetLimbPhysicRect(Limb limb)
{ {
Vector2 size = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) * Cam.Zoom; Vector2 size = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) * Cam.Zoom;
Vector2 up = VectorExtensions.Backward(limb.Rotation); Vector2 up = VectorExtensions.BackwardFlipped(limb.Rotation);
Vector2 limbScreenPos = SimToScreen(limb.SimPosition); Vector2 limbScreenPos = SimToScreen(limb.SimPosition);
corners = MathUtils.GetImaginaryRect(corners, up, limbScreenPos, size); corners = MathUtils.GetImaginaryRect(corners, up, limbScreenPos, size);
return corners; return corners;
@@ -3343,7 +3380,7 @@ namespace Barotrauma
DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: limb.Rotation); DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: limb.Rotation);
} }
// Is the direction inversed incorrectly? // Is the direction inversed incorrectly?
Vector2 to = tformedJointPos + VectorExtensions.Forward(joint.LimbB.Rotation + MathHelper.ToRadians(-RagdollParams.SpritesheetOrientation), 20); Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation + MathHelper.ToRadians(-RagdollParams.SpritesheetOrientation), 20);
GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.Magenta, width: 2); GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.Magenta, width: 2);
var dotSize = new Vector2(5, 5); var dotSize = new Vector2(5, 5);
var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint()); var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint());
@@ -3361,7 +3398,7 @@ namespace Barotrauma
} }
Vector2 input = ConvertUnits.ToSimUnits(scaledMouseSpeed) / Cam.Zoom; Vector2 input = ConvertUnits.ToSimUnits(scaledMouseSpeed) / Cam.Zoom;
input.Y = -input.Y; input.Y = -input.Y;
input = input.TransformVector(VectorExtensions.Forward(limb.Rotation)); input = input.TransformVector(VectorExtensions.ForwardFlipped(limb.Rotation));
if (joint.BodyA == limb.body.FarseerBody) if (joint.BodyA == limb.body.FarseerBody)
{ {
joint.LocalAnchorA += input; joint.LocalAnchorA += input;
@@ -4028,7 +4065,7 @@ namespace Barotrauma
angle = 0; angle = 0;
} }
float drawAngle = clockWise ? -angle : angle; float drawAngle = clockWise ? -angle : angle;
var widgetDrawPos = drawPos + VectorExtensions.Forward(MathHelper.ToRadians(drawAngle) + rotationOffset, circleRadius); var widgetDrawPos = drawPos + VectorExtensions.ForwardFlipped(MathHelper.ToRadians(drawAngle) + rotationOffset, circleRadius);
GUI.DrawLine(spriteBatch, drawPos, widgetDrawPos, color); GUI.DrawLine(spriteBatch, drawPos, widgetDrawPos, color);
DrawWidget(spriteBatch, widgetDrawPos, WidgetType.Rectangle, widgetSize, color, toolTip, () => DrawWidget(spriteBatch, widgetDrawPos, WidgetType.Rectangle, widgetSize, color, toolTip, () =>
{ {
@@ -4044,7 +4081,7 @@ namespace Barotrauma
GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUI.SmallFont); GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUI.SmallFont);
} }
onClick(angle); onClick(angle);
var zeroPos = drawPos + VectorExtensions.Forward(rotationOffset, circleRadius); var zeroPos = drawPos + VectorExtensions.ForwardFlipped(rotationOffset, circleRadius);
GUI.DrawLine(spriteBatch, drawPos, zeroPos, Color.Red, width: 3); GUI.DrawLine(spriteBatch, drawPos, zeroPos, Color.Red, width: 3);
}, autoFreeze, onHovered: () => }, autoFreeze, onHovered: () =>
{ {
@@ -33,12 +33,14 @@ namespace Barotrauma
private Tab selectedTab; private Tab selectedTab;
private Sprite backgroundSprite;
#region Creation #region Creation
public MainMenuScreen(GameMain game) public MainMenuScreen(GameMain game)
{ {
buttonsParent = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 0.5f), parent: Frame.RectTransform, anchor: Anchor.BottomLeft) buttonsParent = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.85f), parent: Frame.RectTransform, anchor: Anchor.BottomLeft, pivot: Pivot.BottomLeft)
{ {
RelativeOffset = new Vector2(0, 0.1f), RelativeOffset = new Vector2(0, 0),
AbsoluteOffset = new Point(50, 0) AbsoluteOffset = new Point(50, 0)
}) })
{ {
@@ -46,9 +48,188 @@ namespace Barotrauma
RelativeSpacing = 0.02f RelativeSpacing = 0.02f
}; };
// === CAMPAIGN
var campaignHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), parent: buttonsParent.RectTransform) { RelativeOffset = new Vector2(0.1f, 0.0f) }, isHorizontal: true);
new GUIImage(new RectTransform(new Vector2(0.2f, 0.7f), campaignHolder.RectTransform), "MainMenuCampaignIcon")
{
CanBeFocused = false
};
//spacing
new GUIFrame(new RectTransform(new Vector2(0.02f, 0.0f), campaignHolder.RectTransform), style: null);
var campaignNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: campaignHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) });
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), campaignNavigation.RectTransform),
TextManager.Get("CampaignLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true };
var campaignButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: campaignNavigation.RectTransform), style: "MainMenuGUIFrame");
var campaignList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: campaignButtons.RectTransform))
{
Stretch = false,
RelativeSpacing = 0.035f
};
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("LoadGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.LoadGame,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("NewGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.NewGame,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
// === MULTIPLAYER
var multiplayerHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), parent: buttonsParent.RectTransform) { RelativeOffset = new Vector2(0.05f, 0.0f) }, isHorizontal: true);
new GUIImage(new RectTransform(new Vector2(0.2f, 0.7f), multiplayerHolder.RectTransform), "MainMenuMultiplayerIcon")
{
CanBeFocused = false
};
//spacing
new GUIFrame(new RectTransform(new Vector2(0.02f, 0.0f), multiplayerHolder.RectTransform), style: null);
var multiplayerNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: multiplayerHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) });
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), multiplayerNavigation.RectTransform),
TextManager.Get("MultiplayerLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true };
var multiplayerButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: multiplayerNavigation.RectTransform), style: "MainMenuGUIFrame");
var multiplayerList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: multiplayerButtons.RectTransform))
{
Stretch = false,
RelativeSpacing = 0.035f
};
joinServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), multiplayerList.RectTransform), TextManager.Get("JoinServerButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.JoinServer,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
hostServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), multiplayerList.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.HostServer,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
// === CUSTOMIZE
var customizeHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), parent: buttonsParent.RectTransform) { RelativeOffset = new Vector2(0.15f, 0.0f) }, isHorizontal: true);
new GUIImage(new RectTransform(new Vector2(0.2f, 0.7f), customizeHolder.RectTransform), "MainMenuCustomizeIcon")
{
CanBeFocused = false
};
//spacing
new GUIFrame(new RectTransform(new Vector2(0.02f, 0.0f), customizeHolder.RectTransform), style: null);
var customizeNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: customizeHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) });
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), customizeNavigation.RectTransform),
TextManager.Get("CustomizeLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true };
var customizeButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: customizeNavigation.RectTransform), style: "MainMenuGUIFrame");
var customizeList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: customizeButtons.RectTransform))
{
Stretch = false,
RelativeSpacing = 0.035f
};
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.SubmarineEditor,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("CharacterEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.CharacterEditor,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
if (Steam.SteamManager.USE_STEAM)
{
steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
Enabled = false,
UserData = Tab.SteamWorkshop,
OnClicked = SelectTab
};
}
// === OPTION
var optionHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.5f), parent: buttonsParent.RectTransform), isHorizontal: true);
new GUIImage(new RectTransform(new Vector2(0.15f, 0.6f), optionHolder.RectTransform), "MainMenuOptionIcon")
{
CanBeFocused = false
};
//spacing
new GUIFrame(new RectTransform(new Vector2(0.01f, 0.0f), optionHolder.RectTransform), style: null);
var optionButtons = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), parent: optionHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.15f) });
var optionList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: optionButtons.RectTransform))
{
Stretch = false,
RelativeSpacing = 0.035f
};
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
UserData = Tab.Settings,
OnClicked = SelectTab
};
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("QuitButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton")
{
ForceUpperCase = true,
OnClicked = QuitClicked
};
//debug button for quickly starting a new round //debug button for quickly starting a new round
#if DEBUG #if DEBUG
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, -40) }, new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), buttonsParent.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(0, -40) },
"Quickstart (dev)", style: "GUIButtonLarge", color: Color.Red) "Quickstart (dev)", style: "GUIButtonLarge", color: Color.Red)
{ {
IgnoreLayoutGroups = true, IgnoreLayoutGroups = true,
@@ -71,94 +252,6 @@ namespace Barotrauma
Enabled = false Enabled = false
};*/ };*/
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), buttonsParent.RectTransform), style: null); //spacing
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("NewGameButton"), style: "GUIButtonLarge")
{
UserData = Tab.NewGame,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("LoadGameButton"), style: "GUIButtonLarge")
{
UserData = Tab.LoadGame,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), buttonsParent.RectTransform), style: null); //spacing
joinServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("JoinServerButton"), style: "GUIButtonLarge")
{
UserData = Tab.JoinServer,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
hostServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("HostServerButton"), style: "GUIButtonLarge")
{
UserData = Tab.HostServer,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), buttonsParent.RectTransform), style: null); //spacing
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("SubEditorButton"), style: "GUIButtonLarge")
{
UserData = Tab.SubmarineEditor,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("CharacterEditorButton"), style: "GUIButtonLarge")
{
UserData = Tab.CharacterEditor,
OnClicked = (tb, userdata) =>
{
SelectTab(tb, userdata);
return true;
}
};
if (Steam.SteamManager.USE_STEAM)
{
steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("SteamWorkshopButton"), style: "GUIButtonLarge")
{
Enabled = false,
UserData = Tab.SteamWorkshop,
OnClicked = SelectTab
};
}
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), buttonsParent.RectTransform), style: null); //spacing
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("SettingsButton"), style: "GUIButtonLarge")
{
UserData = Tab.Settings,
OnClicked = SelectTab
};
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonsParent.RectTransform), TextManager.Get("QuitButton"), style: "GUIButtonLarge")
{
OnClicked = QuitClicked
};
/* var buttons = GUI.CreateButtons(9, new Vector2(1, 0.04f), buttonsParent.RectTransform, anchor: Anchor.BottomLeft, /* var buttons = GUI.CreateButtons(9, new Vector2(1, 0.04f), buttonsParent.RectTransform, anchor: Anchor.BottomLeft,
minSize: minButtonSize, maxSize: maxButtonSize, relativeSpacing: 0.005f, extraSpacing: i => i % 2 == 0 ? 20 : 0); minSize: minButtonSize, maxSize: maxButtonSize, relativeSpacing: 0.005f, extraSpacing: i => i % 2 == 0 ? 20 : 0);
buttons.ForEach(b => b.Color *= 0.8f); buttons.ForEach(b => b.Color *= 0.8f);
@@ -172,9 +265,9 @@ namespace Barotrauma
var pivot = Pivot.Center; var pivot = Pivot.Center;
menuTabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length + 1]; menuTabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length + 1];
menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize, Frame.RectTransform, anchor, pivot, minSize, maxSize)); menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize));
var paddedNewGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center), style: null); var paddedNewGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center), style: null);
menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, Frame.RectTransform, anchor, pivot, minSize, maxSize)); menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize));
var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center), style: null); var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center), style: null);
campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame) campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame)
@@ -185,13 +278,13 @@ namespace Barotrauma
var hostServerScale = new Vector2(0.7f, 1.0f); var hostServerScale = new Vector2(0.7f, 1.0f);
menuTabs[(int)Tab.HostServer] = new GUIFrame(new RectTransform( menuTabs[(int)Tab.HostServer] = new GUIFrame(new RectTransform(
Vector2.Multiply(relativeSize, hostServerScale), Frame.RectTransform, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale))); Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)));
CreateHostServerFields(); CreateHostServerFields();
//---------------------------------------------------------------------- //----------------------------------------------------------------------
menuTabs[(int)Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, Frame.RectTransform, anchor, pivot, minSize, maxSize)); menuTabs[(int)Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize));
//PLACEHOLDER //PLACEHOLDER
var tutorialList = new GUIListBox( var tutorialList = new GUIListBox(
@@ -540,8 +633,7 @@ namespace Barotrauma
public override void AddToGUIUpdateList() public override void AddToGUIUpdateList()
{ {
Frame.AddToGUIUpdateList(ignoreChildren: true); Frame.AddToGUIUpdateList();
buttonsParent.AddToGUIUpdateList();
if (selectedTab > 0 && menuTabs[(int)selectedTab] != null) if (selectedTab > 0 && menuTabs[(int)selectedTab] != null)
{ {
menuTabs[(int)selectedTab].AddToGUIUpdateList(); menuTabs[(int)selectedTab].AddToGUIUpdateList();
@@ -572,12 +664,21 @@ namespace Barotrauma
#endif #endif
} }
public void DrawBackground(GraphicsDevice graphics, SpriteBatch spriteBatch)
{
graphics.Clear(Color.Black);
if (backgroundSprite == null)
{
backgroundSprite = (LocationType.List.Where(l => l.UseInMainMenu).GetRandom()).GetPortrait(0);
}
if (backgroundSprite != null) { GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); }
}
public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
{ {
graphics.Clear(Color.CornflowerBlue); DrawBackground(graphics, spriteBatch);
GameMain.TitleScreen.DrawLoadingText = false;
GameMain.TitleScreen.Draw(spriteBatch, graphics, (float)deltaTime);
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable);
@@ -672,7 +773,7 @@ namespace Barotrauma
}; };
GUIComponent parent = paddedFrame; GUIComponent parent = paddedFrame;
new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUI.LargeFont); new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUI.LargeFont) { ForceUpperCase = true };
var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment);
serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment); serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment);
@@ -53,7 +53,7 @@ namespace Barotrauma
menu = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas, Anchor.Center)); menu = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas, Anchor.Center));
new GUITextBlock(new RectTransform(new Vector2(0.95f, 0.133f), menu.RectTransform, Anchor.TopCenter), new GUITextBlock(new RectTransform(new Vector2(0.95f, 0.133f), menu.RectTransform, Anchor.TopCenter),
TextManager.Get("JoinServer"), textAlignment: Alignment.Left, font: GUI.LargeFont); TextManager.Get("JoinServer"), textAlignment: Alignment.Left, font: GUI.LargeFont) { ForceUpperCase = true };
var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), menu.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.0f, 0.03f) }, style: null); var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), menu.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.0f, 0.03f) }, style: null);
@@ -733,7 +733,7 @@ namespace Barotrauma
graphics.Clear(Color.CornflowerBlue); graphics.Clear(Color.CornflowerBlue);
GameMain.TitleScreen.DrawLoadingText = false; GameMain.TitleScreen.DrawLoadingText = false;
GameMain.TitleScreen.Draw(spriteBatch, graphics, (float)deltaTime); GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch);
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable);
@@ -269,6 +269,7 @@ namespace Barotrauma.Sounds
//we couldn't get a free source to assign to this channel! //we couldn't get a free source to assign to this channel!
return -1; return -1;
} }
}
#if DEBUG #if DEBUG
public void DebugSource(int ind) public void DebugSource(int ind)
@@ -413,6 +414,10 @@ namespace Barotrauma.Sounds
{ {
categoryModifiers[category].Second = muffle; categoryModifiers[category].Second = muffle;
} }
else
{
categoryModifiers[category].Second = muffle;
}
} }
for (int i = 0; i < playingChannels.Length; i++) for (int i = 0; i < playingChannels.Length; i++)
@@ -1571,8 +1571,6 @@ namespace Barotrauma
} }
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.Status }); GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.Status });
item.NeedsPositionUpdate = true;
} }
} }
@@ -9,8 +9,6 @@ namespace Barotrauma
{ {
partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
{ {
private bool prevBodyAwake;
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
{ {
string errorMsg = ""; string errorMsg = "";
@@ -255,17 +253,71 @@ namespace Barotrauma
} }
partial void UpdateNetPosition() partial void UpdateNetPosition(float deltaTime)
{ {
if (parentInventory != null) return; if (parentInventory != null || body == null || !body.Enabled || Removed)
if (prevBodyAwake != body.FarseerBody.Awake ||
Vector2.DistanceSquared(lastSentPos, SimPosition) > NetConfig.ItemPosUpdateDistance * NetConfig.ItemPosUpdateDistance)
{ {
needsPositionUpdate = true; PositionUpdateInterval = float.PositiveInfinity;
return;
} }
prevBodyAwake = body.FarseerBody.Awake; //gradually increase the interval of position updates
PositionUpdateInterval += deltaTime;
float maxInterval = 30.0f;
float velSqr = body.LinearVelocity.LengthSquared();
if (velSqr > 10.0f * 10.0f)
{
//over 10 m/s (projectile, thrown item or similar) -> send updates very frequently
maxInterval = 0.1f;
}
else if (velSqr > 1.0f)
{
//over 1 m/s
maxInterval = 0.25f;
}
else if (velSqr > 0.05f * 0.05f)
{
//over 0.05 m/s
maxInterval = 1.0f;
}
PositionUpdateInterval = Math.Min(PositionUpdateInterval, maxInterval);
}
public float GetPositionUpdateInterval(Client recipient)
{
if (PositionUpdateInterval == float.PositiveInfinity)
{
return float.PositiveInfinity;
}
if (recipient.Character == null || recipient.Character.IsDead)
{
//less frequent updates for clients who aren't controlling a character (max 2 updates/sec)
return Math.Max(PositionUpdateInterval, 0.5f);
}
else
{
float distSqr = Vector2.DistanceSquared(recipient.Character.WorldPosition, WorldPosition);
if (distSqr > 20000.0f * 20000.0f)
{
//don't send position updates at all if >20 000 units away
return float.PositiveInfinity;
}
else if (distSqr > 10000.0f * 10000.0f)
{
//drop the update rate to 10% if too far to see the item
return PositionUpdateInterval * 10;
}
else if (distSqr > 1000.0f * 1000.0f)
{
//halve the update rate if the client is far away (but still close enough to possibly see the item)
return PositionUpdateInterval * 2;
}
return PositionUpdateInterval;
}
} }
public void ServerWritePosition(NetBuffer msg, Client c, object[] extraData = null) public void ServerWritePosition(NetBuffer msg, Client c, object[] extraData = null)
@@ -277,8 +329,6 @@ namespace Barotrauma
msg.Write((byte)tempBuffer.LengthBytes); msg.Write((byte)tempBuffer.LengthBytes);
msg.Write(tempBuffer); msg.Write(tempBuffer);
msg.WritePadBits(); msg.WritePadBits();
lastSentPos = SimPosition;
} }
public void CreateServerEvent<T>(T ic) where T : ItemComponent, IServerSerializable public void CreateServerEvent<T>(T ic) where T : ItemComponent, IServerSerializable
@@ -614,10 +614,6 @@ namespace Barotrauma.Networking
} }
} }
foreach (Item item in Item.ItemList)
{
item.NeedsPositionUpdate = false;
}
foreach (Character character in Character.CharacterList) foreach (Character character in Character.CharacterList)
{ {
if (character.healthUpdateTimer <= 0.0f) if (character.healthUpdateTimer <= 0.0f)
@@ -1229,7 +1225,10 @@ namespace Barotrauma.Networking
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
if (!item.NeedsPositionUpdate) continue; if (item.PositionUpdateInterval == float.PositiveInfinity) { continue; }
float updateInterval = item.GetPositionUpdateInterval(c);
c.PositionUpdateLastSent.TryGetValue(item.ID, out float lastSent);
if (lastSent > NetTime.Now - item.PositionUpdateInterval) { continue; }
if (!c.PendingPositionUpdates.Contains(item)) c.PendingPositionUpdates.Enqueue(item); if (!c.PendingPositionUpdates.Contains(item)) c.PendingPositionUpdates.Enqueue(item);
} }
} }
@@ -2023,8 +2022,6 @@ namespace Barotrauma.Networking
targetmsg += $"/\n/ServerMessage.Reason/: /{reason}"; targetmsg += $"/\n/ServerMessage.Reason/: /{reason}";
} }
Log(msg, ServerLog.MessageType.ServerMessage);
if (client.SteamID > 0) { SteamManager.StopAuthSession(client.SteamID); } if (client.SteamID > 0) { SteamManager.StopAuthSession(client.SteamID); }
client.Connection.Disconnect(targetmsg); client.Connection.Disconnect(targetmsg);
@@ -2278,8 +2275,7 @@ namespace Barotrauma.Networking
if (type.Value != ChatMessageType.MessageBox) if (type.Value != ChatMessageType.MessageBox)
{ {
string myReceivedMessage = TextManager.GetServerMessage(message); string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message) : message;
if (!string.IsNullOrWhiteSpace(myReceivedMessage) && if (!string.IsNullOrWhiteSpace(myReceivedMessage) &&
(targetClient == null || senderClient == null)) (targetClient == null || senderClient == null))
{ {
@@ -33,6 +33,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveGetItem.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveGetItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveGoTo.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveGoTo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveIdle.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveIdle.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\AIObjectivePumpWater.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectivePumpWater.cs" />
@@ -47,7 +48,6 @@
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\AnimController.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\AnimController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\FishAnimController.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\FishAnimController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\HumanoidAnimController.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\HumanoidAnimController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\HumanoidAnimParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Params\Animation\AnimationParams.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Params\Animation\AnimationParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Params\Animation\FishAnimations.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Params\Animation\FishAnimations.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Params\Animation\HumanoidAnimations.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Params\Animation\HumanoidAnimations.cs" />
@@ -23,7 +23,6 @@
<Folder Include="$(MSBuildThisFileDirectory)Content\Characters\Tigerthresherboss\" /> <Folder Include="$(MSBuildThisFileDirectory)Content\Characters\Tigerthresherboss\" />
<Folder Include="$(MSBuildThisFileDirectory)Content\Characters\Tigerthresher\Animations\" /> <Folder Include="$(MSBuildThisFileDirectory)Content\Characters\Tigerthresher\Animations\" />
<Folder Include="$(MSBuildThisFileDirectory)Content\Characters\Watcher\Animations\" /> <Folder Include="$(MSBuildThisFileDirectory)Content\Characters\Watcher\Animations\" />
<Folder Include="$(MSBuildThisFileDirectory)Content\Fonts\" />
<Folder Include="$(MSBuildThisFileDirectory)Content\BackgroundCreatures\" /> <Folder Include="$(MSBuildThisFileDirectory)Content\BackgroundCreatures\" />
<Folder Include="$(MSBuildThisFileDirectory)Data\Saves\" /> <Folder Include="$(MSBuildThisFileDirectory)Data\Saves\" />
</ItemGroup> </ItemGroup>
@@ -343,6 +342,96 @@
<Content Include="$(MSBuildThisFileDirectory)Content\Effects\waterbump.png"> <Content Include="$(MSBuildThisFileDirectory)Content\Effects\waterbump.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\LICENSE_OFL.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\NotoSans-Black.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\NotoSans-Bold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\NotoSans-ExtraBold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\NotoSans-Medium.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\NotoSans-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\NotoSans\NotoSans-SemiBold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\LICENSE.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Black.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-BlackItalic.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Bold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-BoldItalic.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Italic.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Light.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-LightItalic.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Medium.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-MediumItalic.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-Thin.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\Roboto\Roboto-ThinItalic.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\OFL.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-Black.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-Bold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-ExtraBold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-ExtraLight.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-Light.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-Medium.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-SemiBold.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Fonts\WorkSans\WorkSans-Thin.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Content\Items\Alien\AlienArtifact1.png"> <Content Include="$(MSBuildThisFileDirectory)Content\Items\Alien\AlienArtifact1.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@@ -1808,6 +1897,9 @@
<Content Include="$(MSBuildThisFileDirectory)Content\UI\UI_Atlas2.png"> <Content Include="$(MSBuildThisFileDirectory)Content\UI\UI_Atlas2.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="$(MSBuildThisFileDirectory)Content\UI\UI_MainMenu.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(MSBuildThisFileDirectory)Data\clientpermissions.xml"> <Content Include="$(MSBuildThisFileDirectory)Data\clientpermissions.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@@ -356,7 +356,6 @@ namespace Barotrauma
private void UpdateIdle(float deltaTime) private void UpdateIdle(float deltaTime)
{ {
float speed = Character.AnimController.GetCurrentSpeed(false);
if (Character.Submarine == null && SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) if (Character.Submarine == null && SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f))
{ {
//steer straight up if very deep //steer straight up if very deep
@@ -370,17 +369,17 @@ namespace Barotrauma
{ {
Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, speed); steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f);
steeringManager.SteeringSeek(targetSimPos, speed); steeringManager.SteeringSeek(targetSimPos);
} }
else else
{ {
//wander around randomly //wander around randomly
if (Character.Submarine == null) if (Character.Submarine == null)
{ {
steeringManager.SteeringAvoid(deltaTime, colliderSize * 5.0f, speed); steeringManager.SteeringAvoid(deltaTime, colliderSize * 5.0f);
} }
steeringManager.SteeringWander(speed / 2); steeringManager.SteeringWander(0.5f);
} }
} }
@@ -398,12 +397,11 @@ namespace Barotrauma
Vector2 escapeDir = Vector2.Normalize(SimPosition - SelectedAiTarget.SimPosition); Vector2 escapeDir = Vector2.Normalize(SimPosition - SelectedAiTarget.SimPosition);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
float speed = Character.AnimController.GetCurrentSpeed(useMaxSpeed: true); SteeringManager.SteeringManual(deltaTime, escapeDir);
SteeringManager.SteeringManual(deltaTime, escapeDir * speed); SteeringManager.SteeringWander();
SteeringManager.SteeringWander(speed);
if (Character.CurrentHull == null) if (Character.CurrentHull == null)
{ {
SteeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, speed); SteeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f);
} }
} }
@@ -419,8 +417,6 @@ namespace Barotrauma
return; return;
} }
float speed = Character.AnimController.GetCurrentSpeed(true);
selectedTargetMemory.Priority -= deltaTime * 0.1f; selectedTargetMemory.Priority -= deltaTime * 0.1f;
Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
@@ -447,9 +443,12 @@ namespace Barotrauma
if (wallTarget != null) if (wallTarget != null)
{ {
attackSimPosition = ConvertUnits.ToSimUnits(wallTarget.Position); attackSimPosition = ConvertUnits.ToSimUnits(wallTarget.Position);
if (Character.Submarine == null && SelectedAiTarget.Entity?.Submarine != null) attackSimPosition += ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position); if (Character.Submarine == null && SelectedAiTarget.Entity?.Submarine != null)
{
attackSimPosition += ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position);
}
} }
else if (SelectedAiTarget.Entity is Character) else if (SelectedAiTarget.Entity is Character c)
{ {
//target the closest limb if the target is a character //target the closest limb if the target is a character
float closestDist = Vector2.DistanceSquared(SelectedAiTarget.SimPosition, SimPosition) * 10.0f; float closestDist = Vector2.DistanceSquared(SelectedAiTarget.SimPosition, SimPosition) * 10.0f;
@@ -503,11 +502,11 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs(); Character.AnimController.ReleaseStuckLimbs();
if (steeringManager is IndoorsSteeringManager) if (steeringManager is IndoorsSteeringManager)
{ {
steeringManager.SteeringManual(deltaTime, targetPos - Character.WorldPosition); steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetPos - Character.WorldPosition));
} }
else else
{ {
steeringManager.SteeringSeek(ConvertUnits.ToSimUnits(targetPos), speed); steeringManager.SteeringSeek(ConvertUnits.ToSimUnits(targetPos));
} }
return; return;
} }
@@ -518,11 +517,12 @@ namespace Barotrauma
//steer through the door manually if it's open or broken //steer through the door manually if it's open or broken
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.Item.Condition <= 0.0f)) if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.Item.Condition <= 0.0f))
{ {
var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition);
if (door.LinkedGap.IsHorizontal) if (door.LinkedGap.IsHorizontal)
{ {
if (Character.WorldPosition.Y < door.Item.WorldRect.Y && Character.WorldPosition.Y > door.Item.WorldRect.Y - door.Item.Rect.Height) if (Character.WorldPosition.Y < door.Item.WorldRect.Y && Character.WorldPosition.Y > door.Item.WorldRect.Y - door.Item.Rect.Height)
{ {
var velocity = Vector2.UnitX * (door.LinkedGap.FlowTargetHull.WorldPosition.X - Character.WorldPosition.X) * speed; velocity.Y = 0;
steeringManager.SteeringManual(deltaTime, velocity); steeringManager.SteeringManual(deltaTime, velocity);
return; return;
} }
@@ -531,7 +531,7 @@ namespace Barotrauma
{ {
if (Character.WorldPosition.X < door.Item.WorldRect.X && Character.WorldPosition.X > door.Item.WorldRect.Right) if (Character.WorldPosition.X < door.Item.WorldRect.X && Character.WorldPosition.X > door.Item.WorldRect.Right)
{ {
var velocity = Vector2.UnitY * (door.LinkedGap.FlowTargetHull.WorldPosition.Y - Character.WorldPosition.Y) * speed; velocity.X = 0;
steeringManager.SteeringManual(deltaTime, velocity); steeringManager.SteeringManual(deltaTime, velocity);
return; return;
} }
@@ -638,10 +638,10 @@ namespace Barotrauma
Vector2 targetingVector = Vector2.Normalize(steeringVector) * attackingLimb.attack.Range; Vector2 targetingVector = Vector2.Normalize(steeringVector) * attackingLimb.attack.Range;
// Offset the position a bit so that we don't overshoot the movement. // Offset the position a bit so that we don't overshoot the movement.
Vector2 steerPos = attackSimPosition + targetingVector; Vector2 steerPos = attackSimPosition + targetingVector;
steeringManager.SteeringSeek(steerPos, speed); steeringManager.SteeringSeek(steerPos, 10);
if (Character.CurrentHull == null) if (Character.CurrentHull == null)
{ {
SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f, speed); SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f);
} }
if (steeringManager is IndoorsSteeringManager indoorsSteering) if (steeringManager is IndoorsSteeringManager indoorsSteering)
@@ -656,7 +656,7 @@ namespace Barotrauma
} }
else if (indoorsSteering.CurrentPath.Finished) else if (indoorsSteering.CurrentPath.Finished)
{ {
steeringManager.SteeringManual(deltaTime, steeringVector); steeringManager.SteeringManual(deltaTime, Vector2.Normalize(steeringVector));
} }
else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null) else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null)
{ {
@@ -800,7 +800,7 @@ namespace Barotrauma
if (attacker == null || attacker.AiTarget == null) return; if (attacker == null || attacker.AiTarget == null) return;
AITargetMemory targetMemory = FindTargetMemory(attacker.AiTarget); AITargetMemory targetMemory = FindTargetMemory(attacker.AiTarget);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * aggressionhurt; ; targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * aggressionhurt;
} }
// 10 dmg, 100 health -> 0.1 // 10 dmg, 100 health -> 0.1
@@ -838,7 +838,7 @@ namespace Barotrauma
steeringManager.SteeringManual(deltaTime, attackDir * (1.0f - (dist / 500.0f))); steeringManager.SteeringManual(deltaTime, attackDir * (1.0f - (dist / 500.0f)));
} }
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, Character.AnimController.GetCurrentSpeed(false)); steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f);
} }
#endregion #endregion
@@ -870,12 +870,12 @@ namespace Barotrauma
if (limbDist < 2.0f) if (limbDist < 2.0f)
{ {
Character.SelectCharacter(SelectedAiTarget.Entity as Character); Character.SelectCharacter(SelectedAiTarget.Entity as Character);
steeringManager.SteeringManual(deltaTime, limbDiff); steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff));
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
} }
else else
{ {
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), Character.AnimController.GetCurrentSpeed(useMaxSpeed: true)); steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition));
} }
} }
@@ -1,6 +1,7 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Barotrauma namespace Barotrauma
@@ -19,8 +20,16 @@ namespace Barotrauma
private float crouchRaycastTimer; private float crouchRaycastTimer;
const float CrouchRaycastInterval = 1.0f; const float CrouchRaycastInterval = 1.0f;
public const float HULL_SAFETY_THRESHOLD = 50;
// TODO: update the list when someone gives a report
public HashSet<Hull> UnsafeHulls { get; private set; } = new HashSet<Hull>();
private SteeringManager outsideSteering, insideSteering; private SteeringManager outsideSteering, insideSteering;
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
public override AIObjectiveManager ObjectiveManager public override AIObjectiveManager ObjectiveManager
{ {
get { return objectiveManager; } get { return objectiveManager; }
@@ -68,17 +77,18 @@ namespace Barotrauma
steeringManager = outsideSteering; steeringManager = outsideSteering;
} }
(Character.AnimController as HumanoidAnimController).Crouching = shouldCrouch; AnimController.Crouching = shouldCrouch;
CheckCrouching(deltaTime); CheckCrouching(deltaTime);
Character.ClearInputs(); Character.ClearInputs();
objectiveManager.UpdateObjectives(deltaTime);
if (updateObjectiveTimer > 0.0f) if (updateObjectiveTimer > 0.0f)
{ {
updateObjectiveTimer -= deltaTime; updateObjectiveTimer -= deltaTime;
} }
else else
{ {
objectiveManager.UpdateObjectives(); objectiveManager.SortObjectives();
updateObjectiveTimer = UpdateObjectiveInterval; updateObjectiveTimer = UpdateObjectiveInterval;
} }
@@ -90,68 +100,106 @@ namespace Barotrauma
objectiveManager.DoCurrentObjective(deltaTime); objectiveManager.DoCurrentObjective(deltaTime);
float currObjectivePriority = objectiveManager.GetCurrentPriority(); bool run = objectiveManager.GetCurrentPriority() > AIObjectiveManager.OrderPriority;
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
bool run = currObjectivePriority > 30.0f; {
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run)); if (Vector2.DistanceSquared(Character.SimPosition, goTo.Target.SimPosition) > 3 * 3)
{
run = true;
}
}
if (!run)
{
run = objectiveManager.CurrentObjective.ForceRun;
}
if (run)
{
run = !AnimController.Crouching && !AnimController.IsMovingBackwards;
}
float currentSpeed = Character.AnimController.GetCurrentSpeed(run);
steeringManager.Update(currentSpeed);
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
var currPath = (steeringManager as IndoorsSteeringManager)?.CurrentPath; if (steeringManager == insideSteering)
if (currPath != null && currPath.CurrentNode != null)
{ {
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) var currPath = PathSteering.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{ {
ignorePlatforms = true; if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
ignorePlatforms = true;
}
} }
} }
Character.AnimController.IgnorePlatforms = ignorePlatforms; Character.AnimController.IgnorePlatforms = ignorePlatforms;
if (Character.IsClimbing)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
Vector2 targetMovement = AnimController.TargetMovement;
if (!Character.AnimController.InWater) if (!Character.AnimController.InWater)
{ {
Vector2 targetMovement = new Vector2( targetMovement = new Vector2(
Character.AnimController.TargetMovement.X, Character.AnimController.TargetMovement.X,
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)); MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
float maxSpeed = Character.GetCurrentMaxSpeed(run);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
} }
if (Character.AnimController.Anim == AnimController.Animation.Climbing && float maxSpeed = Character.ApplyTemporarySpeedLimits(currentSpeed);
Character.SelectedConstruction != null && targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
Character.SelectedConstruction.GetComponent<Items.Components.Ladder>() != null) targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
if (!NeedsDivingGear(Character.CurrentHull))
{ {
if (currPath != null && currPath.CurrentNode != null && currPath.CurrentNode.Ladders != null) bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool highPressure = Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0 && Character.PressureProtection <= 0;
bool shouldKeepTheGearOn = objectiveManager.CurrentObjective.KeepDivingGearOn;
bool removeDivingSuit = (oxygenLow && !highPressure) || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 1 && !Character.IsClimbing && steeringManager == insideSteering && !PathSteering.InStairs);
if (removeDivingSuit)
{ {
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
// TODO: take the item where it was taken from?
divingSuit.Drop(Character);
}
}
bool takeMaskOff = oxygenLow || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 20);
if (takeMaskOff)
{
var mask = Character.Inventory.FindItemByIdentifier("divingmask");
if (mask != null)
{
// Try to put the mask in an Any slot, and drop it if that fails
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
mask.Drop();
}
}
} }
} }
if (!(ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires) && !(ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire))
//suit can be taken off if there character is inside a hull and there's air in the room
bool canTakeOffSuit = Character.AnimController.CurrentHull != null &&
Character.AnimController.CurrentHull.OxygenPercentage > 30.0f &&
Character.AnimController.CurrentHull.WaterVolume < Character.AnimController.CurrentHull.Volume * 0.3f;
//the suit can be taken off and the character is running out of oxygen (couldn't find a tank for the suit?) or idling
//-> take the suit off
if (canTakeOffSuit && (Character.Oxygen < 50.0f || objectiveManager.CurrentObjective is AIObjectiveIdle))
{ {
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit"); var extinguisherItem = Character.Inventory.FindItemByIdentifier("extinguisher") ?? Character.Inventory.FindItemByTag("extinguisher");
if (divingSuit != null) divingSuit.Drop(Character); if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem))
{
// TODO: take the item where it was taken from?
extinguisherItem.Drop();
}
} }
if (Character.IsKeyDown(InputType.Aim)) if (Character.IsKeyDown(InputType.Aim))
@@ -173,14 +221,61 @@ namespace Barotrauma
{ {
Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
} }
if (Character.CurrentHull != null)
{
PropagateHullSafety(Character, Character.CurrentHull);
}
}
protected void ReportProblems()
{
Order newOrder = null;
if (Character.CurrentHull != null)
{
if (Character.CurrentHull.FireSources.Count > 0)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
if (Character.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor == null && g.Open > 0.0f))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
foreach (Character c in Character.CharacterList)
{
if (c.CurrentHull == Character.CurrentHull && !c.IsDead &&
(c.AIController is EnemyAIController || c.TeamID != Character.TeamID))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
}
}
if (Character.CurrentHull != null && (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
if (newOrder != null)
{
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(
newOrder.GetChatMessage("", Character.CurrentHull?.RoomName, givingOrderToSelf: false), ChatMessageType.Order);
}
}
} }
private void ReportProblems() private void ReportProblems()
{ {
if (GameMain.Client != null) return; if (GameMain.Client != null) return;
partial void ReportProblems();
private void UpdateSpeaking() private void UpdateSpeaking()
{ {
if (Character.Oxygen < 20.0f) if (Character.Oxygen < 20.0f)
@@ -204,11 +299,26 @@ namespace Barotrauma
float totalDamage = attackResult.Damage; float totalDamage = attackResult.Damage;
if (totalDamage <= 0.0f || attacker == null) return; if (totalDamage <= 0.0f || attacker == null) return;
if (attacker.AnimController.Anim == AnimController.Animation.CPR && attacker.SelectedCharacter == Character) if (attacker.SpeciesName == Character.SpeciesName)
{ {
// Don't attack characters that damage you while doing cpr, because let's assume that they are helping you. if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled)
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). {
return; // Don't react to damage done by friendly ai, because we know that it's accidental
return;
}
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
{
// Don't attack characters that damage you while doing cpr, because let's assume that they are helping you.
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
float currentVitality = Character.CharacterHealth.Vitality;
float dmgPercentage = totalDamage / currentVitality * 100;
if (dmgPercentage < currentVitality / 10)
{
// Don't react to a minor amount of (accidental) dmg done by friendly characters
return;
}
} }
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () => objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () =>
@@ -251,5 +361,87 @@ namespace Barotrauma
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f; float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
} }
public static bool NeedsDivingGear(Hull hull) => hull == null || hull.OxygenPercentage < 50 || hull.WaterPercentage > 50;
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character) => HasItem(character, "divingsuit", "oxygensource");
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingGear(Character character) => HasItem(character, "diving", "oxygensource");
public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0)
{
var item = character.Inventory.FindItemByTag(tag);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
(containedTag == null ||
(item.ContainedItems != null &&
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
}
/// <summary>
/// Updates the hull safety for all ai characters in the team.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
foreach (var c in Character.CharacterList)
{
if (c.TeamID == character.TeamID)
{
if (c.AIController is HumanAIController humanAi)
{
humanAi.RefreshHullSafety(hull);
}
}
}
}
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull) > HULL_SAFETY_THRESHOLD)
{
UnsafeHulls.Remove(hull);
}
else
{
UnsafeHulls.Add(hull);
}
}
public float GetHullSafety(Hull hull)
{
if (hull == null) { return 0; }
bool ignoreFire = ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire || ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires;
bool ignoreWater = HasDivingSuit(Character);
bool ignoreOxygen = ignoreWater || HasDivingGear(Character);
return GetHullSafety(hull, Character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies: false);
}
public static float GetHullSafety(Hull hull, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100);
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100);
if (!character.NeedsAir)
{
oxygenFactor = 1;
waterFactor = 1;
}
// 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 fireFactor = ignoreFire ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
int enemyCount = Character.CharacterList.Count(e => e.CurrentHull == hull && !e.IsDead && !e.IsUnconscious && (e.AIController is EnemyAIController || e.TeamID != character.TeamID));
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
float enemyFactor = ignoreEnemies ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor;
return MathHelper.Clamp(safety * 100, 0, 100);
}
} }
} }
@@ -4,7 +4,6 @@ namespace Barotrauma
{ {
interface ISteerable interface ISteerable
{ {
Vector2 Steering Vector2 Steering
{ {
get; get;
@@ -25,8 +24,5 @@ namespace Barotrauma
{ {
get; get;
} }
} }
} }
@@ -43,8 +43,13 @@ namespace Barotrauma
private set; private set;
} }
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) public bool InLadders => currentPath != null && currentPath.CurrentNode != null && currentPath.CurrentNode.Ladders != null;
: base(host) private bool IsNextNodeLadder => currentPath.NextNode != null && currentPath.CurrentNode.Ladders != null;
private bool IsNextLadderSameAsCurrent => IsNextNodeLadder && currentPath.CurrentNode.Ladders == currentPath.NextNode.Ladders;
public bool InStairs => currentPath != null && currentPath.CurrentNode != null && currentPath.CurrentNode.Stairs != null;
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{ {
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true); pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
pathFinder.GetNodePenalty = GetNodePenalty; pathFinder.GetNodePenalty = GetNodePenalty;
@@ -73,7 +78,7 @@ namespace Barotrauma
IsPathDirty = false; IsPathDirty = false;
} }
protected override Vector2 DoSteeringSeek(Vector2 target, float speed) protected override Vector2 DoSteeringSeek(Vector2 target, float weight)
{ {
//find a new path if one hasn't been found yet or the target is different from the current target //find a new path if one hasn't been found yet or the target is different from the current target
if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f) if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f)
@@ -105,16 +110,14 @@ namespace Barotrauma
var collider = character.AnimController.Collider; var collider = character.AnimController.Collider;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (!character.AnimController.InWater && if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
character.AnimController.Anim != AnimController.Animation.Climbing &&
diff.Y < collider.height / 2 + collider.radius)
{ {
diff.Y = 0.0f; diff.Y = 0.0f;
} }
if (diff.LengthSquared() < 0.001f) return -host.Steering; if (diff.LengthSquared() < 0.001f) return -host.Steering;
return Vector2.Normalize(diff) * speed; return Vector2.Normalize(diff) * weight;
} }
private Vector2 DiffToCurrentNode() private Vector2 DiffToCurrentNode()
@@ -155,9 +158,7 @@ namespace Barotrauma
} }
//only humanoids can climb ladders //only humanoids can climb ladders
if (currentPath.CurrentNode != null && if (character.AnimController is HumanoidAnimController && InLadders && IsNextLadderSameAsCurrent)
currentPath.CurrentNode.Ladders != null &&
character.AnimController is HumanoidAnimController)
{ {
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item && if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition)) currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
@@ -167,13 +168,15 @@ namespace Barotrauma
} }
var collider = character.AnimController.Collider; var collider = character.AnimController.Collider;
if (character.IsClimbing)
if (character.AnimController.Anim == AnimController.Animation.Climbing)
{ {
Vector2 diff = currentPath.CurrentNode.SimPosition - pos; Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
//climbing ladders -> don't move horizontally if (IsNextLadderSameAsCurrent)
diff.X = 0.0f; {
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint //at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f) if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
@@ -184,26 +187,22 @@ namespace Barotrauma
diff.Y = Math.Max(diff.Y, 1.0f); diff.Y = Math.Max(diff.Y, 1.0f);
} }
//we can safely skip to the next waypoint if the character is at a safe height above the floor, bool aboveFloor = heightFromFloor > 0.0f && heightFromFloor < collider.height * 1.5f;
//or if the next waypoint in the path is also on ladders if (aboveFloor || IsNextNodeLadder)
if ((heightFromFloor > 0.0f && heightFromFloor < collider.height * 1.5f) ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null))
{ {
if (currentPath.NextNode != null && currentPath.NextNode.Ladders == null) if (!IsNextLadderSameAsCurrent)
{ {
character.AnimController.Anim = AnimController.Animation.None; character.AnimController.Anim = AnimController.Animation.None;
} }
currentPath.SkipToNextNode(); currentPath.SkipToNextNode();
} }
} }
else else if (IsNextLadderSameAsCurrent)
{ {
//if the current node is below the character and the next one is above (or vice versa) //if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one //and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders == currentPath.NextNode?.Ladders && if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{ {
currentPath.SkipToNextNode(); currentPath.SkipToNextNode();
} }
@@ -222,9 +221,11 @@ namespace Barotrauma
else else
{ {
Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderBottom = character.AnimController.GetColliderBottom();
if (Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X) < collider.radius * 2 && float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y && bool isAboveVerticalPosition = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + 1.5f) bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + 1.5f;
if (horizontalDistance < collider.radius * 2 && isAboveVerticalPosition && isNotTooHigh)
{ {
currentPath.SkipToNextNode(); currentPath.SkipToNextNode();
} }
@@ -180,8 +180,8 @@ namespace Barotrauma
{ {
//move closer to the wall //move closer to the wall
DeattachFromBody(); DeattachFromBody();
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, character.AnimController.GetCurrentSpeed(false) * 0.1f); enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos, character.AnimController.GetCurrentSpeed(true)); enemyAI.SteeringManager.SteeringSeek(wallAttachPos);
} }
} }
break; break;
@@ -1,20 +1,31 @@
using System; 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
{ {
abstract class AIObjective abstract class AIObjective
{ {
protected readonly List<AIObjective> subObjectives; public virtual float Devotion => AIObjectiveManager.baseDevotion;
public abstract string DebugTag { get; }
public virtual bool ForceRun => false;
public virtual bool KeepDivingGearOn => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
protected float priority; protected float priority;
protected readonly Character character; protected readonly Character character;
protected string option; protected string option;
protected bool abandon;
public virtual bool CanBeCompleted public virtual bool CanBeCompleted => !abandon && subObjectives.All(so => so.CanBeCompleted);
{ public IEnumerable<AIObjective> SubObjectives => subObjectives;
get { return true; } public AIObjective CurrentSubObjective { get; private set; }
}
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
public string Option public string Option
{ {
@@ -23,7 +34,6 @@ namespace Barotrauma
public AIObjective(Character character, string option) public AIObjective(Character character, string option)
{ {
subObjectives = new List<AIObjective>();
this.character = character; this.character = character;
this.option = option; this.option = option;
@@ -38,7 +48,36 @@ namespace Barotrauma
/// </summary> /// </summary>
public void TryComplete(float deltaTime) public void TryComplete(float deltaTime)
{ {
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted || ShouldInterruptSubObjective(s)); for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
if (subObjective.IsCompleted())
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.");
#endif
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.");
#endif
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)
{ {
@@ -56,32 +95,53 @@ namespace Barotrauma
subObjectives.Add(objective); subObjectives.Add(objective);
} }
public AIObjective GetCurrentSubObjective()
{
AIObjective currentSubObjective = this;
while (currentSubObjective.subObjectives.Count > 0)
{
currentSubObjective = subObjectives[0];
}
return currentSubObjective;
}
public void SortSubObjectives(AIObjectiveManager objectiveManager) public void SortSubObjectives(AIObjectiveManager objectiveManager)
{ {
if (!subObjectives.Any()) return; if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager))); subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager)));
subObjectives[0].SortSubObjectives(objectiveManager); CurrentSubObjective = SubObjectives.First();
CurrentSubObjective.SortSubObjectives(objectiveManager);
} }
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective) public virtual float GetPriority(AIObjectiveManager objectiveManager) => priority;
public virtual void Update(AIObjectiveManager objectiveManager, float deltaTime)
{ {
return false; var subObjective = objectiveManager.CurrentObjective?.CurrentSubObjective;
if (objectiveManager.CurrentOrder == this)
{
priority = AIObjectiveManager.OrderPriority;
}
else if (objectiveManager.CurrentObjective == this || subObjective == this)
{
priority += Devotion * deltaTime;
}
priority = MathHelper.Clamp(priority, 0, 100);
subObjectives.ForEach(so => so.Update(objectiveManager, deltaTime));
} }
/// <summary>
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
/// </summary>
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
{
foreach (T1 key in collection)
{
if (dictionary.TryGetValue(key, out T2 objective))
{
if (!subObjectives.Contains(objective))
{
dictionary.Remove(key);
}
}
}
}
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective) => false;
protected abstract void Act(float deltaTime); protected abstract void Act(float deltaTime);
public abstract bool IsCompleted(); public abstract bool IsCompleted();
public abstract float GetPriority(AIObjectiveManager objectiveManager);
public abstract bool IsDuplicate(AIObjective otherObjective); public abstract bool IsDuplicate(AIObjective otherObjective);
} }
} }
@@ -1,66 +1,58 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveChargeBatteries : AIObjective class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{ {
private List<PowerContainer> availableBatteries; public override string DebugTag => "charge batteries";
private readonly IEnumerable<PowerContainer> batteryList;
private string orderOption; public AIObjectiveChargeBatteries(Character character, string option) : base(character, option)
public AIObjectiveChargeBatteries(Character character, string option)
: base(character, option)
{ {
orderOption = option; batteryList = Item.ItemList.Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
availableBatteries = new List<PowerContainer>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null) continue;
if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) continue;
var powerContainer = item.GetComponent<PowerContainer>();
availableBatteries.Add(powerContainer);
}
if (availableBatteries.Count == 0)
{
character?.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public override bool IsCompleted()
{
return false;
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
return otherObjective is AIObjectiveChargeBatteries other && other.orderOption == orderOption; return otherObjective is AIObjectiveChargeBatteries other && other.Option == Option;
} }
protected override void Act(float deltaTime) protected override void FindTargets()
{ {
if (availableBatteries.Count == 0) foreach (Item item in Item.ItemList)
{ {
AddSubObjective(new AIObjectiveIdle(character)); if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) { continue; }
return; 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);
}
}
}
} }
foreach (PowerContainer battery in availableBatteries) if (targets.None())
{ {
AddSubObjective(new AIObjectiveOperateItem(battery, character, orderOption, false)); character.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
}
else
{
targets.Sort((x, y) => x.ChargePercentage.CompareTo(y.ChargePercentage));
} }
} }
protected override bool Filter(PowerContainer battery) => true;
protected override float Average(PowerContainer battery) => 100 - battery.ChargePercentage;
protected override IEnumerable<PowerContainer> GetList() => batteryList;
protected override AIObjective ObjectiveConstructor(PowerContainer battery) => new AIObjectiveOperateItem(battery, character, Option, false);
} }
} }
@@ -8,6 +8,10 @@ namespace Barotrauma
{ {
class AIObjectiveCombat : AIObjective class AIObjectiveCombat : AIObjective
{ {
public override string DebugTag => "combat";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float CoolDown = 10.0f; const float CoolDown = 10.0f;
//the largest amount of damage the enemy has inflicted on this character //the largest amount of damage the enemy has inflicted on this character
@@ -117,6 +121,7 @@ namespace Barotrauma
private void Escape(float deltaTime) private void Escape(float deltaTime)
{ {
// TODO: just let the find safety run?
if (escapeObjective == null) if (escapeObjective == null)
{ {
escapeObjective = new AIObjectiveFindSafety(character); escapeObjective = new AIObjectiveFindSafety(character);
@@ -7,6 +7,8 @@ namespace Barotrauma
{ {
class AIObjectiveContainItem: AIObjective class AIObjectiveContainItem: AIObjective
{ {
public override string DebugTag => "contain item";
public int MinContainedAmount = 1; public int MinContainedAmount = 1;
//can either be a tag or an identifier //can either be a tag or an identifier
@@ -6,6 +6,8 @@ namespace Barotrauma
{ {
class AIObjectiveDecontainItem : AIObjective class AIObjectiveDecontainItem : AIObjective
{ {
public override string DebugTag => "decontain item";
//can either be a tag or an identifier //can either be a tag or an identifier
private string[] itemIdentifiers; private string[] itemIdentifiers;
@@ -8,6 +8,10 @@ namespace Barotrauma
{ {
class AIObjectiveExtinguishFire : AIObjective class AIObjectiveExtinguishFire : AIObjective
{ {
public override string DebugTag => "extinguish fire";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private Hull targetHull; private Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective; private AIObjectiveGetItem getExtinguisherObjective;
@@ -16,15 +20,19 @@ namespace Barotrauma
private float useExtinquisherTimer; private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull) : public AIObjectiveExtinguishFire(Character character, Hull targetHull) : base(character, "")
base(character, "")
{ {
this.targetHull = targetHull; this.targetHull = targetHull;
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
return targetHull.FireSources.Sum(fs => fs.Size.X * 0.1f); if (gotoObjective != null && !gotoObjective.CanBeCompleted) { return 0; }
// 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 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));
return MathHelper.Clamp(priority * severityFactor * distanceFactor, 0, 100);
} }
public override bool IsCompleted() public override bool IsCompleted()
@@ -40,7 +48,7 @@ namespace Barotrauma
public override bool CanBeCompleted public override bool CanBeCompleted
{ {
get { return getExtinguisherObjective == null || getExtinguisherObjective.CanBeCompleted; } get { return getExtinguisherObjective == null || getExtinguisherObjective.IsCompleted() || getExtinguisherObjective.CanBeCompleted; }
} }
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
@@ -68,9 +76,11 @@ namespace Barotrauma
return; return;
} }
foreach (FireSource fs in targetHull.FireSources.ToList()) foreach (FireSource fs in targetHull.FireSources)
{ {
if (fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range)) || useExtinquisherTimer > 0.0f) // TODO: check if in the same room?
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
if (!character.IsClimbing && (inRange || useExtinquisherTimer > 0.0f))
{ {
useExtinquisherTimer += deltaTime; useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f) useExtinquisherTimer = 0.0f; if (useExtinquisherTimer > 2.0f) useExtinquisherTimer = 0.0f;
@@ -86,20 +96,19 @@ namespace Barotrauma
} }
return; return;
} }
}
foreach (FireSource fs in targetHull.FireSources)
{
//go to the first firesource
if (gotoObjective == null || !gotoObjective.CanBeCompleted || gotoObjective.IsCompleted())
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character);
}
else else
{ {
gotoObjective.TryComplete(deltaTime); //go to the first firesource
if (gotoObjective == null || !gotoObjective.CanBeCompleted || gotoObjective.IsCompleted())
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character);
}
else
{
gotoObjective.TryComplete(deltaTime);
}
break;
} }
break;
} }
} }
} }
@@ -1,32 +1,34 @@
using System.Linq; using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveExtinguishFires : AIObjective class AIObjectiveExtinguishFires : AIObjective
{ {
public AIObjectiveExtinguishFires(Character character) : public override string DebugTag => "extinguish fires";
base(character, "") public override bool ForceRun => true;
{ public override bool KeepDivingGearOn => true;
if (!Hull.hullList.Any(h => h.FireSources.Count > 0))
{ private Dictionary<Hull, AIObjectiveExtinguishFire> extinguishObjectives = new Dictionary<Hull, AIObjectiveExtinguishFire>();
character?.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
} public AIObjectiveExtinguishFires(Character character) : base(character, "") { }
}
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (objectiveManager.CurrentObjective == this) if (character.Submarine == null) { return 0; }
int fireCount = character.Submarine.GetHulls(true).Sum(h => h.FireSources.Count);
if (objectiveManager.CurrentOrder == this && fireCount > 0)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
} }
return Hull.hullList.Count(h => h.FireSources.Count > 0) * 10; return MathHelper.Clamp(fireCount * 20, 0, 100);
} }
public override bool IsCompleted() public override bool IsCompleted() => false;
{ public override bool CanBeCompleted => true;
return !Hull.hullList.Any(h => h.FireSources.Count > 0);
}
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
@@ -35,13 +37,26 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
SyncRemovedObjectives(extinguishObjectives, Hull.hullList);
if (character.Submarine == null) { return; }
foreach (Hull hull in Hull.hullList) foreach (Hull hull in Hull.hullList)
{ {
if (hull.FireSources.Count > 0) if (hull.FireSources.None()) { continue; }
if (hull.Submarine == null) { continue; }
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))
{ {
AddSubObjective(new AIObjectiveExtinguishFire(character, hull)); 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);
}
} }
} }
} }
@@ -6,6 +6,9 @@ namespace Barotrauma
{ {
class AIObjectiveFindDivingGear : AIObjective class AIObjectiveFindDivingGear : AIObjective
{ {
public override string DebugTag => "find diving gear";
public override bool ForceRun => true;
private AIObjective subObjective; private AIObjective subObjective;
private string gearTag; private string gearTag;
@@ -28,6 +31,8 @@ namespace Barotrauma
return false; return false;
} }
public override bool CanBeCompleted => subObjective == null || subObjective.CanBeCompleted;
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit) public AIObjectiveFindDivingGear(Character character, bool needDivingSuit)
: base(character, "") : base(character, "")
{ {
@@ -2,100 +2,127 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveFindSafety : AIObjective class AIObjectiveFindSafety : AIObjective
{ {
public override string DebugTag => "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
// TODO: expose?
const float priorityIncrease = 25;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f; const float SearchHullInterval = 3.0f;
const float MinSafety = 50.0f;
private AIObjectiveGoTo goToObjective; private List<Hull> unreachable = new List<Hull>();
private List<Hull> unreachable;
private float currenthullSafety; private float currenthullSafety;
private float searchHullTimer; private float searchHullTimer;
private AIObjectiveGoTo goToObjective;
private AIObjective divingGearObjective; private AIObjective divingGearObjective;
public float? OverrideCurrentHullSafety; public float? OverrideCurrentHullSafety;
public AIObjectiveFindSafety(Character character) public AIObjectiveFindSafety(Character character) : base(character, "") { }
: base(character, "")
{
unreachable = new List<Hull>();
}
public override bool IsCompleted() public override bool IsCompleted() => false;
{ public override bool CanBeCompleted => true;
return false;
}
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
var currentHull = character.AnimController.CurrentHull; var currentHull = character.AnimController.CurrentHull;
if (HumanAIController.NeedsDivingGear(currentHull))
currenthullSafety = OverrideCurrentHullSafety == null ?
GetHullSafety(currentHull, character) : (float)OverrideCurrentHullSafety;
if (NeedsDivingGear())
{ {
if (!FindDivingGear(deltaTime)) return; bool needsDivingSuit = currentHull == null || currentHull.WaterPercentage > 90;
bool hasEquipment = needsDivingSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingGear(character);
if ((divingGearObjective == null || !divingGearObjective.CanBeCompleted) && !hasEquipment)
{
// If the previous objective cannot be completed, create a new and try again.
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, wait for it to complete.
return;
}
} }
if (searchHullTimer > 0.0f) if (searchHullTimer > 0.0f)
{ {
searchHullTimer -= deltaTime; searchHullTimer -= deltaTime;
} }
else else if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{ {
var bestHull = FindBestHull(); var bestHull = FindBestHull();
if (bestHull != null) if (bestHull != null && bestHull != currentHull)
{ {
goToObjective = new AIObjectiveGoTo(bestHull, character) if (goToObjective != null)
{ {
AllowGoingOutside = true if (goToObjective.Target != bestHull)
}; {
goToObjective = new AIObjectiveGoTo(bestHull, character)
{
AllowGoingOutside = true
};
}
}
else
{
goToObjective = new AIObjectiveGoTo(bestHull, character)
{
AllowGoingOutside = true
};
}
} }
searchHullTimer = SearchHullInterval; searchHullTimer = SearchHullInterval;
} }
if (goToObjective != null) if (goToObjective != null)
{ {
goToObjective.TryComplete(deltaTime); goToObjective.TryComplete(deltaTime);
if (!goToObjective.CanBeCompleted)
if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
pathSteering.CurrentPath.Unreachable && !unreachable.Contains(goToObjective.Target))
{ {
unreachable.Add(goToObjective.Target as Hull); if (!unreachable.Contains(goToObjective.Target))
{
unreachable.Add(goToObjective.Target as Hull);
}
goToObjective = null; goToObjective = null;
} }
} }
//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
else if (currentHull != null) else if (currentHull != null)
{ {
//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
Vector2 escapeVel = Vector2.Zero; Vector2 escapeVel = Vector2.Zero;
foreach (FireSource fireSource in currentHull.FireSources) foreach (FireSource fireSource in currentHull.FireSources)
{ {
int dir = Math.Sign(character.Position.X - fireSource.Position.X); Vector2 dir = character.Position - fireSource.Position;
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(dir * distMultiplier, 0.0f); 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)
{ {
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))
{ {
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(character.Position.X - enemy.Position.X) * distMultiplier, 0.0f); escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
} }
} }
@@ -120,40 +147,70 @@ namespace Barotrauma
} }
} }
private bool FindDivingGear(float deltaTime)
{
if (divingGearObjective == null)
{
divingGearObjective = new AIObjectiveFindDivingGear(character, false);
}
if (divingGearObjective.IsCompleted()) return true;
divingGearObjective.TryComplete(deltaTime);
return divingGearObjective.IsCompleted();
}
private Hull FindBestHull() private Hull FindBestHull()
{ {
Hull bestHull = null; Hull bestHull = character.CurrentHull;
float bestValue = currenthullSafety; float bestValue = currenthullSafety;
foreach (Hull hull in Hull.hullList) foreach (Hull hull in Hull.hullList)
{ {
if (hull == character.AnimController.CurrentHull || unreachable.Contains(hull)) continue; if (unreachable.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
float hullSafety = 0;
if (character.Submarine == null)
{
// Outside
if (hull.RoomName?.ToLowerInvariant() == "airlock")
{
hullSafety = 100;
}
else
{
// TODO: could also target gaps that get us inside?
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull == hull && item.HasTag("airlock"))
{
hullSafety = 100;
break;
}
}
}
float hullValue = GetHullSafety(hull, character); // Huge preference for closer targets
//slight preference over hulls that are closer float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 100000, Vector2.Distance(character.WorldPosition, hull.WorldPosition)));
hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.X - hull.Position.X)) * 0.1f; hullSafety *= distanceFactor;
hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.Y - hull.Position.Y)) * 0.2f; // If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
if (bestHull == null || hullValue > bestValue) {
hullSafety /= 10;
}
}
else
{
// Inside
// Not connected.
if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, character);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
// Each unsafe node reduces the hull safety value.
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
{
hullSafety /= 10;
}
}
if (hullSafety > bestValue)
{ {
bestHull = hull; bestHull = hull;
bestValue = hullValue; bestValue = hullSafety;
} }
} }
return bestHull; return bestHull;
} }
@@ -162,113 +219,30 @@ namespace Barotrauma
return (otherObjective is AIObjectiveFindSafety); return (otherObjective is AIObjectiveFindSafety);
} }
private bool NeedsDivingGear() public override void Update(AIObjectiveManager objectiveManager, float deltaTime)
{ {
var currentHull = character.AnimController.CurrentHull; if (character.CurrentHull == null)
if (currentHull == null) return true;
//there's lots of water in the room -> get a suit
if (currentHull.WaterVolume / currentHull.Volume > 0.5f) return true;
if (currentHull.OxygenPercentage < 30.0f) return true;
return false;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (character.Oxygen < 80.0f)
{ {
return 150.0f - character.Oxygen; currenthullSafety = 0;
priority = 5;
return;
} }
if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { priority = 100; }
if (character.CurrentHull == null) return 5.0f; currenthullSafety = OverrideCurrentHullSafety ?? HumanAIController.GetHullSafety(character.CurrentHull);
currenthullSafety = GetHullSafety(character.CurrentHull, character); if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
priority = 100.0f - currenthullSafety;
//var nearbyHulls = character.CurrentHull.GetConnectedHulls(3);
//increase priority slightly if there's a fire in the room
//(will increase more heavily if near the damage range of the fire)
if (character.CurrentHull.FireSources.Count > 0)
{ {
priority += 5.0f; priority -= priorityDecrease * deltaTime;
}
/*foreach (Hull hull in nearbyHulls)
{
foreach (FireSource fireSource in hull.FireSources)
{
//heavily increase priority if almost within damage range of a fire
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * 1.25f))
{
priority += Math.Max(fireSource.Size.X, 50.0f);
}
}
}*/
if (NeedsDivingGear())
{
if (divingGearObjective != null && !divingGearObjective.IsCompleted()) priority += 20.0f;
}
return priority;
}
public static float GetHullSafety(Hull hull, Character character)
{
if (hull == null) return 0.0f;
float safety = 100.0f;
float waterPercentage = (hull.WaterVolume / hull.Volume) * 100.0f;
if (hull.LethalPressure > 0.0f && character.PressureProtection <= 0.0f)
{
safety -= 100.0f;
}
else if (character.OxygenAvailable <= 0.0f)
{
safety -= waterPercentage;
} }
else else
{ {
safety -= waterPercentage * 0.1f; float dangerFactor = (100 - currenthullSafety) / 100;
priority += dangerFactor * priorityIncrease * deltaTime;
} }
priority = MathHelper.Clamp(priority, 0, 100);
if (hull.OxygenPercentage < 30.0f) safety -= (30.0f - hull.OxygenPercentage) * 5.0f; if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted)
if (safety <= 0.0f) return 0.0f;
bool extinguishFires =
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFires ||
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFire;
float fireAmount = 0.0f;
var nearbyHulls = hull.GetConnectedHulls(3);
foreach (Hull hull2 in nearbyHulls)
{ {
foreach (FireSource fireSource in hull2.FireSources) priority = Math.Max(priority, AIObjectiveManager.OrderPriority + 10);
{
//increase priority if near the damage range of a fire
//if extinguishing fires, the character can go closer the damage range
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * (extinguishFires ? 1.25f : 5.0f)))
{
fireAmount += Math.Max(fireSource.Size.X, AIObjectiveManager.OrderPriority + 1.0f);
}
}
} }
safety -= fireAmount;
foreach (Character enemy in Character.CharacterList)
{
if (enemy.CurrentHull == hull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{
safety -= 10.0f;
}
}
return MathHelper.Clamp(safety, 0.0f, 100.0f);
} }
} }
} }
@@ -8,17 +8,22 @@ namespace Barotrauma
{ {
class AIObjectiveFixLeak : AIObjective class AIObjectiveFixLeak : AIObjective
{ {
public override string DebugTag => "fix leak";
public override bool KeepDivingGearOn => true;
private readonly Gap leak; private readonly Gap leak;
private bool pathUnreachable; private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective;
public Gap Leak public Gap Leak
{ {
get { return leak; } get { return leak; }
} }
public AIObjectiveFixLeak(Gap leak, Character character) public AIObjectiveFixLeak(Gap leak, Character character) : base (character, "")
: base (character, "")
{ {
this.leak = leak; this.leak = leak;
} }
@@ -28,10 +33,11 @@ namespace Barotrauma
return leak.Open <= 0.0f || leak.Removed || pathUnreachable; return leak.Open <= 0.0f || leak.Removed || pathUnreachable;
} }
public override bool CanBeCompleted => !abandon && base.CanBeCompleted;
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (leak.Open == 0.0f) { return 0.0f; } if (leak.Open == 0.0f) { return 0.0f; }
if (pathUnreachable) { return 0.0f; }
float leakSize = (leak.IsHorizontal ? leak.Rect.Height : leak.Rect.Width) * Math.Max(leak.Open, 0.1f); float leakSize = (leak.IsHorizontal ? leak.Rect.Height : leak.Rect.Width) * Math.Max(leak.Open, 0.1f);
@@ -49,6 +55,20 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (!leak.IsRoomToRoom)
{
if (findDivingGear == null)
{
findDivingGear = new AIObjectiveFindDivingGear(character, true);
AddSubObjective(findDivingGear);
}
else if (!findDivingGear.CanBeCompleted)
{
abandon = true;
return;
}
}
var weldingTool = character.Inventory.FindItemByTag("weldingtool"); var weldingTool = character.Inventory.FindItemByTag("weldingtool");
if (weldingTool == null) if (weldingTool == null)
@@ -72,31 +92,48 @@ namespace Barotrauma
var repairTool = weldingTool.GetComponent<RepairTool>(); var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null) { return; } if (repairTool == null) { return; }
Vector2 standPosition = GetStandPosition();
Vector2 gapDiff = leak.WorldPosition - character.WorldPosition; Vector2 gapDiff = leak.WorldPosition - character.WorldPosition;
var humanoidController = character.AnimController as HumanoidAnimController;
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && // TODO: use the collider size/reach?
Math.Abs(gapDiff.X) < 100.0f && gapDiff.Y < 0.0f && gapDiff.Y > -150.0f) if (!character.AnimController.InWater && humanoidController != null && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
{ {
((HumanoidAnimController)character.AnimController).Crouching = true; ((HumanoidAnimController)character.AnimController).Crouching = true;
} }
if (Math.Abs(gapDiff.X) > 100.0f || Math.Abs(gapDiff.Y) > 150.0f) float armLength = humanoidController != null ? ConvertUnits.ToDisplayUnits(humanoidController.ArmLength) : 100;
bool cannotReach = gapDiff.Length() > armLength + repairTool.Range;
if (cannotReach)
{ {
var gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(standPosition), character); if (gotoObjective != null)
if (!gotoObjective.IsCompleted())
{ {
pathUnreachable = !gotoObjective.CanBeCompleted; // Check if the objective is already removed -> completed/impossible
if (!pathUnreachable) if (!subObjectives.Contains(gotoObjective))
{
gotoObjective = null;
}
}
else
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character);
if (!subObjectives.Contains(gotoObjective))
{ {
AddSubObjective(gotoObjective); AddSubObjective(gotoObjective);
} }
return;
} }
} }
if (gotoObjective == null || gotoObjective.IsCompleted())
AddSubObjective(new AIObjectiveOperateItem(repairTool, character, "", true, leak)); {
if (operateObjective == null)
{
operateObjective = new AIObjectiveOperateItem(repairTool, character, "", true, leak);
AddSubObjective(operateObjective);
}
else if (!subObjectives.Contains(operateObjective))
{
operateObjective = null;
}
}
} }
private Vector2 GetStandPosition() private Vector2 GetStandPosition()
@@ -1,142 +1,45 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveFixLeaks : AIObjective class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{ {
const float UpdateGapListInterval = 5.0f; public override string DebugTag => "fix leaks";
public override bool KeepDivingGearOn => true;
private double lastGapUpdate; public AIObjectiveFixLeaks(Character character) : base (character, "") { }
private AIObjectiveIdle idleObjective;
private AIObjectiveFindDivingGear findDivingGear;
private List<AIObjectiveFixLeak> objectiveList;
public AIObjectiveFixLeaks(Character character)
: base (character, "")
{
}
public override bool IsCompleted()
{
if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null)
{
UpdateGapList();
lastGapUpdate = Timing.TotalTime;
}
return objectiveList.Count == 0;
}
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null) if (character.Submarine == null) { return 0; }
if (targets.None()) { return 0; }
if (objectiveManager.CurrentOrder == this)
{ {
UpdateGapList(); return AIObjectiveManager.OrderPriority;
lastGapUpdate = Timing.TotalTime;
} }
return MathHelper.Lerp(0, AIObjectiveManager.OrderPriority, targets.Average(t => Average(t)));
float priority = 0.0f;
foreach (AIObjectiveFixLeak fixObjective in objectiveList)
{
//gaps from outside to inside significantly increase the priority
if (!fixObjective.Leak.IsRoomToRoom)
{
priority = Math.Max(priority + fixObjective.Leak.Open * 100.0f, 50.0f);
}
else
{
priority += fixObjective.Leak.Open * 10.0f;
}
if (priority >= 100.0f) break;
}
return Math.Min(priority, 100.0f);
} }
protected override void Act(float deltaTime) protected override void FindTargets()
{ {
if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null) base.FindTargets();
{ targets.Sort((x, y) => GetGapFixPriority(y).CompareTo(GetGapFixPriority(x)));
UpdateGapList();
lastGapUpdate = Timing.TotalTime;
}
if (objectiveList.Any())
{
if (!objectiveList[objectiveList.Count - 1].Leak.IsRoomToRoom)
{
if (findDivingGear == null) findDivingGear = new AIObjectiveFindDivingGear(character, true);
if (!findDivingGear.IsCompleted() && findDivingGear.CanBeCompleted)
{
findDivingGear.TryComplete(deltaTime);
return;
}
}
objectiveList[objectiveList.Count - 1].TryComplete(deltaTime);
if (!objectiveList[objectiveList.Count - 1].CanBeCompleted ||
objectiveList[objectiveList.Count - 1].IsCompleted())
{
objectiveList.RemoveAt(objectiveList.Count - 1);
}
}
else
{
if (idleObjective == null) idleObjective = new AIObjectiveIdle(character);
idleObjective.TryComplete(deltaTime);
}
} }
private void UpdateGapList() protected override bool Filter(Gap gap)
{ {
if (objectiveList == null) { objectiveList = new List<AIObjectiveFixLeak>(); } bool ignore = ignoreList.Contains(gap) || gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null);
objectiveList.Clear(); if (!ignore)
foreach (Gap gap in Gap.GapList)
{ {
if (gap.ConnectedWall == null) { continue; } if (gap.Submarine == null) { ignore = true; }
if (gap.ConnectedDoor != null || gap.Open <= 0.0f) { continue; } else if (gap.Submarine.TeamID != character.TeamID) { ignore = true; }
//not linked to a hull -> ignore else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { ignore = true; }
if (gap.linkedTo.All(l => l == null)) { continue; }
if (character.TeamID == Character.TeamType.None)
{
//TODO: make sure the gap isn't in another sub (outpost, respawn shuttle...?)
if (gap.Submarine == null) continue;
}
else
{
//prevent characters from attempting to fix leaks in the enemy sub
//team 1 plays in sub 0, team 2 in sub 1
Submarine mySub = Submarine.MainSub;
if (character.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1)
{
mySub = Submarine.MainSubs[1];
}
if (gap.Submarine != mySub) continue;
}
float gapPriority = GetGapFixPriority(gap);
int index = 0;
while (index < objectiveList.Count &&
GetGapFixPriority(objectiveList[index].Leak) < gapPriority)
{
index++;
}
objectiveList.Insert(index, new AIObjectiveFixLeak(gap, character));
} }
return ignore;
} }
private float GetGapFixPriority(Gap gap) private float GetGapFixPriority(Gap gap)
@@ -156,9 +59,9 @@ namespace Barotrauma
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks;
{ protected override float Average(Gap gap) => gap.Open;
return otherObjective is AIObjectiveFixLeaks; protected override IEnumerable<Gap> GetList() => Gap.GapList;
} protected override AIObjective ObjectiveConstructor(Gap gap) => new AIObjectiveFixLeak(gap, character);
} }
} }
@@ -8,6 +8,8 @@ namespace Barotrauma
{ {
class AIObjectiveGetItem : AIObjective class AIObjectiveGetItem : AIObjective
{ {
public override string DebugTag => "get item";
public Func<Item, float> GetItemPriority; public Func<Item, float> GetItemPriority;
//can be either tags or identifiers //can be either tags or identifiers
@@ -17,8 +19,6 @@ namespace Barotrauma
private int currSearchIndex; private int currSearchIndex;
private bool canBeCompleted;
public bool IgnoreContainedItems; public bool IgnoreContainedItems;
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
@@ -27,10 +27,8 @@ namespace Barotrauma
private bool equip; private bool equip;
public override bool CanBeCompleted private bool canBeCompleted = true;
{ public override bool CanBeCompleted => canBeCompleted;
get { return canBeCompleted; }
}
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
@@ -45,7 +43,6 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, Item targetItem, bool equip = false) public AIObjectiveGetItem(Character character, Item targetItem, bool equip = false)
: base(character, "") : base(character, "")
{ {
canBeCompleted = true;
currSearchIndex = -1; currSearchIndex = -1;
this.equip = equip; this.equip = equip;
this.targetItem = targetItem; this.targetItem = targetItem;
@@ -59,7 +56,6 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false) public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false)
: base(character, "") : base(character, "")
{ {
canBeCompleted = true;
currSearchIndex = -1; currSearchIndex = -1;
this.equip = equip; this.equip = equip;
this.itemIdentifiers = itemIdentifiers; this.itemIdentifiers = itemIdentifiers;
@@ -112,6 +108,7 @@ namespace Barotrauma
FindTargetItem(); FindTargetItem();
if (targetItem == null || moveToTarget == null) if (targetItem == null || moveToTarget == null)
{ {
// TODO: cannot be completed?
character?.AIController?.SteeringManager?.Reset(); character?.AIController?.SteeringManager?.Reset();
return; return;
} }
@@ -184,12 +181,16 @@ namespace Barotrauma
{ {
if (itemIdentifiers == null) if (itemIdentifiers == null)
{ {
if (targetItem == null) canBeCompleted = false; if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined.");
#endif
canBeCompleted = false;
}
return; return;
} }
float currDist = moveToTarget == null ? 0.0f : Vector2.DistanceSquared(moveToTarget.Position, character.Position);
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++;
@@ -234,7 +235,13 @@ namespace Barotrauma
} }
//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) canBeCompleted = false; if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}");
#endif
canBeCompleted = false;
}
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components; using FarseerPhysics;
using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
@@ -7,9 +6,14 @@ namespace Barotrauma
{ {
class AIObjectiveGoTo : AIObjective class AIObjectiveGoTo : AIObjective
{ {
public override string DebugTag => "go to";
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;
@@ -40,24 +44,36 @@ namespace Barotrauma
{ {
get get
{ {
if (FollowControlledCharacter && Character.Controlled == null) { return false; } bool canComplete = !cannotReach && !abandon;
if (FollowControlledCharacter && Character.Controlled == null) { canComplete = false; }
if (Target != null && Target.Removed) { return false; } else if (Target != null && Target.Removed) { canComplete = false; }
else if (repeat || waitUntilPathUnreachable > 0.0f) { canComplete = true; }
if (repeat || waitUntilPathUnreachable > 0.0f) { return true; } else if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering)
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager; {
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable TODO: add a timer?
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable if (pathSteering.CurrentPath == null) { canComplete = true; }
if (pathSteering?.CurrentPath == null) { return true; } else if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) { canComplete = false; }
if (canComplete)
if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) { return false; } {
canComplete = !pathSteering.CurrentPath.Unreachable;
return !pathSteering.CurrentPath.Unreachable; }
}
if (!canComplete)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target.");
#endif
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 bool FollowControlledCharacter; public bool FollowControlledCharacter;
public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true) public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true)
@@ -97,7 +113,7 @@ namespace Barotrauma
waitUntilPathUnreachable -= deltaTime; waitUntilPathUnreachable -= deltaTime;
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null) if (!character.IsClimbing)
{ {
character.SelectedConstruction = null; character.SelectedConstruction = null;
} }
@@ -128,48 +144,32 @@ namespace Barotrauma
} }
else else
{ {
if (!AllowGoingOutside) var indoorSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
bool targetIsOutside = (Target != null && Target.Submarine == null) || (indoorSteering != null && indoorSteering.CurrentPath != null && indoorSteering.CurrentPath.HasOutdoorsNodes);
if (targetIsOutside && !AllowGoingOutside)
{ {
//if path is up-to-date and contains outdoors nodes, this path is unreachable cannotReach = true;
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering?.CurrentPath != null &&
Vector2.Distance(pathSteering.CurrentTarget, currTargetPos) < 1.0f &&
pathSteering.CurrentPath.HasOutdoorsNodes)
{
waitUntilPathUnreachable = 0.0f;
character.AIController.SteeringManager.Reset();
return;
}
} }
else
float normalSpeed = character.AnimController.GetCurrentSpeed(false);
character.AIController.SteeringManager.SteeringSeek(currTargetPos, normalSpeed);
if (getDivingGearIfNeeded && Target?.Submarine == null && AllowGoingOutside)
{ {
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable) character.AIController.SteeringManager.SteeringSeek(currTargetPos);
if (getDivingGearIfNeeded)
{ {
indoorsSteering.SteeringWander(normalSpeed); if (targetIsOutside ||
} Target is Hull h && HumanAIController.NeedsDivingGear(h) ||
else if (AllowGoingOutside && Target is Item i && HumanAIController.NeedsDivingGear(i.CurrentHull) ||
getDivingGearIfNeeded && Target is Character c && HumanAIController.NeedsDivingGear(c.CurrentHull))
indoorsSteering.CurrentPath != null && {
indoorsSteering.CurrentPath.HasOutdoorsNodes) if (findDivingGear == null)
{ {
AddSubObjective(new AIObjectiveFindDivingGear(character, true)); findDivingGear = new AIObjectiveFindDivingGear(character, true);
} AddSubObjective(findDivingGear);
} }
else if (character.AIController.SteeringManager is IndoorsSteeringManager indoorsSteering) else if (!findDivingGear.CanBeCompleted)
{ {
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable) abandon = true;
{ }
indoorsSteering.SteeringWander(normalSpeed); }
}
else if (AllowGoingOutside &&
getDivingGearIfNeeded &&
indoorsSteering.CurrentPath != null &&
indoorsSteering.CurrentPath.HasOutdoorsNodes)
{
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
} }
} }
} }
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components; using FarseerPhysics;
using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -9,26 +8,24 @@ namespace Barotrauma
{ {
class AIObjectiveIdle : AIObjective class AIObjectiveIdle : AIObjective
{ {
public override string DebugTag => "idle";
const float WallAvoidDistance = 150.0f; const float WallAvoidDistance = 150.0f;
private AITarget currentTarget; private Hull currentTarget;
private float newTargetTimer; private float newTargetTimer;
private float standStillTimer; private float standStillTimer;
private float walkDuration; private float walkDuration;
private AIObjectiveFindSafety findSafety;
public AIObjectiveIdle(Character character) : base(character, "") public AIObjectiveIdle(Character character) : base(character, "")
{ {
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);
} }
public override bool IsCompleted() public override bool IsCompleted() => false;
{ public override bool CanBeCompleted => true;
return false;
}
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
@@ -37,64 +34,59 @@ namespace Barotrauma
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager; 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)
{ {
character.DeselectCharacter(); character.DeselectCharacter();
} }
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null) if (!character.IsClimbing)
{ {
character.SelectedConstruction = null; character.SelectedConstruction = null;
} }
if (character.AnimController.InWater) if (currentTarget == null && (IsForbidden(character.CurrentHull) || HumanAIController.UnsafeHulls.Contains(character.CurrentHull)))
{ {
//attempt to find a safer place if in water newTargetTimer = 0;
if (findSafety == null) findSafety = new AIObjectiveFindSafety(character); standStillTimer = 0;
findSafety.TryComplete(deltaTime); }
return; if (character.AnimController.InWater || character.IsClimbing)
{
standStillTimer = 0;
} }
if (newTargetTimer <= 0.0f) if (newTargetTimer <= 0.0f)
{ {
currentTarget = FindRandomTarget(); currentTarget = FindRandomHull();
if (currentTarget != null) if (currentTarget != null)
{ {
Vector2 pos = character.SimPosition; string errorMsg = null;
if (character != null && character.Submarine == null) { pos -= Submarine.MainSub.SimPosition; } #if DEBUG
bool isRoomNameFound = currentTarget.RoomName != null;
string errorMsg = "(Character " + character.Name + " idling, target " errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.RoomName : currentTarget.ToString()) + ")";
+ ((currentTarget.Entity is Hull hull && hull.RoomName != null) ? hull.RoomName : currentTarget.Entity.ToString()) + ")"; #endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsg);
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition, errorMsg); PathSteering.SetPath(path);
if (path.Cost > 1000.0f && character.AnimController.CurrentHull!=null) return;
pathSteering.SetPath(path);
} }
newTargetTimer = currentTarget == null ? 5.0f : 15.0f; newTargetTimer = currentTarget == null ? 5.0f : 15.0f;
} }
newTargetTimer -= deltaTime; newTargetTimer -= deltaTime;
//wander randomly //wander randomly
// - if reached the end of the path // - if reached the end of the path
// - if the target is unreachable // - if the target is unreachable
// - if the path requires going outside // - if the path requires going outside
if (pathSteering == null || (pathSteering.CurrentPath != null && if (PathSteering == null || (PathSteering.CurrentPath != null &&
(pathSteering.CurrentPath.NextNode == null || pathSteering.CurrentPath.Unreachable || pathSteering.CurrentPath.HasOutdoorsNodes))) (PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{ {
standStillTimer -= deltaTime; standStillTimer -= deltaTime;
if (standStillTimer > 0.0f) if (standStillTimer > 0.0f)
{ {
walkDuration = Rand.Range(1.0f, 5.0f); walkDuration = Rand.Range(1.0f, 5.0f);
pathSteering.Reset(); PathSteering.Reset();
return; return;
} }
@@ -104,7 +96,7 @@ namespace Barotrauma
} }
//steer away from edges of the hull //steer away from edges of the hull
if (character.AnimController.CurrentHull != null) if (character.AnimController.CurrentHull != null && !character.IsClimbing)
{ {
float leftDist = character.Position.X - character.AnimController.CurrentHull.Rect.X; float leftDist = character.Position.X - character.AnimController.CurrentHull.Rect.X;
float rightDist = character.AnimController.CurrentHull.Rect.Right - character.Position.X; float rightDist = character.AnimController.CurrentHull.Rect.Right - character.Position.X;
@@ -113,95 +105,119 @@ namespace Barotrauma
{ {
if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2) if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2)
{ {
pathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist)); PathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
} }
else else
{ {
pathSteering.Reset(); PathSteering.Reset();
return; return;
} }
} }
else if (leftDist < WallAvoidDistance) else if (leftDist < WallAvoidDistance)
{ {
pathSteering.SteeringManual(deltaTime, Vector2.UnitX * (WallAvoidDistance-leftDist)/WallAvoidDistance); PathSteering.SteeringManual(deltaTime, Vector2.UnitX * (WallAvoidDistance-leftDist) / WallAvoidDistance);
pathSteering.WanderAngle = 0.0f; PathSteering.WanderAngle = 0.0f;
return; return;
} }
else if (rightDist < WallAvoidDistance) else if (rightDist < WallAvoidDistance)
{ {
pathSteering.SteeringManual(deltaTime, -Vector2.UnitX * (WallAvoidDistance-rightDist)/WallAvoidDistance); PathSteering.SteeringManual(deltaTime, -Vector2.UnitX * (WallAvoidDistance-rightDist) / WallAvoidDistance);
pathSteering.WanderAngle = MathHelper.Pi; PathSteering.WanderAngle = MathHelper.Pi;
return; return;
} }
} }
character.AIController.SteeringManager.SteeringWander(character.AnimController.GetCurrentSpeed(false)); character.AIController.SteeringManager.SteeringWander();
//reset vertical steering to prevent dropping down from platforms etc if (!character.IsClimbing)
character.AIController.SteeringManager.ResetY(); {
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
}
return; return;
} }
if (currentTarget?.Entity == null) return; if (currentTarget != null)
if (currentTarget.Entity.Removed)
{ {
currentTarget = null; character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition);
return;
} }
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, character.AnimController.GetCurrentSpeed(true));
} }
private AITarget FindRandomTarget() private readonly List<Hull> targetHulls = new List<Hull>(20);
{ private readonly List<float> hullWeights = new List<float>(20);
//random chance of navigating back to the room where the character spawned
if (Rand.Int(5) == 1)
{
var idCard = character.Inventory.FindItemByIdentifier("idcard");
if (idCard == null) return null;
private Hull FindRandomHull()
{
var idCard = character.Inventory.FindItemByIdentifier("idcard");
Hull targetHull = null;
//random chance of navigating back to the room where the character spawned
if (Rand.Int(5) == 1 && idCard != null)
{
foreach (WayPoint wp in WayPoint.WayPointList) foreach (WayPoint wp in WayPoint.WayPointList)
{ {
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull == null) continue; if (wp.SpawnType != SpawnType.Human || wp.CurrentHull == null) { continue; }
foreach (string tag in wp.IdCardTags) foreach (string tag in wp.IdCardTags)
{ {
if (idCard.HasTag(tag)) return wp.CurrentHull.AiTarget; if (idCard.HasTag(tag))
{
targetHull = wp.CurrentHull;
}
} }
} }
} }
else if (targetHull == null)
{ {
List<Hull> targetHulls = new List<Hull>(Hull.hullList); targetHulls.Clear();
//ignore all hulls with fires or water in them hullWeights.Clear();
targetHulls.RemoveAll(h => h.FireSources.Any() || h.WaterVolume / h.Volume > 0.1f); foreach (var hull in Hull.hullList)
if (character.Submarine != null)
{ {
targetHulls.RemoveAll(h => h.Submarine != character.Submarine); if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
} if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != character.TeamID) { continue; }
//remove ballast hulls // If the character is inside, only take connected hulls into account.
foreach (Item item in Item.ItemList) if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
{ if (IsForbidden(hull)) { continue; }
if (item.HasTag("ballast") && targetHulls.Contains(item.CurrentHull)) // Ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{ {
targetHulls.Remove(item.CurrentHull); if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
{
continue;
}
}
// Check that there is no unsafe or forbidden hulls on the way to the target
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) { continue; }
// If we want to do a steering check, we should do it here, before setting the path
//if (path.Cost > 1000.0f) { continue; }
if (!targetHulls.Contains(hull))
{
targetHulls.Add(hull);
hullWeights.Add(hull.Volume);
} }
} }
return ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
//ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{
float minHeight = ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value);
targetHulls.RemoveAll(h => h.CeilingHeight < minHeight);
}
if (!targetHulls.Any()) return null;
//prefer larger hulls
var targetHull = ToolBox.SelectWeightedRandom(targetHulls, targetHulls.Select(h => h.Volume).ToList(), Rand.RandSync.Unsynced);
return targetHull?.AiTarget;
} }
return targetHull;
}
return null; private bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName?.ToLowerInvariant();
bool isForbidden = hullName == "ballast" || hullName == "airlock";
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull == hull && (item.HasTag("ballast") || item.HasTag("airlock")))
{
isForbidden = true;
break;
}
}
return isForbidden;
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
abstract class AIObjectiveLoop<T> : AIObjective
{
protected List<T> targets = new List<T>();
protected Dictionary<T, AIObjective> objectives = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
protected readonly float ignoreListClearInterval = 30;
private float ignoreListTimer;
protected readonly float targetUpdateInterval = 2;
private float targetUpdateTimer;
public AIObjectiveLoop(Character character, string option) : base(character, option)
{
FindTargets();
CreateObjectives();
}
protected override void Act(float deltaTime) { }
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
public override void Update(AIObjectiveManager objectiveManager, float deltaTime)
{
base.Update(objectiveManager, deltaTime);
if (ignoreListTimer > ignoreListClearInterval)
{
ignoreList.Clear();
ignoreListTimer = 0;
UpdateTargets();
}
else
{
ignoreListTimer += deltaTime;
}
if (targetUpdateTimer > targetUpdateInterval)
{
targetUpdateTimer = 0;
UpdateTargets();
}
else
{
targetUpdateTimer += deltaTime;
}
// Sync objectives, subobjectives and targets
foreach (var objective in objectives)
{
var target = objective.Key;
if (!objective.Value.CanBeCompleted)
{
ignoreList.Add(target);
}
if (!targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(objectives, GetList());
if (objectives.None())
{
CreateObjectives();
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (character.Submarine == null) { return 0; }
if (targets.None()) { return 0; }
float avg = targets.Average(t => Average(t));
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority - MathHelper.Max(0, AIObjectiveManager.OrderPriority - avg);
}
return MathHelper.Lerp(0, AIObjectiveManager.OrderPriority, avg / 100);
}
protected void UpdateTargets()
{
targets.Clear();
FindTargets();
CreateObjectives();
}
protected virtual void FindTargets()
{
foreach (T item in GetList())
{
if (Filter(item)) { continue; }
if (!targets.Contains(item))
{
targets.Add(item);
}
}
}
protected virtual void CreateObjectives()
{
foreach (T target in targets)
{
if (!objectives.TryGetValue(target, out AIObjective objective))
{
objective = ObjectiveConstructor(target);
objectives.Add(target, objective);
AddSubObjective(objective);
}
}
}
/// <summary>
/// List of all possible items of the specified type. Used for filtering the removed objectives.
/// </summary>
protected abstract IEnumerable<T> GetList();
protected abstract float Average(T target);
protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target);
}
}
@@ -2,47 +2,58 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveManager class AIObjectiveManager
{ {
// TODO: expose
public const float OrderPriority = 50.0f; public const float OrderPriority = 50.0f;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 2;
private List<AIObjective> objectives; public List<AIObjective> Objectives { get; private set; }
private Character character; private Character character;
private AIObjective currentOrder;
/// <summary> /// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out (assuming they don't a high priority order active) /// When set above zero, the character will stand still doing nothing until the timer runs out (assuming they don't a high priority order active)
/// </summary> /// </summary>
public float WaitTimer; public float WaitTimer;
public AIObjective CurrentOrder public AIObjective CurrentOrder { get; private set; }
{ public AIObjective CurrentObjective { get; private set; }
get { return currentOrder; }
}
public AIObjective CurrentObjective
{
get;
private set;
}
public AIObjectiveManager(Character character) public AIObjectiveManager(Character character)
{ {
this.character = character; this.character = character;
objectives = new List<AIObjective>(); Objectives = new List<AIObjective>();
} }
public void AddObjective(AIObjective objective) public void AddObjective(AIObjective objective)
{ {
if (objectives.Find(o => o.IsDuplicate(objective)) != null) return; if (Objectives.Find(o => o.IsDuplicate(objective)) != null) return;
objectives.Add(objective); Objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void AddObjective(AIObjective objective, float delay, Action callback = null)
{
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
{
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
{
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
}, delay);
DelayedObjectives.Add(objective, coroutine);
} }
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>(); public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
@@ -64,7 +75,7 @@ namespace Barotrauma
public T GetObjective<T>() where T : AIObjective public T GetObjective<T>() where T : AIObjective
{ {
foreach (AIObjective objective in objectives) foreach (AIObjective objective in Objectives)
{ {
if (objective is T) return (T)objective; if (objective is T) return (T)objective;
} }
@@ -73,62 +84,85 @@ namespace Barotrauma
private AIObjective GetCurrentObjective() private AIObjective GetCurrentObjective()
{ {
if (CurrentOrder != null && var firstObjective = Objectives.FirstOrDefault();
(objectives.Count == 0 || currentOrder.GetPriority(this) > objectives[0].GetPriority(this))) if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority(this) > firstObjective.GetPriority(this))
{ {
return CurrentOrder; CurrentObjective = CurrentOrder;
} }
else
return objectives.Count == 0 ? null : objectives[0]; {
CurrentObjective = firstObjective;
}
return CurrentObjective;
} }
public float GetCurrentPriority() public float GetCurrentPriority()
{ {
var currentObjective = GetCurrentObjective(); return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority(this);
return currentObjective == null ? 0.0f : currentObjective.GetPriority(this);
} }
public void UpdateObjectives() public void UpdateObjectives(float deltaTime)
{ {
if (!objectives.Any()) return; CurrentOrder?.Update(this, deltaTime);
for (int i = 0; i < Objectives.Count; i++)
//remove completed objectives and ones that can't be completed {
objectives = objectives.FindAll(o => !o.IsCompleted() && o.CanBeCompleted); var objective = Objectives[i];
if (objective.IsCompleted())
//sort objectives according to priority {
objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this))); #if DEBUG
GetCurrentObjective()?.SortSubObjectives(this); DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it is completed.");
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it cannot be completed.");
#endif
Objectives.Remove(objective);
}
else
{
objective.Update(this, deltaTime);
}
}
GetCurrentObjective();
} }
public void SortObjectives()
{
if (Objectives.Any())
{
Objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this)));
}
CurrentObjective?.SortSubObjectives(this);
}
public void DoCurrentObjective(float deltaTime) public void DoCurrentObjective(float deltaTime)
{ {
CurrentObjective = GetCurrentObjective();
if (CurrentObjective == null || (CurrentObjective.GetPriority(this) < OrderPriority && WaitTimer > 0.0f)) if (CurrentObjective == null || (CurrentObjective.GetPriority(this) < OrderPriority && WaitTimer > 0.0f))
{ {
WaitTimer -= deltaTime; WaitTimer -= deltaTime;
character.AIController.SteeringManager.Reset(); character.AIController.SteeringManager.Reset();
return; return;
} }
CurrentObjective?.TryComplete(deltaTime); CurrentObjective?.TryComplete(deltaTime);
} }
public void SetOrder(AIObjective objective) public void SetOrder(AIObjective objective)
{ {
currentOrder = objective; CurrentOrder = objective;
} }
public void SetOrder(Order order, string option, Character orderGiver) public void SetOrder(Order order, string option, Character orderGiver)
{ {
currentOrder = null; CurrentOrder = null;
if (order == null) return; if (order == null) return;
switch (order.AITag.ToLowerInvariant()) switch (order.AITag.ToLowerInvariant())
{ {
case "follow": case "follow":
currentOrder = new AIObjectiveGoTo(orderGiver, character, true) CurrentOrder = new AIObjectiveGoTo(orderGiver, character, true)
{ {
CloseEnough = 1.5f, CloseEnough = 1.5f,
AllowGoingOutside = true, AllowGoingOutside = true,
@@ -137,13 +171,34 @@ namespace Barotrauma
}; };
break; break;
case "wait": case "wait":
currentOrder = new AIObjectiveGoTo(character, character, true) CurrentOrder = new AIObjectiveGoTo(character, character, true)
{ {
AllowGoingOutside = true AllowGoingOutside = true
}; };
break; break;
case "fixleaks": case "fixleaks":
currentOrder = new AIObjectiveFixLeaks(character); CurrentOrder = new AIObjectiveFixLeaks(character);
break;
case "chargebatteries":
CurrentOrder = new AIObjectiveChargeBatteries(character, option);
break;
case "rescue":
CurrentOrder = new AIObjectiveRescueAll(character);
break;
case "repairsystems":
CurrentOrder = new AIObjectiveRepairItems(character) { RequireAdequateSkills = option != "all" };
break;
case "pumpwater":
CurrentOrder = new AIObjectivePumpWater(character, option);
break;
case "extinguishfires":
CurrentOrder = new AIObjectiveExtinguishFires(character);
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (order.TargetItemComponent == null) return;
CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break; break;
case "chargebatteries": case "chargebatteries":
currentOrder = new AIObjectiveChargeBatteries(character, option); currentOrder = new AIObjectiveChargeBatteries(character, option);
@@ -168,7 +223,7 @@ namespace Barotrauma
break; break;
default: default:
if (order.TargetItemComponent == null) return; if (order.TargetItemComponent == null) return;
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController); CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break; break;
} }
} }
@@ -7,6 +7,8 @@ namespace Barotrauma
{ {
class AIObjectiveOperateItem : AIObjective class AIObjectiveOperateItem : AIObjective
{ {
public override string DebugTag => "operate item";
private ItemComponent component, controller; private ItemComponent component, controller;
private Entity operateTarget; private Entity operateTarget;
@@ -38,14 +40,16 @@ namespace Barotrauma
get { return operateTarget; } get { return operateTarget; }
} }
public ItemComponent Component => component;
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (gotoObjective != null && !gotoObjective.CanBeCompleted) { return 0; }
if (objectiveManager.CurrentOrder == this) if (objectiveManager.CurrentOrder == this)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
} }
return base.GetPriority(objectiveManager);
return 1.0f;
} }
public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false) public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false)
@@ -158,7 +162,7 @@ namespace Barotrauma
AIObjectiveOperateItem operateItem = otherObjective as AIObjectiveOperateItem; AIObjectiveOperateItem operateItem = otherObjective as AIObjectiveOperateItem;
if (operateItem == null) return false; if (operateItem == null) return false;
return (operateItem.component == component); return (operateItem.component == component ||otherObjective.Option == Option);
} }
} }
} }
@@ -1,108 +1,68 @@
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 System.Text;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectivePumpWater : AIObjective class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{ {
private const float FindPumpsInterval = 5.0f; public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
private readonly IEnumerable<Pump> pumpList;
private string orderOption; public AIObjectivePumpWater(Character character, string option) : base(character, option)
private List<Pump> pumps;
private float lastFindPumpsTime;
public AIObjectivePumpWater(Character character, string option)
: base(character, option)
{ {
orderOption = option; pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (Timing.TotalTime >= lastFindPumpsTime + FindPumpsInterval) if (character.Submarine == null) { return 0; }
{ if (objectiveManager.CurrentOrder == this && targets.Count > 0)
FindPumps();
}
if (objectiveManager.CurrentOrder == this && pumps.Count > 0)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
} }
return 0.0f; return 0.0f;
} }
public override bool IsCompleted() 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()
{ {
return false; if (option == null) { return; }
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectivePumpWater;
}
protected override void Act(float deltaTime)
{
if (Timing.TotalTime < lastFindPumpsTime + FindPumpsInterval)
{
return;
}
FindPumps();
}
private void FindPumps()
{
lastFindPumpsTime = (float)Timing.TotalTime;
pumps = new List<Pump>();
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
//don't attempt to use pumps outside the sub if (item.HasTag("ballast")) { continue; }
if (item.Submarine == null) { continue; } 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>(); var pump = item.GetComponent<Pump>();
if (pump == null) continue; if (pump != null)
if (item.HasTag("ballast")) continue;
//if the pump is connected to an item with a steering component, it must be a ballast pump
//(This may not work correctly if the signals are passed through some fancy circuit or a wifi component,
//which is why sub creators are encouraged to tag the ballast pumps)
bool connectedToSteering = false;
foreach (Connection c in item.Connections)
{ {
if (c.IsPower) continue; if (!ignoreList.Contains(pump))
if (item.GetConnectedComponentsRecursive<Steering>(c).Count > 0)
{ {
connectedToSteering = true; if (option == "stoppumping")
break; {
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);
}
} }
} }
if (connectedToSteering) continue;
if (orderOption.ToLowerInvariant() == "stop pumping")
{
if (!pump.IsActive || pump.FlowPercentage == 0.0f) continue;
}
else
{
if (!pump.Item.InWater) continue;
if (pump.IsActive && pump.FlowPercentage <= -90.0f) continue;
}
pumps.Add(pump);
}
foreach (Pump pump in pumps)
{
AddSubObjective(new AIObjectiveOperateItem(pump, character, orderOption, false));
} }
} }
protected override bool Filter(Pump pump) => true;
protected override IEnumerable<Pump> GetList() => pumpList;
protected override AIObjective ObjectiveConstructor(Pump pump) => new AIObjectiveOperateItem(pump, character, Option, false);
protected override float Average(Pump target) => 0;
} }
} }
@@ -2,100 +2,125 @@
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 AIObjectiveRepairItem : AIObjective class AIObjectiveRepairItem : AIObjective
{ {
private Item item; public override string DebugTag => "repair item";
public AIObjectiveRepairItem(Character character, Item item) public override bool KeepDivingGearOn => true;
: base(character, "")
public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective;
private float previousCondition = -1;
public AIObjectiveRepairItem(Character character, Item item) : base(character, "")
{ {
this.item = item; Item = item;
} }
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
bool insufficientSkills = true; // TODO: priority list?
bool repairablesFound = false; if (Item.Repairables.None()) { return 0; }
foreach (Repairable repairable in item.Repairables) // Ignore items that are being repaired by someone else.
{ if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
if (item.Condition > repairable.ShowRepairUIThreshold) { continue; } // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
if (repairable.DegreeOfSuccess(character) >= 0.5f) { insufficientSkills = false; } float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y) * 2.0f;
repairablesFound = true; float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist));
} float damagePriority = MathHelper.Lerp(1, 0, (Item.Condition + 10) / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
if (!repairablesFound) { return 0.0f; } float isSelected = character.SelectedConstruction == Item ? 50 : 0;
float baseLevel = Math.Max(priority + isSelected, 1);
float priority = item.Prefab.Health - item.Condition; return MathHelper.Clamp(baseLevel * damagePriority * distanceFactor * successFactor, 0, 100);
//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;
//heavily increase the priority if the item is already selected
//so characters don't keep switching between nearby damaged items
if (character.SelectedConstruction == item)
{
priority += 50.0f;
}
if (insufficientSkills)
{
return MathHelper.Lerp(0.0f, 50.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
}
else
{
return MathHelper.Lerp(50.0f, 100.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
}
} }
public override bool CanBeCompleted => !abandon;
public override bool IsCompleted() public override bool IsCompleted()
{ {
foreach (Repairable repairable in item.Repairables) bool isCompleted = Item.IsFullCondition;
if (isCompleted)
{ {
if (item.Condition < Math.Max(repairable.ShowRepairUIThreshold, item.Prefab.Health * 0.98f)) return false; character?.Speak(TextManager.Get("DialogItemRepaired").Replace("[itemname]", Item.Name), null, 0.0f, "itemrepaired", 10.0f);
} }
return isCompleted;
character?.Speak(TextManager.Get("DialogItemRepaired").Replace("[itemname]", item.Name), null, 0.0f, "itemrepaired", 10.0f);
return true;
} }
public override bool IsDuplicate(AIObjective otherObjective) public override bool IsDuplicate(AIObjective otherObjective)
{ {
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.item == item; return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.Item == Item;
} }
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
foreach (Repairable repairable in item.Repairables) if (goToObjective != null && !subObjectives.Contains(goToObjective))
{ {
//make sure we have all the items required to fix the target item if (!goToObjective.IsCompleted() && !goToObjective.CanBeCompleted)
foreach (var kvp in repairable.requiredItems)
{ {
foreach (RelatedItem requiredItem in kvp.Value) 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)
{
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
{ {
if (!character.Inventory.Items.Any(it => it != null && requiredItem.MatchesItem(it))) foreach (RelatedItem requiredItem in kvp.Value)
{ {
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true)); AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true));
return;
} }
} }
return;
} }
} }
if (character.CanInteractWith(Item))
if (character.CanInteractWith(item))
{ {
foreach (Repairable repairable in item.Repairables) foreach (Repairable repairable in Item.Repairables)
{ {
if (character.SelectedConstruction != item) { item.TryInteract(character, true, true); } if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
repairable.CurrentFixer = character; {
// Someone else is repairing the target. Abandon the objective if the other is better at this then us.
abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!abandon)
{
if (character.SelectedConstruction != Item)
{
Item.TryInteract(character, true, true);
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// 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;
character?.Speak(TextManager.Get("DialogRepairFailed").Replace("[itemname]", Item.Name), null, 0.0f, "repairfailed", 10.0f);
}
}
repairable.CurrentFixer = abandon && repairable.CurrentFixer == character ? null : character;
break; break;
} }
} }
else else if (goToObjective == null || goToObjective.Target != Item)
{ {
AddSubObjective(new AIObjectiveGoTo(item, character)); previousCondition = -1;
if (goToObjective != null)
{
subObjectives.Remove(goToObjective);
}
goToObjective = new AIObjectiveGoTo(Item, character);
AddSubObjective(goToObjective);
} }
} }
} }
@@ -1,68 +1,68 @@
using Barotrauma.Items.Components; using System.Collections.Generic;
using System; using Barotrauma.Items.Components;
using System.Collections.Generic; using Barotrauma.Extensions;
using System.Text;
namespace Barotrauma namespace Barotrauma
{ {
class AIObjectiveRepairItems : AIObjective class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{ {
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) public AIObjectiveRepairItems(Character character) : base(character, "") { }
: base(character, "")
{
}
public override float GetPriority(AIObjectiveManager objectiveManager) // 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;
protected override void CreateObjectives()
{ {
GetBrokenItems(); foreach (var item in targets)
if (subObjectives.Count > 0 && objectiveManager.CurrentOrder == this)
{ {
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
}
protected override void Act(float deltaTime)
{
GetBrokenItems();
}
private void GetBrokenItems()
{
foreach (Item item in Item.ItemList)
{
//ignore items that are in full condition
if (item.Condition >= item.Prefab.Health) continue;
foreach (Repairable repairable in item.Repairables) foreach (Repairable repairable in item.Repairables)
{ {
//ignore ones that are already fixed if (!objectives.TryGetValue(item, out AIObjective objective))
if (item.Condition > repairable.ShowRepairUIThreshold) continue;
if (RequireAdequateSkills)
{ {
if (!repairable.HasRequiredSkills(character)) { continue; } objective = ObjectiveConstructor(item);
objectives.Add(item, objective);
AddSubObjective(objective);
} }
AddSubObjective(new AIObjectiveRepairItem(character, item));
break; break;
} }
} }
} }
protected override bool Filter(Item item)
{
bool ignore = ignoreList.Contains(item) || item.IsFullCondition;
if (!ignore)
{
if (item.Submarine == null) { ignore = true; }
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 (item.Condition > repairable.ShowRepairUIThreshold) { ignore = true; }
else if (RequireAdequateSkills && !repairable.HasRequiredSkills(character)) { ignore = true; }
if (ignore) { break; }
}
}
}
}
return ignore;
}
protected override float Average(Item item) => 100 - item.ConditionPercentage;
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item);
} }
} }
@@ -9,6 +9,10 @@ namespace Barotrauma
{ {
class AIObjectiveRescue : AIObjective class AIObjectiveRescue : AIObjective
{ {
public override string DebugTag => "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f; const float TreatmentDelay = 0.5f;
private readonly Character targetCharacter; private readonly Character targetCharacter;
@@ -102,7 +106,7 @@ namespace Barotrauma
} }
return !character.AnimController.InWater && !targetCharacter.AnimController.InWater && return !character.AnimController.InWater && !targetCharacter.AnimController.InWater &&
AIObjectiveFindSafety.GetHullSafety(character.CurrentHull, character) > 50.0f; HumanAIController.GetHullSafety(character.CurrentHull, character) > HumanAIController.HULL_SAFETY_THRESHOLD;
} }
return false; return false;
@@ -5,13 +5,16 @@ namespace Barotrauma
{ {
class AIObjectiveRescueAll : AIObjective class AIObjectiveRescueAll : AIObjective
{ {
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; private List<Character> rescueTargets;
public AIObjectiveRescueAll(Character character) public AIObjectiveRescueAll(Character character) : base (character, "")
: base (character, "")
{ {
rescueTargets = new List<Character>(); rescueTargets = new List<Character>();
} }
@@ -23,10 +26,11 @@ namespace Barotrauma
public override float GetPriority(AIObjectiveManager objectiveManager) public override float GetPriority(AIObjectiveManager objectiveManager)
{ {
if (character.Submarine == null) { return 0; }
GetRescueTargets(); GetRescueTargets();
if (!rescueTargets.Any()) { return 0.0f; } if (!rescueTargets.Any()) { return 0.0f; }
if (objectiveManager.CurrentObjective == this) if (objectiveManager.CurrentOrder == this)
{ {
return AIObjectiveManager.OrderPriority; return AIObjectiveManager.OrderPriority;
} }
@@ -40,6 +44,7 @@ namespace Barotrauma
{ {
rescueTargets = Character.CharacterList.FindAll(c => rescueTargets = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController && c.AIController is HumanAIController &&
c.TeamID == character.TeamID &&
c != character && c != character &&
!c.IsDead && !c.IsDead &&
c.Vitality / c.MaxVitality < VitalityThreshold); c.Vitality / c.MaxVitality < VitalityThreshold);
@@ -53,9 +58,7 @@ namespace Barotrauma
} }
} }
public override bool IsCompleted() public override bool IsCompleted() => false;
{ public override bool CanBeCompleted => true;
return false;
}
} }
} }
@@ -155,7 +155,7 @@ namespace Barotrauma
} }
} }
public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr) public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr = null)
{ {
float closestDist = 0.0f; float closestDist = 0.0f;
PathNode startNode = null; PathNode startNode = null;
@@ -33,19 +33,19 @@ namespace Barotrauma
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi); wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
} }
public void SteeringSeek(Vector2 targetSimPos, float speed) public void SteeringSeek(Vector2 targetSimPos, float weight = 1)
{ {
steering += DoSteeringSeek(targetSimPos, speed); steering += DoSteeringSeek(targetSimPos, weight);
} }
public void SteeringWander(float speed) public void SteeringWander(float weight = 1)
{ {
steering += DoSteeringWander(speed); steering += DoSteeringWander(weight);
} }
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float speed) public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
{ {
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, speed); steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight);
} }
public void SteeringManual(float deltaTime, Vector2 velocity) public void SteeringManual(float deltaTime, Vector2 velocity)
@@ -76,39 +76,36 @@ namespace Barotrauma
host.Steering = Vector2.Zero; host.Steering = Vector2.Zero;
return; return;
} }
if (steering.LengthSquared() > speed * speed)
float steeringSpeed = steering.Length();
if (steeringSpeed > speed)
{ {
steering = Vector2.Normalize(steering) * Math.Abs(speed); steering = Vector2.Normalize(steering) * Math.Abs(speed);
} }
host.Steering = steering; host.Steering = steering;
} }
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed) protected virtual Vector2 DoSteeringSeek(Vector2 target, float weight)
{ {
Vector2 targetVel = target - host.SimPosition; Vector2 targetVel = target - host.SimPosition;
if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero; if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero;
targetVel = Vector2.Normalize(targetVel) * speed; targetVel = Vector2.Normalize(targetVel) * weight;
Vector2 newSteering = targetVel - host.Steering; Vector2 newSteering = targetVel - host.Steering;
if (newSteering == Vector2.Zero) return Vector2.Zero; if (newSteering == Vector2.Zero) return Vector2.Zero;
float steeringSpeed = (newSteering + host.Steering).Length(); float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > Math.Abs(speed)) if (steeringSpeed > Math.Abs(weight))
{ {
newSteering = Vector2.Normalize(newSteering) * Math.Abs(speed); newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
} }
return newSteering; return newSteering;
} }
protected virtual Vector2 DoSteeringWander(float speed) protected virtual Vector2 DoSteeringWander(float weight)
{ {
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(speed) : host.Steering; Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(weight) : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance; circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2( Vector2 displacement = new Vector2(
@@ -122,15 +119,15 @@ namespace Barotrauma
Vector2 newSteering = circleCenter + displacement; Vector2 newSteering = circleCenter + displacement;
float steeringSpeed = (newSteering + host.Steering).Length(); float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > speed) if (steeringSpeed > weight)
{ {
newSteering = Vector2.Normalize(newSteering) * speed; newSteering = Vector2.Normalize(newSteering) * weight;
} }
return newSteering; return newSteering;
} }
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float speed) protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float weight)
{ {
if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero; if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero;
@@ -187,7 +184,7 @@ namespace Barotrauma
if (dist > maxDistance) return Vector2.Zero; if (dist > maxDistance) return Vector2.Zero;
return -diff * (1.0f - dist / maxDistance) * speed; return -diff * (1.0f - dist / maxDistance) * weight;
} }
} }
@@ -130,6 +130,9 @@ namespace Barotrauma
public bool Crouching; public bool Crouching;
private float upperArmLength = 0.0f, forearmLength = 0.0f; private float upperArmLength = 0.0f, forearmLength = 0.0f;
public float ArmLength => upperArmLength + forearmLength;
public Vector2 RightHandIKPos public Vector2 RightHandIKPos
{ {
get; get;
@@ -1623,7 +1626,7 @@ namespace Barotrauma
Holdable holdable = item.GetComponent<Holdable>(); Holdable holdable = item.GetComponent<Holdable>();
if (Anim != Animation.Climbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero) if (!character.IsClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero)
{ {
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
@@ -1653,7 +1656,7 @@ namespace Barotrauma
} }
Vector2 transformedHoldPos = shoulder.WorldAnchorA; Vector2 transformedHoldPos = shoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || Anim == Animation.Climbing || usingController) if (itemPos == Vector2.Zero || character.IsClimbing || usingController)
{ {
if (character.SelectedItems[0] == item) if (character.SelectedItems[0] == item)
{ {
@@ -1742,7 +1745,7 @@ namespace Barotrauma
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir); item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir);
if (Anim == Animation.Climbing) return; if (character.IsClimbing) return;
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
@@ -988,6 +988,8 @@ namespace Barotrauma
public Vector2? OverrideMovement { get; set; } public Vector2? OverrideMovement { get; set; }
public bool ForceRun { get; set; } public bool ForceRun { get; set; }
public bool IsClimbing => AnimController.Anim == AnimController.Animation.Climbing;
public Vector2 GetTargetMovement() public Vector2 GetTargetMovement()
{ {
Vector2 targetMovement = Vector2.Zero; Vector2 targetMovement = Vector2.Zero;
@@ -1023,8 +1025,9 @@ namespace Barotrauma
!AnimController.IsMovingBackwards; !AnimController.IsMovingBackwards;
} }
targetMovement *= AnimController.GetCurrentSpeed(run); float currentSpeed = AnimController.GetCurrentSpeed(run);
float maxSpeed = GetCurrentMaxSpeed(run); targetMovement *= currentSpeed;
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed); targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed); targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
@@ -1084,31 +1087,23 @@ namespace Barotrauma
speedMultipliers.Clear(); speedMultipliers.Clear();
} }
/// <summary> public float ApplyTemporarySpeedLimits(float speed)
/// Applies temporary limits to the speed (damage).
/// </summary>
public float GetCurrentMaxSpeed(bool run)
{ {
float currMaxSpeed = AnimController.GetCurrentSpeed(run);
//?
//currMaxSpeed *= 1.5f;
var leftFoot = AnimController.GetLimb(LimbType.LeftFoot); var leftFoot = AnimController.GetLimb(LimbType.LeftFoot);
if (leftFoot != null) if (leftFoot != null)
{ {
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true); float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true);
currMaxSpeed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f)); speed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
} }
var rightFoot = AnimController.GetLimb(LimbType.RightFoot); var rightFoot = AnimController.GetLimb(LimbType.RightFoot);
if (rightFoot != null) if (rightFoot != null)
{ {
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true); float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true);
currMaxSpeed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f)); speed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
} }
return currMaxSpeed; return speed;
} }
public void Control(float deltaTime, Camera cam) public void Control(float deltaTime, Camera cam)
@@ -2055,7 +2050,7 @@ namespace Barotrauma
prevAiChatMessages.Add(dummyMsg); prevAiChatMessages.Add(dummyMsg);
} }
public void Speak(string message, ChatMessageType? messageType, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f) public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
{ {
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
@@ -76,8 +76,8 @@ namespace Barotrauma
} }
} }
const float InsufficientOxygenThreshold = 30.0f; public const float InsufficientOxygenThreshold = 30.0f;
const float LowOxygenThreshold = 50.0f; public const float LowOxygenThreshold = 50.0f;
protected float minVitality, maxVitality; protected float minVitality, maxVitality;
public bool Unkillable; public bool Unkillable;
@@ -441,28 +441,18 @@ namespace Barotrauma
public bool SectorHit(Vector2 armorSector, Vector2 simPosition) public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{ {
if (armorSector == Vector2.Zero) { return false; } if (armorSector == Vector2.Zero) { return false; }
float rotation = body.TransformedRotation;
// Can't get this to work properly. float offset = (MathHelper.PiOver2 - GetArmorSectorRotationOffset(armorSector)) * Dir;
//float rot = body.Rotation; float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition);
//if (Dir == -1) { rot -= MathHelper.Pi; } float sectorSize = GetArmorSectorSize(armorSector);
//Vector2 armorLimits = new Vector2(rot - armorSector.X * Dir, rot - armorSector.Y * Dir);
//float mid = (armorLimits.X + armorLimits.Y) / 2;
//float angleDiff = MathUtils.GetShortestAngle(MathUtils.VectorToAngle(simPosition - SimPosition), mid);
//return (Math.Abs(angleDiff) < (armorSector.Y - armorSector.X) / 2);
// Alternative implementation
float offset = GetArmorSectorRotationOffset(armorSector, body.Rotation);
Vector2 forward = Vector2.Transform(-Vector2.UnitY, Matrix.CreateRotationZ(offset));
float hitAngle = VectorExtensions.Angle(forward, simPosition - SimPosition);
float sectorSize = MathHelper.ToDegrees(GetArmorSectorSize(armorSector));
return hitAngle < sectorSize / 2; return hitAngle < sectorSize / 2;
} }
protected float GetArmorSectorRotationOffset(Vector2 armorSector, float bodyRotation) protected float GetArmorSectorRotationOffset(Vector2 armorSector)
{ {
float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y); float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y);
float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation); float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation);
return bodyRotation + (midAngle + spritesheetOrientation) * Dir; return midAngle + spritesheetOrientation;
} }
protected float GetArmorSectorSize(Vector2 armorSector) protected float GetArmorSectorSize(Vector2 armorSector)
@@ -216,10 +216,19 @@ namespace Barotrauma
commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory/cargo/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory/cargo/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".",
(string[] args) => (string[] args) =>
{ {
SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg); try
if (!string.IsNullOrWhiteSpace(errorMsg))
{ {
ThrowError(errorMsg); SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg);
if (!string.IsNullOrWhiteSpace(errorMsg))
{
ThrowError(errorMsg);
}
}
catch (Exception e)
{
string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\".";
ThrowError(errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
} }
}, },
() => () =>
@@ -6,13 +6,13 @@ namespace Barotrauma.Extensions
public static class VectorExtensions public static class VectorExtensions
{ {
/// <summary> /// <summary>
/// Unity's Angle implementation. /// Unity's Angle implementation without the conversion to degrees.
/// Returns the angle in degrees. /// Returns the angle in radians between two vectors.
/// 0 - 180. /// 0 - Pi.
/// </summary> /// </summary>
public static float Angle(this Vector2 from, Vector2 to) public static float Angle(this Vector2 from, Vector2 to)
{ {
return (float)Math.Acos(MathHelper.Clamp(Vector2.Dot(Vector2.Normalize(from), Vector2.Normalize(to)), -1f, 1f)) * 57.29578f; return (float)Math.Acos(MathHelper.Clamp(Vector2.Dot(Vector2.Normalize(from), Vector2.Normalize(to)), -1f, 1f));
} }
/// <summary> /// <summary>
@@ -20,7 +20,7 @@ namespace Barotrauma.Extensions
/// </summary> /// </summary>
public static Vector2 Forward(float radians, float length = 1) public static Vector2 Forward(float radians, float length = 1)
{ {
return new Vector2((float)Math.Sin(radians), (float)Math.Cos(radians)) * length; return new Vector2((float)Math.Cos(radians), (float)Math.Sin(radians)) * length;
} }
/// <summary> /// <summary>
@@ -31,6 +31,22 @@ namespace Barotrauma.Extensions
return -Forward(radians, length); return -Forward(radians, length);
} }
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 ForwardFlipped(float radians, float length = 1)
{
return new Vector2((float)Math.Sin(radians), (float)Math.Cos(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 BackwardFlipped(float radians, float length = 1)
{
return -ForwardFlipped(radians, length);
}
/// <summary> /// <summary>
/// Creates a normalized perpendicular vector to the right from a forward vector. /// Creates a normalized perpendicular vector to the right from a forward vector.
/// </summary> /// </summary>
@@ -71,6 +71,14 @@ namespace Barotrauma
public bool MuteOnFocusLost { get; set; } public bool MuteOnFocusLost { get; set; }
public int ParticleLimit { get; set; }
public float LightMapScale { get; set; }
public bool SpecularityEnabled { get; set; }
public bool ChromaticAberrationEnabled { get; set; }
public bool MuteOnFocusLost { get; set; }
public enum VoiceMode public enum VoiceMode
{ {
Disabled, Disabled,
@@ -399,6 +407,8 @@ namespace Barotrauma
AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f); AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f);
AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f);
keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length];
keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.W); keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.W);
keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S); keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S);
@@ -640,6 +650,34 @@ namespace Barotrauma
TextManager.LoadTextPacks(SelectedContentPackages); TextManager.LoadTextPacks(SelectedContentPackages);
//display error messages after all content packages have been loaded
//to make sure the package that contains text files has been loaded before we attempt to use TextManager
foreach (string missingPackagePath in missingPackagePaths)
{
DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", missingPackagePath));
}
foreach (ContentPackage incompatiblePackage in incompatiblePackages)
{
DebugConsole.ThrowError(TextManager.Get(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage")
.Replace("[packagename]", incompatiblePackage.Name)
.Replace("[packageversion]", incompatiblePackage.GameVersion.ToString())
.Replace("[gameversion]", GameMain.Version.ToString()));
}
foreach (ContentPackage contentPackage in SelectedContentPackages)
{
foreach (ContentFile file in contentPackage.Files)
{
if (!System.IO.File.Exists(file.Path))
{
DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found.");
continue;
}
ToolBox.IsProperFilenameCase(file.Path);
}
}
TextManager.LoadTextPacks(SelectedContentPackages);
//display error messages after all content packages have been loaded //display error messages after all content packages have been loaded
//to make sure the package that contains text files has been loaded before we attempt to use TextManager //to make sure the package that contains text files has been loaded before we attempt to use TextManager
foreach (string missingPackagePath in missingPackagePaths) foreach (string missingPackagePath in missingPackagePaths)
@@ -1046,369 +1084,6 @@ namespace Barotrauma
} }
#endregion #endregion
#region Save PlayerConfig
public void SaveNewPlayerConfig()
{
XDocument doc = new XDocument();
UnsavedSettings = false;
if (doc.Root == null)
{
doc.Add(new XElement("config"));
}
doc.Root.Add(
new XAttribute("language", TextManager.Language),
new XAttribute("masterserverurl", MasterServerUrl),
new XAttribute("autocheckupdates", AutoCheckUpdates),
new XAttribute("musicvolume", musicVolume),
new XAttribute("soundvolume", soundVolume),
new XAttribute("verboselogging", VerboseLogging),
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
new XAttribute("enablesplashscreen", EnableSplashScreen),
new XAttribute("usesteammatchmaking", useSteamMatchmaking),
new XAttribute("quickstartsub", QuickStartSubmarineName),
new XAttribute("requiresteamauthentication", requireSteamAuthentication),
new XAttribute("autoupdateworkshopitems", AutoUpdateWorkshopItems),
new XAttribute("aimassistamount", aimAssistAmount));
if (!ShowUserStatisticsPrompt)
{
doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics));
}
XElement gMode = doc.Root.Element("graphicsmode");
if (gMode == null)
{
gMode = new XElement("graphicsmode");
doc.Root.Add(gMode);
}
if (GraphicsWidth == 0 || GraphicsHeight == 0)
{
gMode.ReplaceAttributes(new XAttribute("displaymode", windowMode));
}
else
{
gMode.ReplaceAttributes(
new XAttribute("width", GraphicsWidth),
new XAttribute("height", GraphicsHeight),
new XAttribute("vsync", VSyncEnabled),
new XAttribute("displaymode", windowMode));
}
XElement gSettings = doc.Root.Element("graphicssettings");
if (gSettings == null)
{
gSettings = new XElement("graphicssettings");
doc.Root.Add(gSettings);
}
gSettings.ReplaceAttributes(
new XAttribute("particlelimit", ParticleLimit),
new XAttribute("lightmapscale", LightMapScale),
new XAttribute("specularity", SpecularityEnabled),
new XAttribute("chromaticaberration", ChromaticAberrationEnabled),
new XAttribute("losmode", LosMode),
new XAttribute("hudscale", HUDScale),
new XAttribute("inventoryscale", InventoryScale));
foreach (ContentPackage contentPackage in SelectedContentPackages)
{
if (contentPackage.Path.Contains(vanillaContentPackagePath))
{
doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path)));
break;
}
}
var keyMappingElement = new XElement("keymapping");
doc.Root.Add(keyMappingElement);
for (int i = 0; i < keyMapping.Length; i++)
{
if (keyMapping[i].MouseButton == null)
{
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key));
}
else
{
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton));
}
}
var gameplay = new XElement("gameplay");
var jobPreferences = new XElement("jobpreferences");
foreach (string jobName in JobPreferences)
{
jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName)));
}
gameplay.Add(jobPreferences);
doc.Root.Add(gameplay);
var playerElement = new XElement("player",
new XAttribute("name", defaultPlayerName ?? ""),
new XAttribute("headindex", CharacterHeadIndex),
new XAttribute("gender", CharacterGender),
new XAttribute("race", CharacterRace),
new XAttribute("hairindex", CharacterHairIndex),
new XAttribute("beardindex", CharacterBeardIndex),
new XAttribute("moustacheindex", CharacterMoustacheIndex),
new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex));
doc.Root.Add(playerElement);
XmlWriterSettings settings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineOnAttributes = true
};
try
{
using (var writer = XmlWriter.Create(savePath, settings))
{
doc.WriteTo(writer);
writer.Flush();
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace);
}
}
#endregion
#region Load PlayerConfig
// TODO: DRY
public void LoadPlayerConfig()
{
XDocument doc = XMLExtensions.LoadXml(playerSavePath);
if (doc == null || doc.Root == null)
{
ShowUserStatisticsPrompt = true;
SaveNewPlayerConfig();
return;
}
Language = doc.Root.GetAttributeString("language", Language);
AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates);
sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", true);
XElement graphicsMode = doc.Root.Element("graphicsmode");
GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth);
GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight);
VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled);
XElement graphicsSettings = doc.Root.Element("graphicssettings");
ParticleLimit = graphicsSettings.GetAttributeInt("particlelimit", ParticleLimit);
LightMapScale = MathHelper.Clamp(graphicsSettings.GetAttributeFloat("lightmapscale", LightMapScale), 0.1f, 1.0f);
SpecularityEnabled = graphicsSettings.GetAttributeBool("specularity", SpecularityEnabled);
ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", ChromaticAberrationEnabled);
HUDScale = graphicsSettings.GetAttributeFloat("hudscale", HUDScale);
InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", InventoryScale);
var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent");
if (!Enum.TryParse(losModeStr, out losMode))
{
losMode = LosMode.Transparent;
}
#if CLIENT
if (GraphicsWidth == 0 || GraphicsHeight == 0)
{
GraphicsWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
GraphicsHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
}
#endif
var windowModeStr = graphicsMode.GetAttributeString("displaymode", "Fullscreen");
if (!Enum.TryParse(windowModeStr, out windowMode))
{
windowMode = WindowMode.Fullscreen;
}
XElement audioSettings = doc.Root.Element("audio");
if (audioSettings != null)
{
SoundVolume = audioSettings.GetAttributeFloat("soundvolume", SoundVolume);
MusicVolume = audioSettings.GetAttributeFloat("musicvolume", MusicVolume);
VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume);
string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", "Disabled");
VoiceCaptureDevice = audioSettings.GetAttributeString("voicecapturedevice", "");
NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", -45);
var voiceSetting = VoiceMode.Disabled;
if (Enum.TryParse(voiceSettingStr, out voiceSetting))
{
VoiceSetting = voiceSetting;
}
}
useSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", useSteamMatchmaking);
requireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", requireSteamAuthentication);
EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen);
AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", AimAssistAmount);
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "keymapping":
foreach (XAttribute attribute in subElement.Attributes())
{
if (Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType))
{
if (int.TryParse(attribute.Value.ToString(), out int mouseButton))
{
keyMapping[(int)inputType] = new KeyOrMouse(mouseButton);
}
else
{
if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key))
{
keyMapping[(int)inputType] = new KeyOrMouse(key);
}
}
}
}
break;
case "gameplay":
jobPreferences = new List<string>();
foreach (XElement ele in subElement.Element("jobpreferences").Elements("job"))
{
string jobIdentifier = ele.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(jobIdentifier)) continue;
jobPreferences.Add(jobIdentifier);
}
break;
case "player":
defaultPlayerName = subElement.GetAttributeString("name", defaultPlayerName);
CharacterHeadIndex = subElement.GetAttributeInt("headindex", CharacterHeadIndex);
if (Enum.TryParse(subElement.GetAttributeString("gender", "none"), true, out Gender g))
{
CharacterGender = g;
}
if (Enum.TryParse(subElement.GetAttributeString("race", "white"), true, out Race r))
{
CharacterRace = r;
}
else
{
CharacterRace = Race.White;
}
CharacterHairIndex = subElement.GetAttributeInt("hairindex", CharacterHairIndex);
CharacterBeardIndex = subElement.GetAttributeInt("beardindex", CharacterBeardIndex);
CharacterMoustacheIndex = subElement.GetAttributeInt("moustacheindex", CharacterMoustacheIndex);
CharacterFaceAttachmentIndex = subElement.GetAttributeInt("faceattachmentindex", CharacterFaceAttachmentIndex);
break;
case "tutorials":
foreach (XElement tutorialElement in subElement.Elements())
{
CompletedTutorialNames.Add(tutorialElement.GetAttributeString("name", ""));
}
break;
}
}
foreach (InputType inputType in Enum.GetValues(typeof(InputType)))
{
if (keyMapping[(int)inputType] == null)
{
DebugConsole.ThrowError("Key binding for the input type \"" + inputType + " not set!");
keyMapping[(int)inputType] = new KeyOrMouse(Keys.D1);
}
}
UnsavedSettings = false;
selectedContentPackagePaths = new HashSet<string>();
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "contentpackage":
string path = System.IO.Path.GetFullPath(subElement.GetAttributeString("path", ""));
selectedContentPackagePaths.Add(path);
break;
}
}
LoadContentPackages(selectedContentPackagePaths);
}
public void ReloadContentPackages()
{
LoadContentPackages(selectedContentPackagePaths);
}
private void LoadContentPackages(IEnumerable<string> contentPackagePaths)
{
var missingPackagePaths = new List<string>();
var incompatiblePackages = new List<ContentPackage>();
SelectedContentPackages.Clear();
foreach (string path in contentPackagePaths)
{
var matchingContentPackage = ContentPackage.List.Find(cp => System.IO.Path.GetFullPath(cp.Path) == path);
if (matchingContentPackage == null)
{
missingPackagePaths.Add(path);
}
else if (!matchingContentPackage.IsCompatible())
{
incompatiblePackages.Add(matchingContentPackage);
}
else
{
SelectedContentPackages.Add(matchingContentPackage);
}
}
TextManager.LoadTextPacks(SelectedContentPackages);
foreach (ContentPackage contentPackage in SelectedContentPackages)
{
foreach (ContentFile file in contentPackage.Files)
{
if (!System.IO.File.Exists(file.Path))
{
DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found.");
continue;
}
ToolBox.IsProperFilenameCase(file.Path);
}
}
if (!SelectedContentPackages.Any())
{
var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage);
if (availablePackage != null)
{
SelectedContentPackages.Add(availablePackage);
}
}
//save to get rid of the invalid selected packages in the config file
if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0) { SaveNewPlayerConfig(); }
//display error messages after all content packages have been loaded
//to make sure the package that contains text files has been loaded before we attempt to use TextManager
foreach (string missingPackagePath in missingPackagePaths)
{
DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", missingPackagePath));
}
foreach (ContentPackage incompatiblePackage in incompatiblePackages)
{
DebugConsole.ThrowError(TextManager.Get(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage")
.Replace("[packagename]", incompatiblePackage.Name)
.Replace("[packageversion]", incompatiblePackage.GameVersion.ToString())
.Replace("[gameversion]", GameMain.Version.ToString()));
}
}
#endregion
#region Save PlayerConfig #region Save PlayerConfig
public void SaveNewPlayerConfig() public void SaveNewPlayerConfig()
{ {
@@ -240,7 +240,7 @@ namespace Barotrauma.Items.Components
if (isBroken) if (isBroken)
{ {
//the door has to be restored to 50% health before collision detection on the body is re-enabled //the door has to be restored to 50% health before collision detection on the body is re-enabled
if (item.Condition > item.Prefab.Health / 2.0f) if (item.ConditionPercentage > 50.0f)
{ {
IsBroken = false; IsBroken = false;
} }
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Extensions;
#if CLIENT #if CLIENT
using Barotrauma.Particles; using Barotrauma.Particles;
#endif #endif
@@ -261,14 +262,15 @@ namespace Barotrauma.Items.Components
Gap leak = objective.OperateTarget as Gap; Gap leak = objective.OperateTarget as Gap;
if (leak == null) return true; if (leak == null) return true;
float dist = Vector2.Distance(leak.WorldPosition, item.WorldPosition); Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
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 * 5.0f) return true;
Vector2 gapDiff = leak.WorldPosition - item.WorldPosition; // TODO: use the collider size?
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(gapDiff.X) < 100.0f && gapDiff.Y < 0.0f && gapDiff.Y > -150.0f) Math.Abs(fromItemToLeak.X) < 100.0f && fromItemToLeak.Y < 0.0f && fromItemToLeak.Y > -150.0f)
{ {
((HumanoidAnimController)character.AnimController).Crouching = true; ((HumanoidAnimController)character.AnimController).Crouching = true;
} }
@@ -276,16 +278,15 @@ namespace Barotrauma.Items.Components
//steer closer if almost in range //steer closer if almost in range
if (dist > Range) if (dist > Range)
{ {
Vector2 standPos = leak.IsHorizontal ? Vector2 standPos = leak.IsHorizontal ? new Vector2(Math.Sign(-fromItemToLeak.X), 0.0f) : new Vector2(0.0f, Math.Sign(-fromItemToLeak.Y) * 0.5f);
new Vector2(Math.Sign(-gapDiff.X), 0.0f)
: new Vector2(0.0f, Math.Sign(-gapDiff.Y) * 0.5f);
standPos = leak.WorldPosition + standPos * Range; standPos = leak.WorldPosition + standPos * Range;
// TODO: check if too close to the stand pos -> move away so that the tool can hit the target and not through it?
character.AIController.SteeringManager.SteeringManual(deltaTime, (standPos - character.WorldPosition) / 1000.0f); Vector2 dir = Vector2.Normalize(standPos - character.WorldPosition);
character.AIController.SteeringManager.SteeringManual(deltaTime, dir / 2);
} }
else else
{ {
// TODO: sometimes stuck here, if too close to the target
//close enough -> stop moving //close enough -> stop moving
character.AIController.SteeringManager.Reset(); character.AIController.SteeringManager.Reset();
} }
@@ -293,8 +294,13 @@ namespace Barotrauma.Items.Components
character.CursorPosition = leak.Position; character.CursorPosition = leak.Position;
character.SetInput(InputType.Aim, false, true); character.SetInput(InputType.Aim, false, true);
Use(deltaTime, character); if (VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak) < MathHelper.PiOver4)
{
// Press the trigger only when the tool is approximately facing the target.
Use(deltaTime, character);
}
// TODO: fix until the wall is fixed?
bool leakFixed = leak.Open <= 0.0f || leak.Removed; bool leakFixed = leak.Open <= 0.0f || leak.Removed;
if (leakFixed && leak.FlowTargetHull != null) if (leakFixed && leak.FlowTargetHull != null)
@@ -399,9 +399,9 @@ namespace Barotrauma.Items.Components
{ {
float transferAmount = 0.0f; float transferAmount = 0.0f;
if (this.Item.Condition <= item.Condition) if (this.Item.Condition <= item.Condition)
transferAmount = Math.Min(item.Condition, this.item.Prefab.Health - this.item.Condition); transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition);
else else
transferAmount = -Math.Min(this.item.Condition, item.Prefab.Health - item.Condition); transferAmount = -Math.Min(this.item.Condition, item.MaxCondition - item.Condition);
if (transferAmount == 0.0f) if (transferAmount == 0.0f)
return false; return false;
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption;
//pumps consume more power when in a bad condition //pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.Prefab.Health); currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
if (powerConsumption == 0.0f) voltage = 1.0f; if (powerConsumption == 0.0f) voltage = 1.0f;
@@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components
{ {
Vector2 currForce = new Vector2((force / 100.0f) * maxForce * Math.Min(voltage / minVoltage, 1.0f), 0.0f); Vector2 currForce = new Vector2((force / 100.0f) * maxForce * Math.Min(voltage / minVoltage, 1.0f), 0.0f);
//less effective when in a bad condition //less effective when in a bad condition
currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.Prefab.Health); currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition);
item.Submarine.ApplyForce(currForce); item.Submarine.ApplyForce(currForce);
@@ -237,7 +237,7 @@ namespace Barotrauma.Items.Components
outputContainer.Inventory.Locked = true; outputContainer.Inventory.Locked = true;
currPowerConsumption = powerConsumption; currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.Prefab.Health); currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
} }
private void CancelFabricating(Character user = null) private void CancelFabricating(Character user = null)
@@ -73,7 +73,7 @@ namespace Barotrauma.Items.Components
} }
currPowerConsumption = powerConsumption; currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.Prefab.Health); currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
hasPower = voltage > minVoltage; hasPower = voltage > minVoltage;
if (hasPower) if (hasPower)
@@ -42,7 +42,7 @@ namespace Barotrauma.Items.Components
CurrFlow = 0.0f; CurrFlow = 0.0f;
currPowerConsumption = powerConsumption; currPowerConsumption = powerConsumption;
//consume more power when in a bad condition //consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.Prefab.Health); currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
if (powerConsumption <= 0.0f) if (powerConsumption <= 0.0f)
{ {
@@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components
CurrFlow = Math.Min(voltage, 1.0f) * generatedAmount * 100.0f; CurrFlow = Math.Min(voltage, 1.0f) * generatedAmount * 100.0f;
//less effective when in bad condition //less effective when in bad condition
CurrFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.Prefab.Health); CurrFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
UpdateVents(CurrFlow); UpdateVents(CurrFlow);
@@ -66,7 +66,7 @@ namespace Barotrauma.Items.Components
currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f);
//pumps consume more power when in a bad condition //pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.Prefab.Health); currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
if (voltage < minVoltage) return; if (voltage < minVoltage) return;
@@ -82,7 +82,7 @@ namespace Barotrauma.Items.Components
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
//less effective when in a bad condition //less effective when in a bad condition
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.Prefab.Health); currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
item.CurrentHull.WaterVolume += currFlow; item.CurrentHull.WaterVolume += currFlow;
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; } if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
@@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components
{ {
item.SendSignal(0, "1", "meltdown_warning", null); item.SendSignal(0, "1", "meltdown_warning", null);
//faster meltdown if the item is in a bad condition //faster meltdown if the item is in a bad condition
meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.Prefab.Health); meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
if (meltDownTimer > MeltdownDelay) if (meltDownTimer > MeltdownDelay)
{ {
@@ -336,7 +336,7 @@ namespace Barotrauma.Items.Components
if (temperature > optimalTemperature.Y) if (temperature > optimalTemperature.Y)
{ {
float prevFireTimer = fireTimer; float prevFireTimer = fireTimer;
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.Prefab.Health); fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
if (fireTimer >= FireDelay && prevFireTimer < fireDelay) if (fireTimer >= FireDelay && prevFireTimer < fireDelay)
{ {
@@ -91,6 +91,8 @@ namespace Barotrauma.Items.Components
} }
} }
public float ChargePercentage => MathUtils.Percentage(Charge, Capacity);
[Serialize(10.0f, true), Editable(ToolTip = "How fast the device can be recharged. "+ [Serialize(10.0f, true), Editable(ToolTip = "How fast the device can be recharged. "+
"For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] "For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")]
public float MaxRechargeSpeed public float MaxRechargeSpeed
@@ -215,12 +217,6 @@ namespace Barotrauma.Items.Components
power: gridLoad <= 0.0f ? 1.0f : CurrPowerOutput / gridLoad); power: gridLoad <= 0.0f ? 1.0f : CurrPowerOutput / gridLoad);
} }
foreach (Pair<Powered, Connection> connected in directlyConnected)
{
connected.First.ReceiveSignal(0, "", connected.Second, source: item, sender: null,
power: gridLoad <= 0.0f ? 1.0f : CurrPowerOutput / gridLoad);
}
rechargeVoltage = 0.0f; rechargeVoltage = 0.0f;
outputVoltage = 0.0f; outputVoltage = 0.0f;
} }
@@ -176,7 +176,7 @@ namespace Barotrauma.Items.Components
#endif #endif
//items in a bad condition are more sensitive to overvoltage //items in a bad condition are more sensitive to overvoltage
float maxOverVoltage = MathHelper.Lerp(Math.Min(OverloadVoltage, 1.0f), OverloadVoltage, item.Condition / item.Prefab.Health); float maxOverVoltage = MathHelper.Lerp(Math.Min(OverloadVoltage, 1.0f), OverloadVoltage, item.Condition / item.MaxCondition);
//if the item can't be fixed, don't allow it to break //if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) continue; if (!item.Repairables.Any() || !CanBeOverloaded) continue;
@@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components
get { return currentFixer; } get { return currentFixer; }
set set
{ {
if (currentFixer == value || item.Condition >= item.Prefab.Health) return; if (currentFixer == value || item.IsFullCondition) return;
if (currentFixer != null) currentFixer.AnimController.Anim = AnimController.Animation.None; if (currentFixer != null) currentFixer.AnimController.Anim = AnimController.Animation.None;
currentFixer = value; currentFixer = value;
} }
@@ -138,7 +138,7 @@ namespace Barotrauma.Items.Components
return; return;
} }
if (CurrentFixer.SelectedConstruction != item || !currentFixer.CanInteractWith(item)) if (Item.IsFullCondition || CurrentFixer.SelectedConstruction != item || !currentFixer.CanInteractWith(item))
{ {
currentFixer.AnimController.Anim = AnimController.Animation.None; currentFixer.AnimController.Anim = AnimController.Animation.None;
currentFixer = null; currentFixer = null;
@@ -159,18 +159,18 @@ namespace Barotrauma.Items.Components
CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f);
} }
bool wasBroken = item.Condition < item.Prefab.Health; bool wasBroken = !item.IsFullCondition;
float fixDuration = MathHelper.Lerp(fixDurationLowSkill, fixDurationHighSkill, successFactor); float fixDuration = MathHelper.Lerp(fixDurationLowSkill, fixDurationHighSkill, successFactor);
if (fixDuration <= 0.0f) if (fixDuration <= 0.0f)
{ {
item.Condition = item.Prefab.Health; item.Condition = item.MaxCondition;
} }
else else
{ {
item.Condition += deltaTime / (fixDuration / item.Prefab.Health); item.Condition += deltaTime / (fixDuration / item.MaxCondition);
} }
if (wasBroken && item.Condition >= item.Prefab.Health) if (wasBroken && item.IsFullCondition)
{ {
SteamAchievementManager.OnItemRepaired(item, currentFixer); SteamAchievementManager.OnItemRepaired(item, currentFixer);
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
@@ -184,7 +184,7 @@ namespace Barotrauma.Items.Components
private void UpdateFixAnimation(Character character) private void UpdateFixAnimation(Character character)
{ {
character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / 100.0f) % 0.1f)); character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / item.MaxCondition) % 0.1f));
} }
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
@@ -60,8 +60,6 @@ namespace Barotrauma
public readonly XElement StaticBodyConfig; public readonly XElement StaticBodyConfig;
private Vector2 lastSentPos;
private bool needsPositionUpdate; private bool needsPositionUpdate;
private float lastSentCondition; private float lastSentCondition;
@@ -202,18 +200,11 @@ namespace Barotrauma
} }
} }
public bool NeedsPositionUpdate public float PositionUpdateInterval
{ {
get get;
{ private set;
if (body == null || !body.Enabled) return false; } = float.PositiveInfinity;
return needsPositionUpdate;
}
set
{
needsPositionUpdate = value;
}
}
protected Color spriteColor; protected Color spriteColor;
[Editable, Serialize("1.0,1.0,1.0,1.0", true)] [Editable, Serialize("1.0,1.0,1.0,1.0", true)]
@@ -252,6 +243,10 @@ namespace Barotrauma
get { return spriteColor; } get { return spriteColor; }
} }
public bool IsFullCondition => Condition >= MaxCondition;
public float MaxCondition => Prefab.Health;
public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition);
//the default value should be Prefab.Health, but because we can't use it in the attribute, //the default value should be Prefab.Health, but because we can't use it in the attribute,
//we'll just use NaN (which does nothing) and set the default value in the constructor/load //we'll just use NaN (which does nothing) and set the default value in the constructor/load
[Serialize(float.NaN, false), Editable] [Serialize(float.NaN, false), Editable]
@@ -1051,10 +1046,10 @@ namespace Barotrauma
return; return;
} }
} }
UpdateNetPosition();
} }
UpdateNetPosition(deltaTime);
inWater = IsInWater(); inWater = IsInWater();
bool waterProof = WaterProof; bool waterProof = WaterProof;
if (inWater) if (inWater)
@@ -1596,8 +1591,6 @@ namespace Barotrauma
parentInventory.RemoveItem(this); parentInventory.RemoveItem(this);
parentInventory = null; parentInventory = null;
} }
lastSentPos = SimPosition;
} }
public void Equip(Character character) public void Equip(Character character)
@@ -1816,7 +1809,7 @@ namespace Barotrauma
} }
} }
partial void UpdateNetPosition(); partial void UpdateNetPosition(float deltaTime);
public static Item Load(XElement element, Submarine submarine) public static Item Load(XElement element, Submarine submarine)
{ {
@@ -160,6 +160,8 @@ namespace Barotrauma
} }
} }
public float WaterPercentage => MathUtils.Percentage(WaterVolume, Volume);
public float OxygenPercentage public float OxygenPercentage
{ {
get { return oxygen / Volume * 100.0f; } get { return oxygen / Volume * 100.0f; }
@@ -31,6 +31,12 @@ namespace Barotrauma
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>(); public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public bool UseInMainMenu
{
get;
private set;
}
public List<string> NameFormats public List<string> NameFormats
{ {
get { return nameFormats; } get { return nameFormats; }
@@ -57,6 +63,7 @@ namespace Barotrauma
Identifier = element.GetAttributeString("identifier", element.Name.ToString()); Identifier = element.GetAttributeString("identifier", element.Name.ToString());
Name = TextManager.Get("LocationName." + Identifier); Name = TextManager.Get("LocationName." + Identifier);
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt"); string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
try try
@@ -430,6 +430,13 @@ namespace Barotrauma
} }
CurrentLocation.SelectedMissionIndex = missionIndex; CurrentLocation.SelectedMissionIndex = missionIndex;
//the destination must be the same as the destination of the mission
if (CurrentLocation.SelectedMission != null &&
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
{
SelectLocation(CurrentLocation.SelectedMission.Locations[1]);
}
SelectedLocation = location; SelectedLocation = location;
SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
@@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Xml.Linq; using System.Xml.Linq;
using Voronoi2; using Voronoi2;
@@ -401,6 +400,7 @@ namespace Barotrauma
IsOutpost = true; IsOutpost = true;
ShowSonarMarker = false; ShowSonarMarker = false;
PhysicsBody.FarseerBody.IsStatic = true; PhysicsBody.FarseerBody.IsStatic = true;
TeamID = Character.TeamType.FriendlyNPC;
foreach (MapEntity me in MapEntity.mapEntityList) foreach (MapEntity me in MapEntity.mapEntityList)
{ {
@@ -458,11 +458,15 @@ namespace Barotrauma
} }
/// <summary> /// <summary>
/// Returns a list of all submarines that are connected to this one via docking ports. /// Don't use this directly, because the list is updated only when GetConnectedSubs() is called. The method is called so frequently that we don't want to create new list here.
/// </summary>
private readonly List<Submarine> connectedSubs = new List<Submarine>(2);
/// <summary>
/// Returns a list of all submarines that are connected to this one via docking ports, including this sub.
/// </summary> /// </summary>
public List<Submarine> GetConnectedSubs() public List<Submarine> GetConnectedSubs()
{ {
List<Submarine> connectedSubs = new List<Submarine> { this }; connectedSubs.Clear();
GetConnectedSubsRecursive(connectedSubs); GetConnectedSubsRecursive(connectedSubs);
return connectedSubs; return connectedSubs;
@@ -518,45 +522,6 @@ namespace Barotrauma
{ {
maxX = Math.Min(maxX, ruin.Area.X - 100.0f); maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
} }
else
{
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
}
else
{
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
}
else
{
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
}
if (entity.IsVisible(worldView)) { visibleEntities.Add(entity); }
}
if (minX < 0.0f && maxX > Level.Loaded.Size.X)
{
//no walls found at either side, just use the initial spawnpos and hope for the best
}
else if (minX < 0)
{
//no wall found at the left side, spawn to the left from the right-side wall
spawnPos.X = maxX - minWidth - 100.0f;
}
else if (maxX > Level.Loaded.Size.X)
{
//no wall found at right side, spawn to the right from the left-side wall
spawnPos.X = minX + minWidth + 100.0f;
}
else
{
//walls found at both sides, use their midpoint
spawnPos.X = (minX + maxX) / 2;
}
if (minX < 0.0f && maxX > Level.Loaded.Size.X)
{
//no walls found at either side, just use the initial spawnpos and hope for the best
} }
if (minX < 0.0f && maxX > Level.Loaded.Size.X) if (minX < 0.0f && maxX > Level.Loaded.Size.X)
@@ -1026,6 +991,26 @@ namespace Barotrauma
return closest; return closest;
} }
public List<Hull> GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList);
public List<Gap> GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList);
public List<Item> GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList);
public List<T> GetEntities<T>(bool includingConnectedSubs, List<T> list) where T : MapEntity
{
return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs));
}
public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs)
{
if (entity.Submarine == this) { return true; }
if (entity.Submarine == null) { return false; }
if (includingConnectedSubs)
{
return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID);
}
return false;
}
/// <summary> /// <summary>
/// Finds the sub whose borders contain the position /// Finds the sub whose borders contain the position
/// </summary> /// </summary>
@@ -15,7 +15,15 @@ namespace Barotrauma.Networking
public LogMessage(string text, MessageType type) public LogMessage(string text, MessageType type)
{ {
Text = "[" + DateTime.Now.ToString() + "] " + TextManager.GetServerMessage(text); if (type.HasFlag(MessageType.Chat))
{
Text = $"[{DateTime.Now.ToString()}] {text}";
}
else
{
Text = $"[{DateTime.Now.ToString()}] {TextManager.GetServerMessage(text)}";
}
Type = type; Type = type;
} }
} }
@@ -242,6 +242,11 @@ namespace Barotrauma
get { return body.Rotation; } get { return body.Rotation; }
} }
/// <summary>
/// Takes flipping (Dir) into account.
/// </summary>
public float TransformedRotation => Dir < 0 ? Rotation - MathHelper.Pi : Rotation;
public Vector2 LinearVelocity public Vector2 LinearVelocity
{ {
get { return body.LinearVelocity; } get { return body.LinearVelocity; }
@@ -160,6 +160,16 @@ namespace Barotrauma
} }
#endif #endif
public void SetState()
{
hit = binding.IsHit();
if (hit) hitQueue = true;
held = binding.IsDown();
if (held) heldQueue = true;
}
#endif
public bool Hit public bool Hit
{ {
get get
@@ -141,8 +141,6 @@ namespace Barotrauma
// Format: ServerMessage.Identifier1/ServerMessage.Indentifier2~[variable1]=value~[variable2]=value // Format: ServerMessage.Identifier1/ServerMessage.Indentifier2~[variable1]=value~[variable2]=value
public static string GetServerMessage(string serverMessage) public static string GetServerMessage(string serverMessage)
{ {
if (serverMessage.Contains(" ")) return serverMessage; // Spaces found, do not translate
if (!textPacks.ContainsKey(Language)) if (!textPacks.ContainsKey(Language))
{ {
DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English...");
@@ -159,6 +157,8 @@ namespace Barotrauma
{ {
if (!IsServerMessageWithVariables(messages[i])) // No variables, try to translate if (!IsServerMessageWithVariables(messages[i])) // No variables, try to translate
{ {
if (messages[i].Contains(" ")) continue; // Spaces found, do not translate
string msg = Get(messages[i], true); string msg = Get(messages[i], true);
if (msg != null) // If a translation was found, otherwise use the original if (msg != null) // If a translation was found, otherwise use the original
@@ -17,6 +17,11 @@ namespace Barotrauma
static class MathUtils static class MathUtils
{ {
public static float Percentage(float portion, float total)
{
return portion / total * 100;
}
public static int PositiveModulo(int i, int n) public static int PositiveModulo(int i, int n)
{ {
return (i % n + n) % n; return (i % n + n) % n;