Unstable 0.1500.2.0 (Rokvach's dog edition)

This commit is contained in:
Markus Isberg
2021-09-10 04:52:34 +09:00
parent e7b7c1a748
commit 1231170fce
126 changed files with 2424 additions and 1083 deletions
@@ -48,8 +48,13 @@ namespace Barotrauma
{ {
color = Color.CornflowerBlue; color = Color.CornflowerBlue;
} }
else if (Entity is Item) else if (Entity is Item i)
{ {
if (i.Submarine != null && i.GetComponent<Items.Components.Door>() == null)
{
// Don't show items that are inside the submarine, because monsters shouldn't target them when they are inside and the monsters are outside.
return;
}
color = Color.CadetBlue; color = Color.CadetBlue;
} }
else else
@@ -1,12 +1,12 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Networking; using Barotrauma.Networking;
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq; using System.Xml.Linq;
using System.IO; using Barotrauma.IO;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
namespace Barotrauma namespace Barotrauma
@@ -624,7 +624,7 @@ namespace Barotrauma
ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath); ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath);
} }
ch.ExperiencePoints = inc.ReadUInt16(); ch.ExperiencePoints = inc.ReadUInt16();
ch.AdditionalTalentPoints = inc.ReadUInt16();
return ch; return ch;
} }
} }
@@ -422,6 +422,8 @@ namespace Barotrauma
} }
} }
} }
healthWindowVerticalLayout.Recalculate();
} }
private void OnAttacked(Character attacker, AttackResult attackResult) private void OnAttacked(Character attacker, AttackResult attackResult)
@@ -950,6 +952,16 @@ namespace Barotrauma
UpdateAlignment(); UpdateAlignment();
} }
foreach (Affliction affliction in afflictions)
{
if (affliction.Prefab.AfflictionOverlay != null)
{
Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay;
ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * (affliction.GetAfflictionOverlayMultiplier()), Vector2.Zero, 0.0f,
new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y));
}
}
float damageOverlayAlpha = DamageOverlayTimer; float damageOverlayAlpha = DamageOverlayTimer;
if (Vitality < MaxVitality * 0.1f) if (Vitality < MaxVitality * 0.1f)
{ {
@@ -6,7 +6,7 @@ namespace Barotrauma
{ {
public class GUIFrame : GUIComponent public class GUIFrame : GUIComponent
{ {
public int OutlineThickness { get; set; } public float OutlineThickness { get; set; }
public GUIFrame(RectTransform rectT, string style = "", Color? color = null) : base(style, rectT) public GUIFrame(RectTransform rectT, string style = "", Color? color = null) : base(style, rectT)
{ {
@@ -125,7 +125,7 @@ namespace Barotrauma
public TabMenu() public TabMenu()
{ {
if (!initialized) Initialize(); if (!initialized) { Initialize(); }
CreateInfoFrame(selectedTab); CreateInfoFrame(selectedTab);
SelectInfoFrameTab(null, selectedTab); SelectInfoFrameTab(null, selectedTab);
@@ -260,6 +260,10 @@ namespace Barotrauma
} }
var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.talents"); var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.talents");
talentsButton.OnAddedToGUIUpdateList += (GUIComponent component) =>
{
talentsButton.Enabled = Character.Controlled?.Info != null && GameMain.GameSession?.Campaign != null;
};
} }
private bool SelectInfoFrameTab(GUIButton button, object userData) private bool SelectInfoFrameTab(GUIButton button, object userData)
@@ -1177,8 +1181,6 @@ namespace Barotrauma
private GUITextBlock talentPointsText; private GUITextBlock talentPointsText;
private GUITextBlock experienceText; private GUITextBlock experienceText;
private Color experienceBackgroundColor = new Color(255, 255, 255, 155);
private GUIProgressBar experienceBar; private GUIProgressBar experienceBar;
private void CreateTalentInfo(GUIFrame infoFrame) private void CreateTalentInfo(GUIFrame infoFrame)
@@ -1186,12 +1188,13 @@ namespace Barotrauma
infoFrame.ClearChildren(); infoFrame.ClearChildren();
talentButtons.Clear(); talentButtons.Clear();
Character controlledCharacter = Character.Controlled;
if (controlledCharacter == null) { return; }
GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = GUI.IntScale(15); int padding = GUI.IntScale(15);
GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null);
Character controlledCharacter = Character.Controlled;
if (controlledCharacter.Info == null) if (controlledCharacter.Info == null)
{ {
DebugConsole.ThrowError("No character info found for talent UI"); DebugConsole.ThrowError("No character info found for talent UI");
@@ -51,7 +51,7 @@ namespace Barotrauma
{ {
keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length];
keyMapping[(int)InputType.Run] = new KeyOrMouse(Keys.LeftShift); keyMapping[(int)InputType.Run] = new KeyOrMouse(Keys.LeftShift);
keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.R); keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F);
keyMapping[(int)InputType.Crouch] = new KeyOrMouse(Keys.LeftControl); keyMapping[(int)InputType.Crouch] = new KeyOrMouse(Keys.LeftControl);
keyMapping[(int)InputType.Grab] = new KeyOrMouse(Keys.G); keyMapping[(int)InputType.Grab] = new KeyOrMouse(Keys.G);
keyMapping[(int)InputType.Health] = new KeyOrMouse(Keys.H); keyMapping[(int)InputType.Health] = new KeyOrMouse(Keys.H);
@@ -688,6 +688,11 @@ namespace Barotrauma
{ {
break; break;
} }
//if putting an item to a container with a max stack size of 1, only put one item from the stack
if (quickUseAction == QuickUseAction.PutToContainer && (character.SelectedConstruction?.GetComponent<ItemContainer>()?.MaxStackSize ?? 0) <= 1)
{
break;
}
} }
} }
@@ -31,11 +31,23 @@ namespace Barotrauma.Items.Components
name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name); name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name);
} }
if (TextManager.ContainsTag("entitydescription."+Item.prefab.Identifier)) if (TextManager.ContainsTag("entitydescription." + Item.prefab.Identifier))
{ {
int value = (int)MathHelper.Lerp(TooltipValueMin, TooltipValueMax, item.ConditionPercentage / 100.0f); int value = (int)MathHelper.Lerp(TooltipValueMin, TooltipValueMax, item.ConditionPercentage / 100.0f);
description = TextManager.GetWithVariable("entitydescription." + Item.prefab.Identifier, "[value]", value.ToString()); description = TextManager.GetWithVariable("entitydescription." + Item.prefab.Identifier, "[value]", value.ToString());
} }
foreach (Item containedItem in item.ContainedItems)
{
var containedGeneticMaterial = containedItem.GetComponent<GeneticMaterial>();
if (containedGeneticMaterial == null) { continue; }
string _ = string.Empty;
string containedDescription = containedItem.Description;
containedGeneticMaterial.AddTooltipInfo(ref _, ref containedDescription);
if (!string.IsNullOrEmpty(containedDescription))
{
description += '\n' + containedDescription;
}
}
} }
public void ModifyDeconstructInfo(Deconstructor deconstructor, ref string buttonText, ref string infoText) public void ModifyDeconstructInfo(Deconstructor deconstructor, ref string buttonText, ref string infoText)
@@ -19,6 +19,8 @@ namespace Barotrauma.Items.Components
/// </summary> /// </summary>
private float[] containedSpriteDepths; private float[] containedSpriteDepths;
private Sprite[] slotIcons;
public Sprite InventoryTopSprite public Sprite InventoryTopSprite
{ {
get { return inventoryTopSprite; } get { return inventoryTopSprite; }
@@ -88,6 +90,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element) partial void InitProjSpecific(XElement element)
{ {
slotIcons = new Sprite[capacity];
foreach (XElement subElement in element.Elements()) foreach (XElement subElement in element.Elements())
{ {
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
@@ -107,6 +110,17 @@ namespace Barotrauma.Items.Components
case "containedstateindicatorempty": case "containedstateindicatorempty":
ContainedStateIndicatorEmpty = new Sprite(subElement); ContainedStateIndicatorEmpty = new Sprite(subElement);
break; break;
case "sloticon":
int index = subElement.GetAttributeInt("slotindex", -1);
Sprite icon = new Sprite(subElement);
for (int i = 0; i < capacity; i++)
{
if (i == index || index == -1)
{
slotIcons[i] = icon;
}
}
break;
} }
} }
@@ -208,6 +222,12 @@ namespace Barotrauma.Items.Components
} }
} }
public Sprite GetSlotIcon(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= slotIcons.Length) { return null; }
return slotIcons[slotIndex];
}
public bool KeepOpenWhenEquippedBy(Character character) public bool KeepOpenWhenEquippedBy(Character character)
{ {
if (!character.CanAccessInventory(Inventory) || if (!character.CanAccessInventory(Inventory) ||
@@ -24,6 +24,9 @@ namespace Barotrauma.Items.Components
[Serialize("DeconstructorDeconstruct", true)] [Serialize("DeconstructorDeconstruct", true)]
public string ActivateButtonText { get; set; } public string ActivateButtonText { get; set; }
[Serialize("", true)]
public string InfoText { get; set; }
[Serialize(0.0f, true)] [Serialize(0.0f, true)]
public float InfoAreaWidth { get; set; } public float InfoAreaWidth { get; set; }
@@ -102,8 +105,8 @@ namespace Barotrauma.Items.Components
var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f };
// === OUTPUT SLOTS === // // === OUTPUT SLOTS === //
outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f - InfoAreaWidth, 1f), outputArea.RectTransform, Anchor.CenterLeft), style: null); outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f - InfoAreaWidth, 1f), outputArea.RectTransform, Anchor.CenterLeft), style: null);
if (InfoAreaWidth >= 0.0f) if (InfoAreaWidth >= 0.0f)
{ {
@@ -114,10 +117,18 @@ namespace Barotrauma.Items.Components
ActivateButton.OnAddedToGUIUpdateList += (GUIComponent component) => ActivateButton.OnAddedToGUIUpdateList += (GUIComponent component) =>
{ {
activateButton.Enabled = true; activateButton.Enabled = true;
infoArea.Text = string.Empty; if (string.IsNullOrEmpty(InfoText))
{
infoArea.Text = string.Empty;
}
else
{
infoArea.Text = TextManager.Get(InfoText, returnNull: true) ?? InfoText;
}
if (IsActive) if (IsActive)
{ {
activateButton.Text = TextManager.Get("DeconstructorCancel"); activateButton.Text = TextManager.Get("DeconstructorCancel");
infoArea.Text = string.Empty;
return; return;
} }
bool outputsFound = false; bool outputsFound = false;
@@ -12,24 +12,24 @@ namespace Barotrauma.Items.Components
{ {
internal readonly struct MiniMapGUIComponent internal readonly struct MiniMapGUIComponent
{ {
public readonly GUIComponent Component; public readonly GUIComponent RectComponent;
public readonly GUIComponent BorderComponent; public readonly GUIComponent BorderComponent;
public MiniMapGUIComponent(GUIComponent component) public MiniMapGUIComponent(GUIComponent rectComponent)
{ {
Component = component; RectComponent = rectComponent;
BorderComponent = component; BorderComponent = rectComponent;
} }
public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent) public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent)
{ {
Component = frame; RectComponent = frame;
BorderComponent = linkedHullComponent; BorderComponent = linkedHullComponent;
} }
public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent) public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent)
{ {
component = Component; component = RectComponent;
borderComponent = BorderComponent; borderComponent = BorderComponent;
} }
} }
@@ -183,6 +183,7 @@ namespace Barotrauma.Items.Components
private ImmutableDictionary<MapEntity, MiniMapGUIComponent> hullStatusComponents; private ImmutableDictionary<MapEntity, MiniMapGUIComponent> hullStatusComponents;
private ImmutableDictionary<MapEntity, MiniMapGUIComponent> electricalMapComponents; private ImmutableDictionary<MapEntity, MiniMapGUIComponent> electricalMapComponents;
private ImmutableDictionary<MiniMapGUIComponent, GUIComponent> electricalChildren; private ImmutableDictionary<MiniMapGUIComponent, GUIComponent> electricalChildren;
private ImmutableDictionary<MiniMapGUIComponent, GUIComponent> doorChildren;
private ImmutableHashSet<ItemPrefab> itemsFoundOnSub; private ImmutableHashSet<ItemPrefab> itemsFoundOnSub;
@@ -190,7 +191,7 @@ namespace Barotrauma.Items.Components
private float blipState; private float blipState;
private const float maxBlipState = 1f; private const float maxBlipState = 1f;
private const float maxZoom = 2f, private const float maxZoom = 10f,
minZoom = 0.5f, minZoom = 0.5f,
defaultZoom = 1f; defaultZoom = 1f;
@@ -209,13 +210,15 @@ namespace Barotrauma.Items.Components
private bool recalculate; private bool recalculate;
public static readonly Color MiniMapBaseColor = Color.DarkCyan; public static readonly Color MiniMapBaseColor = new Color(15, 178, 107);
private static readonly Color WetHullColor = new Color(9, 80, 159), private static readonly Color WetHullColor = new Color(11, 122, 205),
DoorIndicatorColor = GUI.Style.Green,
NoPowerDoorColor = DoorIndicatorColor * 0.1f,
DefaultNeutralColor = MiniMapBaseColor * 0.8f, DefaultNeutralColor = MiniMapBaseColor * 0.8f,
HoverColor = Color.White, HoverColor = Color.White,
BlueprintBlue = new Color(48, 87, 255), BlueprintBlue = new Color(23, 38, 33),
HullWaterColor = new Color(85, 136, 147), HullWaterColor = new Color(17, 173, 179),
HullWaterLineColor = Color.LightBlue, HullWaterLineColor = Color.LightBlue,
NoPowerColor = MiniMapBaseColor * 0.1f, NoPowerColor = MiniMapBaseColor * 0.1f,
ElectricalBaseColor = GUI.Style.Orange, ElectricalBaseColor = GUI.Style.Orange,
@@ -323,7 +326,7 @@ namespace Barotrauma.Items.Components
CanBeFocused = false CanBeFocused = false
}; };
SetTooltipPosition(searchAutoComplete, searchBar); SetAutoCompletePosition(searchAutoComplete, searchBar);
GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform)) GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform))
{ {
@@ -403,16 +406,17 @@ namespace Barotrauma.Items.Components
scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center));
miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false };
miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, null, out hullStatusComponents); ImmutableHashSet<Item> hullPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && it.GetComponent<Door>() != null).ToImmutableHashSet();
miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents);
IEnumerable<Item> pointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null); IEnumerable<Item> electrialPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null);
electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), pointsOfInterest, out electricalMapComponents); electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electrialPointsOfInterest, out electricalMapComponents);
Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>(); Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
foreach (var (entity, component) in electricalMapComponents) foreach (var (entity, component) in electricalMapComponents)
{ {
GUIComponent parent = component.Component; GUIComponent parent = component.RectComponent;
if (!(entity is Item it )) { continue; } if (!(entity is Item it )) { continue; }
Sprite? sprite = it.Prefab.UpgradePreviewSprite; Sprite? sprite = it.Prefab.UpgradePreviewSprite;
if (sprite is null) { continue; } if (sprite is null) { continue; }
@@ -430,6 +434,31 @@ namespace Barotrauma.Items.Components
electricalChildren = electricChildren.ToImmutableDictionary(); electricalChildren = electricChildren.ToImmutableDictionary();
Dictionary<MiniMapGUIComponent, GUIComponent> doorChilds = new Dictionary<MiniMapGUIComponent, GUIComponent>();
foreach (var (entity, component) in hullStatusComponents)
{
if (!hullPointsOfInterest.Contains(entity)) { continue; }
const int minSize = 8;
const int borderMaxSize = 2;
Point size = component.BorderComponent.Rect.Size;
size.X = Math.Max(size.X, minSize);
size.Y = Math.Max(size.Y, minSize);
float width = Math.Min(borderMaxSize, Math.Min(size.X, size.Y) / 8f);
GUIFrame frame = new GUIFrame(new RectTransform(size, component.RectComponent.RectTransform, anchor: Anchor.Center), style: "ScanLines", color: DoorIndicatorColor)
{
OutlineColor = GUI.Style.Green,
OutlineThickness = width
};
doorChilds.Add(component, frame);
}
doorChildren = doorChilds.ToImmutableDictionary();
Rectangle parentRect = miniMapFrame.Rect; Rectangle parentRect = miniMapFrame.Rect;
displayedSubs.Clear(); displayedSubs.Clear();
@@ -466,7 +495,6 @@ namespace Barotrauma.Items.Components
} }
} }
if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn)))
{ {
float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom);
@@ -501,10 +529,10 @@ namespace Barotrauma.Items.Components
miniMapContainer.RectTransform.AbsoluteOffset = mapOffset.ToPoint(); miniMapContainer.RectTransform.AbsoluteOffset = mapOffset.ToPoint();
recalculate = false; recalculate = false;
var (maxWidth, maxHeight) = miniMapContainer.Rect.Size.ToVector2() / 2f / Zoom; // var (maxWidth, maxHeight) = miniMapContainer.Rect.Size.ToVector2() / 2f;
//
mapOffset.X = Math.Clamp(mapOffset.X, -maxWidth, maxWidth); // mapOffset.X = Math.Clamp(mapOffset.X, -maxWidth, maxWidth);
mapOffset.Y = Math.Clamp(mapOffset.Y, -maxHeight, maxHeight); // mapOffset.Y = Math.Clamp(mapOffset.Y, -maxHeight, maxHeight);
} }
// is there a better way to do this? // is there a better way to do this?
@@ -610,7 +638,7 @@ namespace Barotrauma.Items.Components
{ {
if (!(entity is Hull hull)) { continue; } if (!(entity is Hull hull)) { continue; }
if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; }
DrawHullCards(spriteBatch, hull, hullData, component.Component); DrawHullCards(spriteBatch, hull, hullData, component.RectComponent);
} }
spriteBatch.End(); spriteBatch.End();
@@ -645,7 +673,7 @@ namespace Barotrauma.Items.Components
MiniMapBlips = null; MiniMapBlips = null;
searchedPrefab = null; searchedPrefab = null;
searchAutoComplete.Visible = true; searchAutoComplete.Visible = true;
SetTooltipPosition(searchAutoComplete, box); SetAutoCompletePosition(searchAutoComplete, box);
GUIListBox listBox = searchAutoComplete.GetChild<GUIListBox>(); GUIListBox listBox = searchAutoComplete.GetChild<GUIListBox>();
if (listBox is null) { return false; } if (listBox is null) { return false; }
@@ -677,7 +705,7 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
private void SetTooltipPosition(GUIComponent tooltip, GUITextBox box) private void SetAutoCompletePosition(GUIComponent tooltip, GUITextBox box)
{ {
int height = GuiFrame.Rect.Height / 2; int height = GuiFrame.Rect.Height / 2;
tooltip.RectTransform.NonScaledSize = new Point(box.Rect.Width, height); tooltip.RectTransform.NonScaledSize = new Point(box.Rect.Width, height);
@@ -707,7 +735,6 @@ namespace Barotrauma.Items.Components
{ {
if (searchedPrefab is null) if (searchedPrefab is null)
{ {
Console.WriteLine("Bruh");
ItemPrefab? first = ItemPrefab.Prefabs.FirstOrDefault(p => p.Name.ToLower().Equals(text.ToLower())); ItemPrefab? first = ItemPrefab.Prefabs.FirstOrDefault(p => p.Name.ToLower().Equals(text.ToLower()));
if (first is null) if (first is null)
@@ -790,12 +817,39 @@ namespace Barotrauma.Items.Components
private void UpdateHullStatus() private void UpdateHullStatus()
{ {
bool canHoverOverHull = true;
foreach (var (entity, component) in hullStatusComponents)
{
// we are only interested in non-hull components
if (entity is Hull) { continue; }
GUIComponent rectComponent = component.RectComponent;
if (doorChildren.TryGetValue(component, out GUIComponent? child) && child != null)
{
if (item.Submarine == null || !hasPower)
{
child.Color = child.OutlineColor = NoPowerDoorColor;
}
if (Voltage < MinVoltage) { continue; }
child.Color = child.OutlineColor = DoorIndicatorColor;
if (GUI.MouseOn == child)
{
SetTooltip(rectComponent.Rect.Center, entity.Name, string.Empty, string.Empty, string.Empty);
canHoverOverHull = false;
child.Color = child.OutlineColor = HoverColor;
}
}
}
foreach (var (entity, (component, borderComponent)) in hullStatusComponents) foreach (var (entity, (component, borderComponent)) in hullStatusComponents)
{ {
if (item.Submarine == null || !hasPower) if (item.Submarine == null || !hasPower)
{ {
component.Color = NoPowerColor; component.Color = borderComponent.OutlineColor = NoPowerColor;
borderComponent.OutlineColor = NoPowerColor;
} }
if (Voltage < MinVoltage) { continue; } if (Voltage < MinVoltage) { continue; }
@@ -848,7 +902,7 @@ namespace Barotrauma.Items.Components
borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f)); borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f));
} }
bool isHoveringOver = GUI.MouseOn == component; bool isHoveringOver = canHoverOverHull && GUI.MouseOn == component;
// When drawing tooltip we are only interested in the component we are hovering over // When drawing tooltip we are only interested in the component we are hovering over
if (isHoveringOver) if (isHoveringOver)
@@ -888,7 +942,7 @@ namespace Barotrauma.Items.Components
{ {
if (!hullStatusComponents.ContainsKey(linkedHull)) { continue; } if (!hullStatusComponents.ContainsKey(linkedHull)) { continue; }
isHoveringOver |= hullStatusComponents[linkedHull].Component == GUI.MouseOn; isHoveringOver |= canHoverOverHull && hullStatusComponents[linkedHull].RectComponent == GUI.MouseOn;
if (isHoveringOver) { break; } if (isHoveringOver) { break; }
} }
@@ -919,7 +973,7 @@ namespace Barotrauma.Items.Components
component.Color = component.OutlineColor = NoPowerElectricalColor; component.Color = component.OutlineColor = NoPowerElectricalColor;
} }
if (Voltage < MinVoltage || !miniMapGuiComponent.Component.Visible) { continue; } if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; }
int durability = (int)(it.Condition / it.MaxCondition * 100f); int durability = (int)(it.Condition / it.MaxCondition * 100f);
Color color = ToolBox.GradientLerp(durability / 100f, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green, GUI.Style.Green); Color color = ToolBox.GradientLerp(durability / 100f, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green, GUI.Style.Green);
@@ -956,9 +1010,6 @@ namespace Barotrauma.Items.Components
{ {
if (item.Submarine != null) if (item.Submarine != null)
{ {
Rectangle parentRect = container.Rect;
if (miniMapFrame is { } miniMap) { parentRect = miniMap.Rect; }
DrawSubmarine(spriteBatch); DrawSubmarine(spriteBatch);
} }
@@ -995,7 +1046,7 @@ namespace Barotrauma.Items.Components
if (hullData.Distort) { continue; } if (hullData.Distort) { continue; }
GUIComponent hullFrame = component.Component; GUIComponent hullFrame = component.RectComponent;
if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) if (hullsVisible && hullData.HullWaterAmount is { } waterAmount)
{ {
@@ -1197,10 +1248,12 @@ namespace Barotrauma.Items.Components
const float padding = 8f; const float padding = 8f;
float totalWidth = 0f; float totalWidth = 0f;
float parentWidth = submarineContainer.Rect.Width / 24f;
int i = 0; int i = 0;
foreach (MiniMapSprite info in cardsToDraw) foreach (MiniMapSprite info in cardsToDraw)
{ {
float spriteSize = info.Sprite.size.X * (frame.Rect.Height / info.Sprite.size.Y) + padding; float spriteSize = info.Sprite.size.X * (parentWidth / info.Sprite.size.X) + padding;
if (totalWidth + spriteSize > frame.Rect.Width) { break; } if (totalWidth + spriteSize > frame.Rect.Width) { break; }
totalWidth += spriteSize; totalWidth += spriteSize;
@@ -1213,10 +1266,11 @@ namespace Barotrauma.Items.Components
float offset = 0; float offset = 0;
int amount = 0; int amount = 0;
foreach (MiniMapSprite info in cardsToDraw) foreach (MiniMapSprite info in cardsToDraw)
{ {
Sprite sprite = info.Sprite; Sprite sprite = info.Sprite;
float scale = frame.Rect.Height / sprite.size.Y; float scale = parentWidth / sprite.size.X;
float spriteSize = sprite.size.X * scale; float spriteSize = sprite.size.X * scale;
float posX = adjustedCenterX + offset; float posX = adjustedCenterX + offset;
@@ -1243,7 +1297,7 @@ namespace Barotrauma.Items.Components
float halfSize = spriteSize / 2f; float halfSize = spriteSize / 2f;
if (i > 0) { offset += halfSize; } if (i > 0) { offset += halfSize; }
Vector2 pos = new Vector2(adjustedCenterX + offset, centerY); Vector2 pos = new Vector2(adjustedCenterX + offset, centerY);
sprite.Draw(spriteBatch, pos, info.Color, scale: scale, origin: sprite.size / 2f); sprite.Draw(spriteBatch, pos, info.Color * 0.8f, scale: scale, origin: sprite.size / 2f);
offset += halfSize + padding; offset += halfSize + padding;
amount++; amount++;
} }
@@ -446,11 +446,6 @@ namespace Barotrauma.Items.Components
zoomSlider.BarScroll += PlayerInput.ScrollWheelSpeed / 1000.0f; zoomSlider.BarScroll += PlayerInput.ScrollWheelSpeed / 1000.0f;
zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll); zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll);
} }
if (PlayerInput.KeyHit(InputType.Run))
{
SonarModeSwitch.OnClicked(SonarModeSwitch, null);
}
} }
float distort = 1.0f - item.Condition / item.MaxCondition; float distort = 1.0f - item.Condition / item.MaxCondition;
@@ -1634,7 +1629,7 @@ namespace Barotrauma.Items.Components
void CalculateDistance() void CalculateDistance()
{ {
pathFinder ??= new PathFinder(WayPoint.WayPointList, indoorsSteering: false); pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
if (!path.Unreachable) if (!path.Unreachable)
{ {
@@ -244,7 +244,7 @@ namespace Barotrauma.Items.Components
sabotageButtonText : sabotageButtonText :
sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1);
TinkerButton.Visible = CanTinker(character); TinkerButton.Visible = IsTinkerable(character);
TinkerButton.IgnoreLayoutGroups = !TinkerButton.Visible; TinkerButton.IgnoreLayoutGroups = !TinkerButton.Visible;
TinkerButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Tinker)) && CanTinker(character); TinkerButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Tinker)) && CanTinker(character);
TinkerButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Tinker && CanTinker(character)) ? TinkerButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Tinker && CanTinker(character)) ?
@@ -303,6 +303,7 @@ namespace Barotrauma.Items.Components
deteriorationTimer = msg.ReadSingle(); deteriorationTimer = msg.ReadSingle();
deteriorateAlwaysResetTimer = msg.ReadSingle(); deteriorateAlwaysResetTimer = msg.ReadSingle();
DeteriorateAlways = msg.ReadBoolean(); DeteriorateAlways = msg.ReadBoolean();
tinkeringDuration = msg.ReadSingle();
ushort currentFixerID = msg.ReadUInt16(); ushort currentFixerID = msg.ReadUInt16();
currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2);
CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null;
@@ -1316,6 +1316,15 @@ namespace Barotrauma
{ {
selectedSlot = null; selectedSlot = null;
} }
var parentItem = (selectedSlot?.ParentInventory?.Owner as Item) ?? selectedSlot?.Item;
if ((parentItem?.GetRootInventoryOwner() is Character ownerCharacter) &&
ownerCharacter == Character.Controlled &&
CharacterHealth.OpenHealthWindow?.Character != ownerCharacter &&
ownerCharacter.Inventory.IsInLimbSlot(parentItem, InvSlotType.HealthInterface))
{
highlightedSubInventorySlots.RemoveWhere(s => s.Item == parentItem);
selectedSlot = null;
}
} }
} }
} }
@@ -1394,7 +1403,7 @@ namespace Barotrauma
float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f); float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f);
Vector2 itemPos = PlayerInput.MousePosition; Vector2 itemPos = PlayerInput.MousePosition;
bool mouseOnHealthInterface = CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.MouseOnElement; bool mouseOnHealthInterface = CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.MouseOnElement && DraggingItems.Any(it => it.UseInHealthInterface);
if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null)
{ {
@@ -1453,7 +1462,8 @@ namespace Barotrauma
} }
Color slotColor = Color.White; Color slotColor = Color.White;
if (inventory?.Owner is Item i && !i.IsPlayerTeamInteractable) { slotColor = Color.Gray; } Item parentItem = inventory?.Owner as Item;
if (parentItem != null && !parentItem.IsPlayerTeamInteractable) { slotColor = Color.Gray; }
var itemContainer = item?.GetComponent<ItemContainer>(); var itemContainer = item?.GetComponent<ItemContainer>();
if (itemContainer != null && (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null)) if (itemContainer != null && (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null))
{ {
@@ -1576,6 +1586,14 @@ namespace Barotrauma
pulsate: !usingDefaultSprite && containedState >= 0.0f && containedState < 0.25f && inventory == Character.Controlled?.Inventory && Character.Controlled.HasEquippedItem(item)); pulsate: !usingDefaultSprite && containedState >= 0.0f && containedState < 0.25f && inventory == Character.Controlled?.Inventory && Character.Controlled.HasEquippedItem(item));
} }
} }
else
{
var slotIcon = parentItem?.GetComponent<ItemContainer>()?.GetSlotIcon(slotIndex);
if (slotIcon != null)
{
slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUI.Style.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f);
}
}
} }
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
@@ -1609,7 +1627,7 @@ namespace Barotrauma
Color spriteColor = sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor(); Color spriteColor = sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor();
if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => it.NonInteractable || it.NonPlayerTeamInteractable))) { spriteColor *= 0.5f; } if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => it.NonInteractable || it.NonPlayerTeamInteractable))) { spriteColor *= 0.5f; }
if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface) if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface && !item.AllowedSlots.Contains(InvSlotType.HealthInterface) && item.GetComponent<GeneticMaterial>() == null)
{ {
spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f); spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f);
} }
@@ -77,6 +77,13 @@ namespace Barotrauma
protected set; protected set;
} }
[Serialize(true, false)]
public bool ShowInStatusMonitor
{
get;
private set;
}
[Serialize("", false)] [Serialize("", false)]
public string ImpactSoundTag { get; private set; } public string ImpactSoundTag { get; private set; }
@@ -993,6 +993,10 @@ namespace Barotrauma
clone.Move(moveAmount); clone.Move(moveAmount);
clone.Submarine = Submarine.MainSub; clone.Submarine = Submarine.MainSub;
} }
foreach (MapEntity clone in SelectedList)
{
(clone as Item)?.GetComponent<ItemContainer>()?.SetContainedItemPositions();
}
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false, handleInventoryBehavior: false)); SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false, handleInventoryBehavior: false));
} }
@@ -127,6 +127,13 @@ namespace Barotrauma
ID.ToString(), ID.ToString(),
new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30),
color); color);
if (Tunnel?.Type != null)
{
GUI.SmallFont.DrawString(spriteBatch,
Tunnel.Type.ToString(),
new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45),
color);
}
} }
public override bool IsMouseOn(Vector2 position) public override bool IsMouseOn(Vector2 position)
@@ -494,7 +494,26 @@ namespace Barotrauma.Networking
stream?.Close(); stream?.Close();
break; break;
case FileTransferType.CampaignSave: case FileTransferType.CampaignSave:
//TODO: verify that the received file is a valid save file try
{
var files = SaveUtil.EnumerateContainedFiles(fileTransfer.FilePath);
foreach (var file in files)
{
string extension = Path.GetExtension(file);
if ((!extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
&& !file.Equals("gamesession.xml"))
|| file.CleanUpPathCrossPlatform(correctFilenameCase: false).Contains('/'))
{
ErrorMessage = $"Found unexpected file in \"{fileTransfer.FileName}\"! ({file})";
return false;
}
}
}
catch (Exception e)
{
ErrorMessage = $"Loading received campaign save \"{fileTransfer.FileName}\" failed! {{{e.Message}}}";
return false;
}
break; break;
} }
@@ -1,7 +1,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using Barotrauma.IO;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@@ -56,11 +56,6 @@ namespace Barotrauma
MiniMap miniMap = miniMapItem.GetComponent<MiniMap>(); MiniMap miniMap = miniMapItem.GetComponent<MiniMap>();
miniMap.PowerConsumption = 0; miniMap.PowerConsumption = 0;
foreach (var hull in Hull.hullList)
{
hull.WaterVolume = hull.Volume / 2f;
}
dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false);
dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Info.Name = "Galldren";
dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Inventory.CreateSlots();
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.1500.1.0</Version> <Version>0.1500.2.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright> <Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.1500.1.0</Version> <Version>0.1500.2.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright> <Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.1500.1.0</Version> <Version>0.1500.2.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright> <Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.1500.1.0</Version> <Version>0.1500.2.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright> <Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.1500.1.0</Version> <Version>0.1500.2.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright> <Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -74,6 +74,7 @@ namespace Barotrauma
} }
} }
msg.Write((ushort)ExperiencePoints); msg.Write((ushort)ExperiencePoints);
msg.Write((ushort)AdditionalTalentPoints);
} }
} }
} }
@@ -38,6 +38,7 @@ namespace Barotrauma.Items.Components
msg.Write(deteriorationTimer); msg.Write(deteriorationTimer);
msg.Write(deteriorateAlwaysResetTimer); msg.Write(deteriorateAlwaysResetTimer);
msg.Write(DeteriorateAlways); msg.Write(DeteriorateAlways);
msg.Write(tinkeringDuration);
msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID);
msg.WriteRangedInteger((int)currentFixerAction, 0, 2); msg.WriteRangedInteger((int)currentFixerAction, 0, 2);
} }
@@ -163,9 +163,7 @@ namespace Barotrauma.Networking
RadiationEnabled = incMsg.ReadBoolean(); RadiationEnabled = incMsg.ReadBoolean();
int maxMissionCount = MaxMissionCount + incMsg.ReadByte() - 1; int maxMissionCount = MaxMissionCount + incMsg.ReadByte() - 1;
if (maxMissionCount < CampaignSettings.MinMissionCountLimit) maxMissionCount = CampaignSettings.MaxMissionCountLimit; MaxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit);
if (maxMissionCount > CampaignSettings.MaxMissionCountLimit) maxMissionCount = CampaignSettings.MinMissionCountLimit;
MaxMissionCount = maxMissionCount;
changed |= true; changed |= true;
} }
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.1500.1.0</Version> <Version>0.1500.2.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright> <Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -1,5 +1,6 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Networking; using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -95,14 +96,26 @@ namespace Barotrauma
} }
} }
protected bool HasValidPath(bool requireNonDirty = false) => public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable && (!requireNonDirty || !pathSteering.IsPathDirty); steeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
!pathSteering.CurrentPath.Unreachable &&
(!requireNonDirty || !pathSteering.IsPathDirty);
protected readonly float colliderWidth;
protected readonly float colliderLength;
protected readonly float avoidLookAheadDistance;
public AIController (Character c) public AIController (Character c)
{ {
Character = c; Character = c;
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer); hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
Enabled = true; Enabled = true;
var size = Character.AnimController.Collider.GetSize();
colliderWidth = size.X;
colliderLength = size.Y;
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
} }
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { } public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
@@ -327,6 +340,119 @@ namespace Barotrauma
unequippedItems.Clear(); unequippedItems.Clear();
} }
#region Escape
public abstract void Escape(float deltaTime);
public Gap EscapeTarget { get; private set; }
private readonly float escapeTargetSeekInterval = 2;
private float escapeTimer;
protected bool allGapsSearched;
protected readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
protected bool UpdateEscape(float deltaTime, bool canAttackDoors)
{
IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager;
if (allGapsSearched)
{
escapeTimer -= deltaTime;
if (escapeTimer <= 0)
{
allGapsSearched = false;
}
}
if (Character.CurrentHull != null && pathSteering != null)
{
// Seek exit if inside
if (!allGapsSearched)
{
float closestDistance = 0;
foreach (Gap gap in Gap.GapList)
{
if (gap == null || gap.Removed) { continue; }
if (EscapeTarget == gap) { continue; }
if (unreachableGaps.Contains(gap)) { continue; }
if (gap.Submarine != Character.Submarine) { continue; }
if (gap.IsRoomToRoom) { continue; }
float multiplier = 1;
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
}
}
else
{
if (gap.Open < 1) { continue; }
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
if (!canGetThrough) { continue; }
}
if (gap.FlowTargetHull == Character.CurrentHull)
{
// If the gap is in the same room, it's close enough.
EscapeTarget = gap;
break;
}
float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier;
if (EscapeTarget == null || distance < closestDistance)
{
EscapeTarget = gap;
closestDistance = distance;
}
}
allGapsSearched = true;
escapeTimer = escapeTargetSeekInterval;
}
else if (EscapeTarget != null && EscapeTarget.FlowTargetHull != Character.CurrentHull)
{
if (pathSteering.CurrentPath != null && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
{
unreachableGaps.Add(EscapeTarget);
EscapeTarget = null;
allGapsSearched = false;
}
}
}
if (EscapeTarget != null)
{
SteeringManager.SteeringSeek(EscapeTarget.SimPosition, 10);
float sqrDist = Vector2.DistanceSquared(Character.SimPosition, EscapeTarget.SimPosition);
if (sqrDist < 0.5f || Character.CurrentHull == null || HasValidPath(requireNonDirty: true, requireUnfinished: false) && pathSteering.CurrentPath.Finished)
{
// Very close to the target, outside, or at the end of the path -> just steer towards it manually without using the path
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(EscapeTarget.WorldPosition - Character.WorldPosition));
if (sqrDist < 4)
{
return true;
}
}
}
else
{
// Can't find the target
EscapeTarget = null;
allGapsSearched = false;
unreachableGaps.Clear();
}
return false;
}
public void ResetEscape()
{
EscapeTarget = null;
allGapsSearched = false;
unreachableGaps.Clear();
}
#endregion
protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
@@ -60,8 +60,6 @@ namespace Barotrauma
// Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target.
private const float minPriority = 10; private const float minPriority = 10;
private readonly float avoidLookAheadDistance;
private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
private SteeringManager outsideSteering, insideSteering; private SteeringManager outsideSteering, insideSteering;
@@ -114,19 +112,20 @@ namespace Barotrauma
} }
} }
public AITargetMemory SelectedTargetMemory => selectedTargetMemory;
private AITargetMemory selectedTargetMemory; private AITargetMemory selectedTargetMemory;
private float targetValue; private float targetValue;
private CharacterParams.TargetParams selectedTargetingParams; private CharacterParams.TargetParams selectedTargetingParams;
private Dictionary<AITarget, AITargetMemory> targetMemories; private Dictionary<AITarget, AITargetMemory> targetMemories;
private readonly float colliderWidth;
private readonly float colliderLength;
private readonly int requiredHoleCount; private readonly int requiredHoleCount;
private bool canAttackWalls; private bool canAttackWalls;
public bool CanAttackDoors => canAttackDoors;
private bool canAttackDoors; private bool canAttackDoors;
private bool canAttackCharacters; private bool canAttackCharacters;
public float PriorityFearIncrement => priorityFearIncreasement;
private readonly float priorityFearIncreasement = 2; private readonly float priorityFearIncreasement = 2;
private readonly float memoryFadeTime = 0.5f; private readonly float memoryFadeTime = 0.5f;
@@ -299,12 +298,8 @@ namespace Barotrauma
steeringManager = outsideSteering; steeringManager = outsideSteering;
State = AIState.Idle; State = AIState.Idle;
var size = Character.AnimController.Collider.GetSize();
colliderWidth = size.X;
colliderLength = size.Y;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
} }
@@ -440,9 +435,9 @@ namespace Barotrauma
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
{ {
if (SelectedAiTarget?.Entity != null || escapeTarget != null) if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
{ {
Entity t = SelectedAiTarget?.Entity ?? escapeTarget; Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
} }
@@ -585,7 +580,7 @@ namespace Barotrauma
case AIState.Escape: case AIState.Escape:
case AIState.Flee: case AIState.Flee:
run = true; run = true;
UpdateEscape(deltaTime); Escape(deltaTime);
break; break;
case AIState.Avoid: case AIState.Avoid:
case AIState.PassiveAggressive: case AIState.PassiveAggressive:
@@ -602,7 +597,7 @@ namespace Barotrauma
run = true; run = true;
if (State == AIState.Avoid) if (State == AIState.Avoid)
{ {
UpdateEscape(deltaTime); Escape(deltaTime);
} }
else else
{ {
@@ -827,142 +822,6 @@ namespace Barotrauma
#endregion #endregion
#region Escape
private readonly float escapeTargetSeekInterval = 2;
private float escapeTimer;
private Gap escapeTarget;
private bool allGapsSearched;
private readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
private void UpdateEscape(float deltaTime)
{
if (SelectedAiTarget != null && (SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed))
{
State = AIState.Idle;
return;
}
else if (selectedTargetMemory != null && SelectedAiTarget?.Entity is Character)
{
selectedTargetMemory.Priority += deltaTime * priorityFearIncreasement;
}
IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager;
bool hasValidPath = pathSteering?.CurrentPath != null && !pathSteering.IsPathDirty && !pathSteering.CurrentPath.Unreachable;
if (allGapsSearched)
{
escapeTimer -= deltaTime;
if (escapeTimer <= 0)
{
allGapsSearched = false;
}
}
if (Character.CurrentHull != null && pathSteering != null)
{
// Seek exit if inside
if (!allGapsSearched)
{
float closestDistance = 0;
foreach (Gap gap in Gap.GapList)
{
if (gap == null || gap.Removed) { continue; }
if (escapeTarget == gap) { continue; }
if (unreachableGaps.Contains(gap)) { continue; }
if (gap.Submarine != Character.Submarine) { continue; }
if (gap.IsRoomToRoom) { continue; }
float multiplier = 1;
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
}
}
else
{
if (gap.Open < 1) { continue; }
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
if (!canGetThrough) { continue; }
}
if (gap.FlowTargetHull == Character.CurrentHull)
{
// If the gap is in the same room, it's close enough.
escapeTarget = gap;
break;
}
float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier;
if (escapeTarget == null || distance < closestDistance)
{
escapeTarget = gap;
closestDistance = distance;
}
}
allGapsSearched = true;
escapeTimer = escapeTargetSeekInterval;
}
else if (escapeTarget != null && escapeTarget.FlowTargetHull != Character.CurrentHull)
{
if (pathSteering.CurrentPath != null && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
{
unreachableGaps.Add(escapeTarget);
escapeTarget = null;
allGapsSearched = false;
}
}
}
if (escapeTarget != null && Character.CurrentHull != null && Vector2.DistanceSquared(Character.SimPosition, escapeTarget.SimPosition) > 0.5f)
{
if (hasValidPath && pathSteering.CurrentPath.Finished)
{
// Steer manually towards the gap
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(escapeTarget.WorldPosition - Character.WorldPosition));
}
else if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull)
{
// Steer away from the target if in the same room
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; }
SteeringManager.SteeringManual(deltaTime, escapeDir);
return;
}
else if (pathSteering != null)
{
if (hasValidPath && canAttackDoors)
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
{
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
{
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
State = AIState.Attack;
return;
}
}
}
}
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10);
}
else
{
escapeTarget = null;
allGapsSearched = false;
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
SteeringManager.SteeringManual(deltaTime, escapeDir);
if (Character.CurrentHull == null)
{
SteeringManager.SteeringWander();
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
}
}
#endregion
#region Attack #region Attack
private Vector2 attackWorldPos; private Vector2 attackWorldPos;
@@ -3129,11 +2988,9 @@ namespace Barotrauma
{ {
LatchOntoAI?.DeattachFromBody(reset: true); LatchOntoAI?.DeattachFromBody(reset: true);
Character.AnimController.ReleaseStuckLimbs(); Character.AnimController.ReleaseStuckLimbs();
escapeTarget = null;
AttackingLimb = null; AttackingLimb = null;
movementMargin = 0; movementMargin = 0;
allGapsSearched = false; ResetEscape();
unreachableGaps.Clear();
if (isStateChanged && to == AIState.Idle && from != to) if (isStateChanged && to == AIState.Idle && from != to)
{ {
SetStateResetTimer(); SetStateResetTimer();
@@ -3283,6 +3140,71 @@ namespace Barotrauma
public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount); public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount);
public override void Escape(float deltaTime)
{
if (SelectedAiTarget != null && (SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed))
{
State = AIState.Idle;
return;
}
else if (SelectedTargetMemory is AITargetMemory targetMemory && SelectedAiTarget?.Entity is Character)
{
targetMemory.Priority += deltaTime * PriorityFearIncrement;
}
bool isSteeringThroughGap = UpdateEscape(deltaTime, canAttackDoors);
if (!isSteeringThroughGap)
{
if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull)
{
SteerAwayFromTheEnemy();
}
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true))
{
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
{
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
{
SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority);
State = AIState.Attack;
}
}
}
}
if (EscapeTarget == null)
{
if (SelectedAiTarget?.Entity is Character)
{
SteerAwayFromTheEnemy();
}
else
{
SteeringManager.SteeringWander();
if (Character.CurrentHull == null)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
}
}
void SteerAwayFromTheEnemy()
{
if (SelectedAiTarget == null) { return; }
Vector2 escapeDir = Vector2.Normalize(WorldPosition - SelectedAiTarget.WorldPosition);
if (Character.CurrentHull != null && !Character.AnimController.InWater)
{
// Inside
escapeDir = new Vector2(Math.Sign(escapeDir.X), 0);
}
if (!MathUtils.IsValid(escapeDir))
{
escapeDir = Vector2.UnitY;
}
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, escapeDir);
}
}
private readonly List<Limb> targetLimbs = new List<Limb>(); private readonly List<Limb> targetLimbs = new List<Limb>();
public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None) public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None)
{ {
@@ -20,6 +20,11 @@ namespace Barotrauma
private float reactTimer; private float reactTimer;
private float unreachableClearTimer; private float unreachableClearTimer;
private bool shouldCrouch; private bool shouldCrouch;
public bool IsInsideCave { get; private set; }
/// <summary>
/// Resets each frame
/// </summary>
public bool AutoFaceMovement = true;
const float reactionTime = 0.3f; const float reactionTime = 0.3f;
const float crouchRaycastInterval = 1; const float crouchRaycastInterval = 1;
@@ -52,7 +57,7 @@ namespace Barotrauma
private readonly float steeringBufferIncreaseSpeed = 100; private readonly float steeringBufferIncreaseSpeed = 100;
private float steeringBuffer; private float steeringBuffer;
private readonly float obstacleRaycastInterval = 1; private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer; private float obstacleRaycastTimer;
private readonly float enemyCheckInterval = 0.2f; private readonly float enemyCheckInterval = 0.2f;
@@ -86,6 +91,8 @@ namespace Barotrauma
private readonly SteeringManager outsideSteering, insideSteering; private readonly SteeringManager outsideSteering, insideSteering;
public bool UseIndoorSteeringOutside { get; set; } = false;
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -207,33 +214,78 @@ namespace Barotrauma
IgnoredItems.Clear(); IgnoredItems.Clear();
} }
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true)
{
Entity target = SelectedAiTarget?.Entity;
if (target == null)
{
return false;
}
if (useTargetSub)
{
if (target.Submarine is Submarine sub)
{
target = sub;
threshold += Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2;
}
else
{
return false;
}
}
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2);
}
bool hasValidPath = HasValidPath(); bool hasValidPath = HasValidPath();
if (Character.Submarine == null) if (Character.Submarine == null)
{ {
if (hasValidPath) // When the character is outside, far enough from the target, and the direct route is blocked,
// use the indoor steering with the main and side path waypoints to help avoid getting stuck in level walls
if (SelectedAiTarget?.Entity != null && !IsCloseEnoughToTarget(2000, useTargetSub: false))
{ {
obstacleRaycastTimer -= deltaTime; obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0) if (obstacleRaycastTimer <= 0)
{ {
obstacleRaycastTimer = obstacleRaycastInterval; obstacleRaycastTimer = obstacleRaycastIntervalLong;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). Vector2 rayEnd = SelectedAiTarget.Entity.SimPosition;
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) if (SelectedAiTarget.Entity.Submarine != null)
{ {
if (connectedSub == Submarine.MainSub) { continue; } rayEnd += SelectedAiTarget.Entity.Submarine.SimPosition;
Vector2 rayStart = SimPosition - connectedSub.SimPosition; }
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; UseIndoorSteeringOutside = Submarine.PickBody(SimPosition, rayEnd, collisionCategory: Physics.CollisionLevel) != null;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); }
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) }
else
{
UseIndoorSteeringOutside = false;
if (hasValidPath)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{ {
PathSteering.CurrentPath.Unreachable = true; if (connectedSub == Submarine.MainSub) { continue; }
break; Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
} }
} }
} }
} }
} }
else
{
UseIndoorSteeringOutside = false;
}
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
{ {
@@ -273,13 +325,31 @@ namespace Barotrauma
} }
} }
} }
if (Character.Submarine != null || hasValidPath && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
// Check whether the character is inside a cave
if (IsInsideCave)
{
// If the character was inside a cave, require them to move a bit further from the area to set the field back to false
// This is to avoid any twitchy behavior with the steering managers
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c =>
{
var area = c.Area;
area.Inflate(new Vector2(100));
return area.Contains(Character.WorldPosition);
}) is Level.Cave;
}
else
{
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave;
}
if (UseIndoorSteeringOutside || IsInsideCave || Character.Submarine != null || hasValidPath && IsCloseEnoughToTarget(maxSteeringBuffer) || IsCloseEnoughToTarget(steeringBuffer))
{ {
if (steeringManager != insideSteering) if (steeringManager != insideSteering)
{ {
insideSteering.Reset(); insideSteering.Reset();
steeringManager = insideSteering;
} }
steeringManager = insideSteering;
steeringBuffer += steeringBufferIncreaseSpeed * deltaTime; steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
} }
else else
@@ -287,8 +357,8 @@ namespace Barotrauma
if (steeringManager != outsideSteering) if (steeringManager != outsideSteering)
{ {
outsideSteering.Reset(); outsideSteering.Reset();
steeringManager = outsideSteering;
} }
steeringManager = outsideSteering;
steeringBuffer = minSteeringBuffer; steeringBuffer = minSteeringBuffer;
} }
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
@@ -419,7 +489,7 @@ namespace Barotrauma
Character.SelectedConstruction.SecondaryUse(deltaTime, Character); Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
} }
} }
else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) else if (AutoFaceMovement && Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
{ {
newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
} }
@@ -429,6 +499,7 @@ namespace Barotrauma
flipTimer = FlipInterval; flipTimer = FlipInterval;
} }
} }
AutoFaceMovement = true;
MentalStateManager?.Update(deltaTime); MentalStateManager?.Update(deltaTime);
ShipCommandManager?.Update(deltaTime); ShipCommandManager?.Update(deltaTime);
@@ -1240,10 +1311,7 @@ namespace Barotrauma
{ {
var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager) var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
{ {
HoldPosition = HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
Character.Info?.Job?.Prefab.Identifier == "watchman" ||
Character.CurrentHull == null ||
Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>()?.Target is Character followTarget && followTarget.IsPlayer,
AbortCondition = abortCondition, AbortCondition = abortCondition,
allowHoldFire = allowHoldFire, allowHoldFire = allowHoldFire,
}; };
@@ -1293,6 +1361,11 @@ namespace Barotrauma
ObjectiveManager.WaitTimer = waitDuration; ObjectiveManager.WaitTimer = waitDuration;
} }
public override void Escape(float deltaTime)
{
UpdateEscape(deltaTime, canAttackDoors: false);
}
private void CheckCrouching(float deltaTime) private void CheckCrouching(float deltaTime)
{ {
crouchRaycastTimer -= deltaTime; crouchRaycastTimer -= deltaTime;
@@ -78,7 +78,7 @@ namespace Barotrauma
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host) public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{ {
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), indoorsSteering: true); pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
pathFinder.GetNodePenalty = GetNodePenalty; pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors; this.canOpenDoors = canOpenDoors;
@@ -160,26 +160,34 @@ namespace Barotrauma
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true) private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{ {
Vector2 targetDiff = target - currentTarget; bool needsNewPath = currentPath == null || currentPath.Unreachable;
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null) if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{ {
//target in a different sub than where the character is now Vector2 targetDiff = target - currentTarget;
//take that into account when calculating if the target has moved if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
{ {
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; //target in a different sub than where the character is now
targetDiff += subDiff; //take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
if (targetDiff.LengthSquared() > 1)
{
needsNewPath = true;
} }
} }
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || targetDiff.LengthSquared() > 1);
//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 (needsNewPath || findPathTimer < -1.0f) if (needsNewPath || findPathTimer < -1.0f)
{ {
IsPathDirty = true; IsPathDirty = true;
if (findPathTimer < 0) if (findPathTimer < 0)
{ {
SkipCurrentPathNodes();
currentTarget = target; currentTarget = target;
Vector2 currentPos = host.SimPosition; Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null) if (character != null && character.Submarine == null)
@@ -193,7 +201,7 @@ namespace Barotrauma
pathFinder.InsideSubmarine = character.Submarine != null; pathFinder.InsideSubmarine = character.Submarine != null;
pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0; pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0; bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{ {
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
@@ -206,7 +214,7 @@ namespace Barotrauma
// Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us). // Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us).
float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1); float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1);
useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t); useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t);
if (!useNewPath) if (!useNewPath && character.Submarine != null)
{ {
// It's possible that the current path was calculated from a start point that is no longer valid. // It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
@@ -239,6 +247,32 @@ namespace Barotrauma
findPathTimer = priority * Rand.Range(1.0f, 1.2f); findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false; IsPathDirty = false;
return DiffToCurrentNode(); return DiffToCurrentNode();
void SkipCurrentPathNodes()
{
if (!character.AnimController.InWater || character.Submarine != null) { return; }
if (CurrentPath == null || CurrentPath.Unreachable || CurrentPath.Finished) { return; }
if (CurrentPath.CurrentIndex < 0 || CurrentPath.CurrentIndex >= CurrentPath.Nodes.Count - 1) { return; }
// Check if we could skip ahead to NextNode when the character is swimming and using waypoints outside.
// Do this to optimize the old path before creating and evaluating a new path.
// In general, this is to avoid behavior where:
// a) the character goes back to first reach CurrentNode when the second node would be closer; or
// b) the character moves along the path when they could cut through open space to reduce the total distance.
float pathDistance = Vector2.Distance(character.WorldPosition, CurrentPath.CurrentNode.WorldPosition);
pathDistance += CurrentPath.GetLength(startIndex: CurrentPath.CurrentIndex);
for (int i = CurrentPath.Nodes.Count - 1; i > CurrentPath.CurrentIndex + 1; i--)
{
var waypoint = CurrentPath.Nodes[i];
float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition);
if (directDistance > (pathDistance * pathDistance) || Submarine.PickBody(host.SimPosition, waypoint.SimPosition, collisionCategory: Physics.CollisionLevel) != null)
{
pathDistance -= CurrentPath.GetLength(startIndex: i - 1, endIndex: i);
continue;
}
CurrentPath.SkipToNode(i);
break;
}
}
} }
} }
@@ -278,18 +312,15 @@ namespace Barotrauma
CheckDoorsInPath(); CheckDoorsInPath();
} }
Vector2 pos = host.SimPosition; Vector2 pos = host.SimPosition;
if (character != null && currentPath.CurrentNode != null) if (character != null && CurrentPath.CurrentNode?.Submarine != null)
{ {
if (CurrentPath.CurrentNode.Submarine != null) if (character.Submarine == null)
{ {
if (character.Submarine == null) pos -= CurrentPath.CurrentNode.Submarine.SimPosition;
{ }
pos -= CurrentPath.CurrentNode.Submarine.SimPosition; else if (character.Submarine != currentPath.CurrentNode.Submarine)
} {
else if (character.Submarine != currentPath.CurrentNode.Submarine) pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
{
pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
}
} }
} }
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
@@ -94,7 +94,7 @@ namespace Barotrauma
if (_abandon) if (_abandon)
{ {
#if DEBUG #if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>()) if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
{ {
throw new Exception("Order abandoned!"); throw new Exception("Order abandoned!");
} }
@@ -83,12 +83,13 @@ namespace Barotrauma
if (suitableContainer != null) if (suitableContainer != null)
{ {
bool equip = item.GetComponent<Holdable>() != null || bool equip = item.GetComponent<Holdable>() != null ||
item.AllowedSlots.None(s => item.AllowedSlots.Any(s => s != InvSlotType.Any) &&
s == InvSlotType.Card || item.AllowedSlots.None(s =>
s == InvSlotType.Head || s == InvSlotType.Card ||
s == InvSlotType.Headset || s == InvSlotType.Head ||
s == InvSlotType.InnerClothes || s == InvSlotType.Headset ||
s == InvSlotType.OuterClothes); s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>()) TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{ {
@@ -252,9 +252,40 @@ namespace Barotrauma
{ {
case CombatMode.Offensive: case CombatMode.Offensive:
case CombatMode.Arrest: case CombatMode.Arrest:
Engage(); Engage(deltaTime);
break; break;
case CombatMode.Defensive: case CombatMode.Defensive:
if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
{
Engage(deltaTime);
}
else
{
// Keep following the goto target
var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
if (gotoObjective != null)
{
gotoObjective.ForceAct(deltaTime);
if (!character.AnimController.InWater)
{
HumanAIController.FaceTarget(Enemy);
ForceWalk = true;
HumanAIController.AutoFaceMovement = false;
}
}
else
{
SteeringManager.Reset();
}
}
}
else
{
Retreat(deltaTime);
}
break;
case CombatMode.Retreat: case CombatMode.Retreat:
Retreat(deltaTime); Retreat(deltaTime);
break; break;
@@ -671,6 +702,14 @@ namespace Barotrauma
{ {
RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref retreatObjective);
} }
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance))
{
// Swim away
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{ {
if (findHullTimer > 0) if (findHullTimer > 0)
@@ -704,7 +743,7 @@ namespace Barotrauma
} }
} }
private void Engage() private void Engage(float deltaTime)
{ {
if (WeaponComponent == null) if (WeaponComponent == null)
{ {
@@ -722,6 +761,21 @@ namespace Barotrauma
RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunitionObjective); RemoveSubObjective(ref seekAmmunitionObjective);
RemoveSubObjective(ref seekWeaponObjective); RemoveSubObjective(ref seekWeaponObjective);
if (character.Submarine == null && WeaponComponent is MeleeWeapon meleeWeapon)
{
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{
// Swim towards the target
SteeringManager.Reset();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 15);
}
else
{
SteeringManager.Reset();
}
return;
}
if (followTargetObjective != null && followTargetObjective.Target != Enemy) if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{ {
RemoveFollowTarget(); RemoveFollowTarget();
@@ -46,7 +46,10 @@ namespace Barotrauma
} }
if (character.CurrentHull == null) if (character.CurrentHull == null)
{ {
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.HasActiveObjective<AIObjectiveCombat>()) && HumanAIController.HasDivingSuit(character) ? 0 : 100; Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
} }
else else
{ {
@@ -57,9 +60,11 @@ namespace Barotrauma
{ {
Priority = 100; Priority = 100;
} }
else if (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{ {
// Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear // Ordered to follow, hold position, or return back to main sub inside a hostile sub
// -> ignore find safety unless we need to find a diving gear
Priority = 0; Priority = 0;
} }
Priority = MathHelper.Clamp(Priority, 0, 100); Priority = MathHelper.Clamp(Priority, 0, 100);
@@ -298,6 +303,7 @@ namespace Barotrauma
Hull bestHull = null; Hull bestHull = null;
float bestValue = 0; float bestValue = 0;
bool bestIsAirlock = false;
foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h))) foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h)))
{ {
if (hull.Submarine == null) { continue; } if (hull.Submarine == null) { continue; }
@@ -306,9 +312,10 @@ namespace Barotrauma
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0; float hullSafety = 0;
if (character.CurrentHull != null && character.Submarine != null) bool hullIsAirlock = false;
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
if (isCharacterInside)
{ {
// Inside
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character); hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
@@ -343,24 +350,16 @@ namespace Barotrauma
} }
else else
{ {
// Outside // TODO: could also target gaps that get us inside?
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) if (hull.IsTaggedAirlock())
{
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestIsAirlock && hull.LeadsOutside(character))
{ {
hullSafety = 100; 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;
}
}
}
// TODO: could we get a closest door to the outside and target the flowing hull if no airlock is found?
// Huge preference for closer targets // Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
@@ -372,10 +371,11 @@ namespace Barotrauma
hullSafety /= 10; hullSafety /= 10;
} }
} }
if (hullSafety > bestValue) if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock))
{ {
bestHull = hull; bestHull = hull;
bestValue = hullSafety; bestValue = hullSafety;
bestIsAirlock = hullIsAirlock;
} }
} }
return bestHull; return bestHull;
@@ -159,6 +159,8 @@ namespace Barotrauma
} }
} }
public void ForceAct(float deltaTime) => Act(deltaTime);
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
{ {
if (followControlledCharacter) if (followControlledCharacter)
@@ -240,7 +242,7 @@ namespace Barotrauma
if (getDivingGearIfNeeded && !character.LockHands) if (getDivingGearIfNeeded && !character.LockHands)
{ {
Character followTarget = Target as Character; Character followTarget = Target as Character;
bool needsDivingSuit = targetIsOutside; bool needsDivingSuit = !isInside || targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (mimic) if (mimic)
{ {
@@ -444,13 +446,22 @@ namespace Barotrauma
} }
if (SteeringManager == PathSteering) if (SteeringManager == PathSteering)
{ {
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null; Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside) if (isInside && !AllowGoingOutside)
{ {
nodeFilter = n => n.Waypoint.CurrentHull != null; nodeFilter = n => n.Waypoint.CurrentHull != null;
} }
else if (!isInside && HumanAIController.UseIndoorSteeringOutside)
{
if (character.Submarine == null && Target.Submarine != null)
{
targetPos += Target.Submarine.SimPosition;
}
nodeFilter = n => n.Waypoint.Tunnel != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, PathSteering.SteeringSeek(targetPos, 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter, endNodeFilter,
nodeFilter, nodeFilter,
@@ -233,11 +233,7 @@ namespace Barotrauma
if (orderObjective == null) { return; } if (orderObjective == null) { return; }
#if DEBUG #if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing. // Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (orderObjective.IsCompleted) if (!orderObjective.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!orderObjective.CanBeCompleted)
{ {
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red); DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
} }
@@ -281,9 +277,9 @@ namespace Barotrauma
ForcedOrder?.CalculatePriority(); ForcedOrder?.CalculatePriority();
AIObjective orderWithHighestPriority = null; AIObjective orderWithHighestPriority = null;
float highestPriority = 0; float highestPriority = 0;
foreach (var currentOrder in CurrentOrders) for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{ {
var orderObjective = currentOrder.Objective; var orderObjective = CurrentOrders[i].Objective;
if (orderObjective == null) { continue; } if (orderObjective == null) { continue; }
orderObjective.CalculatePriority(); orderObjective.CalculatePriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority) if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
@@ -467,6 +463,11 @@ namespace Barotrauma
AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine) AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine)
}; };
break; break;
case "return":
newObjective = new AIObjectiveReturn(character, this, priorityModifier: priorityModifier);
newObjective.Abandoned += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order, option);
break;
case "fixleaks": case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull); newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull);
break; break;
@@ -586,6 +587,27 @@ namespace Barotrauma
return newObjective; return newObjective;
} }
private void DismissSelf(Order order, string option)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order, option));
if (currentOrder.Order == null)
{
#if DEBUG
DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found");
#endif
return;
}
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, currentOrder.Order?.TargetSpatialEntity, character, character));
#endif
}
private bool IsAllowedToWait() private bool IsAllowedToWait()
{ {
if (!character.IsOnPlayerTeam) { return false; } if (!character.IsOnPlayerTeam) { return false; }
@@ -606,6 +628,8 @@ namespace Barotrauma
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T; public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T).Objective as T;
/// <summary> /// <summary>
/// Returns the last active objective of the specific type. /// Returns the last active objective of the specific type.
/// </summary> /// </summary>
@@ -0,0 +1,243 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveReturn : AIObjective
{
public override string Identifier { get; set; } = "return";
private AIObjectiveGoTo moveInsideObjective, moveInCaveObjective, moveOutsideObjective;
private bool usingEscapeBehavior;
public Submarine ReturnTarget { get; }
public AIObjectiveReturn(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (ReturnTarget == null)
{
DebugConsole.ThrowError("Error with a Return objective: no suitable return target found");
Abandon = true;
}
Submarine GetReturnTarget(IEnumerable<Submarine> subs)
{
Submarine returnTarget = null;
foreach (var sub in subs)
{
if (sub?.TeamID != character.TeamID) { continue; }
returnTarget = sub;
break;
}
return returnTarget;
}
}
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
// TODO: Consider if this needs to be addressed
Priority = 0;
}
return Priority;
}
protected override void Act(float deltaTime)
{
if (ReturnTarget == null)
{
Abandon = true;
return;
}
bool shouldUseEscapeBehavior = false;
if (character.CurrentHull != null)
{
if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget))
{
// Character is on another sub that is not connected to the target sub, use the escape behavior to get them out
shouldUseEscapeBehavior = true;
if (!usingEscapeBehavior)
{
HumanAIController.ResetEscape();
}
HumanAIController.Escape(deltaTime);
if (HumanAIController.EscapeTarget == null || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
{
Abandon = true;
}
}
else if (character.Submarine != ReturnTarget)
{
// Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub
if (moveInsideObjective == null)
{
Hull targetHull = null;
foreach (var d in ReturnTarget.ConnectedDockingPorts.Values)
{
if (!d.Docked) { continue; }
if (d.DockingTarget == null) { continue; }
if (d.DockingTarget.Item.Submarine != character.Submarine) { continue; }
targetHull = d.Item.CurrentHull;
break;
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInCaveObjective);
RemoveSubObjective(ref moveOutsideObjective);
// TODO: Check 'repeat' and 'onAbandon' parameters
TryAddSubObjective(ref moveInsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager),
onCompleted: () => moveInsideObjective = null);
}
else
{
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveInsideObjective'");
}
}
}
else
{
// Character is on the target sub, the objective is completed
IsCompleted = true;
}
}
else if (moveInCaveObjective == null && moveOutsideObjective == null)
{
if (HumanAIController.IsInsideCave)
{
WayPoint closestOutsideWaypoint = null;
float closestDistance = float.MaxValue;
foreach (var w in WayPoint.WayPointList)
{
if (w.Tunnel == null) { continue; }
if (w.Tunnel.Type == Level.TunnelType.Cave) { continue; }
if (w.linkedTo.None(l => l is WayPoint linkedWaypoint && linkedWaypoint.Tunnel?.Type == Level.TunnelType.Cave)) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, w.WorldPosition);
if (closestOutsideWaypoint == null || distance < closestDistance)
{
closestOutsideWaypoint = w;
closestDistance = distance;
}
}
if (closestOutsideWaypoint != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveOutsideObjective);
// TODO: Check 'repeat' and 'onAbandon' parameters
TryAddSubObjective(ref moveInCaveObjective,
constructor: () => new AIObjectiveGoTo(closestOutsideWaypoint, character, objectiveManager)
{
endNodeFilter = n => n.Waypoint == closestOutsideWaypoint
},
onCompleted: () => moveInCaveObjective = null);
}
else
{
DebugConsole.ThrowError("Error with a Return objective: no suitable main or side path node target found for 'moveOutsideObjective'");
}
}
else
{
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
bool hullIsAirlock = hull.IsTaggedAirlock();
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
if (targetHull == null || distanceSquared < targetDistanceSquared)
{
targetHull = hull;
targetDistanceSquared = distanceSquared;
targetIsAirlock = hullIsAirlock;
}
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveInCaveObjective);
// TODO: Check 'repeat' and 'onAbandon' parameters
TryAddSubObjective(ref moveOutsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager),
onCompleted: () => moveOutsideObjective = null);
}
else
{
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'");
}
}
}
else
{
if (HumanAIController.IsInsideCave)
{
if (moveOutsideObjective != null)
{
RemoveSubObjective(ref moveOutsideObjective);
moveOutsideObjective = null;
}
}
else
{
if (moveInCaveObjective != null)
{
RemoveSubObjective(ref moveInCaveObjective);
moveInCaveObjective = null;
}
}
}
usingEscapeBehavior = shouldUseEscapeBehavior;
}
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted)
{
return true;
}
if (ReturnTarget == null)
{
Abandon = true;
return false;
}
if (character.Submarine == ReturnTarget)
{
IsCompleted = true;
}
return IsCompleted;
}
public override void Reset()
{
base.Reset();
moveInsideObjective = null;
moveInCaveObjective = null;
moveOutsideObjective = null;
usingEscapeBehavior = false;
HumanAIController.ResetEscape();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager.Reset();
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotreturn", returnNull: true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreturn", minDurationBetweenSimilar: 5.0f);
}
}
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework; using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -23,6 +24,8 @@ namespace Barotrauma
public readonly Vector2 Position; public readonly Vector2 Position;
public readonly int WayPointID; public readonly int WayPointID;
public bool blocked;
public override string ToString() public override string ToString()
{ {
return $"PathNode {WayPointID}"; return $"PathNode {WayPointID}";
@@ -86,21 +89,19 @@ namespace Barotrauma
public GetNodePenaltyHandler GetNodePenalty; public GetNodePenaltyHandler GetNodePenalty;
private readonly List<PathNode> nodes; private readonly List<PathNode> nodes;
public readonly bool IndoorsSteering; private readonly bool isCharacter;
public bool InsideSubmarine { get; set; } public bool InsideSubmarine { get; set; }
public bool ApplyPenaltyToOutsideNodes { get; set; } public bool ApplyPenaltyToOutsideNodes { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false) public PathFinder(List<WayPoint> wayPoints, bool isCharacter)
{ {
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering), removeOrphans: true); nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => (w.Submarine != null == isCharacter) || (isCharacter && w.Tunnel != null)), removeOrphans: true);
foreach (WayPoint wp in wayPoints) foreach (WayPoint wp in wayPoints)
{ {
wp.OnLinksChanged += WaypointLinksChanged; wp.OnLinksChanged += WaypointLinksChanged;
} }
this.isCharacter = isCharacter;
IndoorsSteering = indoorsSteering;
} }
void WaypointLinksChanged(WayPoint wp) void WaypointLinksChanged(WayPoint wp)
@@ -145,6 +146,8 @@ namespace Barotrauma
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true) public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{ {
UpdateBlockedNodes();
//sort nodes roughly according to distance //sort nodes roughly according to distance
sortedNodes.Clear(); sortedNodes.Clear();
foreach (PathNode node in nodes) foreach (PathNode node in nodes)
@@ -152,7 +155,9 @@ namespace Barotrauma
node.TempPosition = node.Position; node.TempPosition = node.Position;
if (hostSub != null) if (hostSub != null)
{ {
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition; Vector2 diff = node.Waypoint.Submarine != null ?
hostSub.SimPosition - node.Waypoint.Submarine.SimPosition :
hostSub.SimPosition - node.Waypoint.SimPosition;
node.TempPosition -= diff; node.TempPosition -= diff;
} }
float xDiff = Math.Abs(start.X - node.TempPosition.X); float xDiff = Math.Abs(start.X - node.TempPosition.X);
@@ -174,29 +179,34 @@ namespace Barotrauma
sortedNodes.Insert(i, node); sortedNodes.Insert(i, node);
} }
bool IsWaypointVisible(PathNode node, Vector2 rayStart, bool checkVisibility = true)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility && isCharacter)
{
if (node.Waypoint.isObstructed) { return false; }
var body = Submarine.PickBody(rayStart, node.TempPosition,
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Structure s && !s.IsPlatform) { return false; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; }
}
}
return true;
}
//find the most suitable start node, starting from the ones that are the closest //find the most suitable start node, starting from the ones that are the closest
PathNode startNode = null; PathNode startNode = null;
foreach (PathNode node in sortedNodes) foreach (PathNode node in sortedNodes)
{ {
if (startNode == null || node.TempDistance < startNode.TempDistance) if (startNode == null || node.TempDistance < startNode.TempDistance)
{ {
if (node.blocked) { continue; }
if (nodeFilter != null && !nodeFilter(node)) { continue; } if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; } if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible // Always check the visibility for the start node
if (IndoorsSteering) if (!IsWaypointVisible(node, start)) { continue; }
{
if (node.Waypoint.isObstructed) { continue; }
// Always check the visibility for the start node
var body = Submarine.PickBody(
start, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
startNode = node; startNode = node;
} }
} }
@@ -241,24 +251,11 @@ namespace Barotrauma
{ {
if (endNode == null || node.TempDistance < endNode.TempDistance) if (endNode == null || node.TempDistance < endNode.TempDistance)
{ {
if (node.blocked) { continue; }
if (nodeFilter != null && !nodeFilter(node)) { continue; } if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; } if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
if (IndoorsSteering) // Only check the visibility for the end node when allowed (fix leaks)
{ if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; }
if (node.Waypoint.isObstructed) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility)
{
// Only check the visibility for the end node when allowed (fix leaks)
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
}
endNode = node; endNode = node;
} }
} }
@@ -330,7 +327,8 @@ namespace Barotrauma
foreach (PathNode node in nodes) foreach (PathNode node in nodes)
{ {
if (node.state != 1) { continue; } if (node.state != 1) { continue; }
if (IndoorsSteering && node.Waypoint.isObstructed) { continue; } if (isCharacter && node.Waypoint.isObstructed) { continue; }
if (node.blocked) { continue; }
if (filter != null && !filter(node)) { continue; } if (filter != null && !filter(node)) { continue; }
if (node.F < dist) if (node.F < dist)
{ {
@@ -438,6 +436,25 @@ namespace Barotrauma
return path; return path;
} }
private void UpdateBlockedNodes()
{
if (!isCharacter) { return; }
foreach (var n in nodes)
{
n.blocked = false;
if (n.Waypoint.Submarine != null) { continue; }
if (n.Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { continue; }
foreach (var w in Level.Loaded.ExtraWalls)
{
if (!(w is DestructibleLevelWall d)) { continue; }
if (d.Destroyed) { continue; }
if (!d.IsPointInside(n.Waypoint.Position)) { continue; }
n.blocked = true;
break;
}
}
}
} }
} }
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Barotrauma namespace Barotrauma
@@ -24,16 +25,47 @@ namespace Barotrauma
if (Unreachable) { return float.PositiveInfinity; } if (Unreachable) { return float.PositiveInfinity; }
if (!totalLength.HasValue) if (!totalLength.HasValue)
{ {
totalLength = 0.0f; CalculateTotalLength();
for (int i = 0; i < nodes.Count - 1; i++)
{
totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition);
}
} }
return totalLength.Value; return totalLength.Value;
} }
} }
public float GetLength(int? startIndex = null, int? endIndex = null)
{
if (Unreachable) { return float.PositiveInfinity; }
startIndex ??= 0;
endIndex ??= Nodes.Count - 1;
if (startIndex == 0 && endIndex == Nodes.Count - 1)
{
return TotalLength;
}
if (!totalLength.HasValue)
{
CalculateTotalLength();
}
float length = 0.0f;
for (int i = startIndex.Value; i < endIndex.Value; i++)
{
length += nodeDistances[i];
}
return length;
}
private void CalculateTotalLength()
{
totalLength = 0.0f;
nodeDistances.Clear();
for (int i = 0; i < nodes.Count - 1; i++)
{
float distance = Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition);
totalLength += distance;
nodeDistances.Add(distance);
}
}
private readonly List<float> nodeDistances = new List<float>();
public SteeringPath(bool unreachable = false) public SteeringPath(bool unreachable = false)
{ {
nodes = new List<WayPoint>(); nodes = new List<WayPoint>();
@@ -107,6 +139,11 @@ namespace Barotrauma
currentIndex++; currentIndex++;
} }
public void SkipToNode(int nodeIndex)
{
currentIndex = nodeIndex;
}
public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f) public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f)
{ {
if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; } if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; }
@@ -1130,7 +1130,7 @@ namespace Barotrauma
{ {
nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction); nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction);
} }
if (ragdollParams == null) if (ragdollParams == null && prefab.VariantOf == null)
{ {
string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name) as RagdollParams; ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name) as RagdollParams;
@@ -3078,30 +3078,47 @@ namespace Barotrauma
//set the character order only if the character is close enough to hear the message //set the character order only if the character is close enough to hear the message
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; } if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (order.OrderGiver != orderGiver) if (order != null && order.OrderGiver != orderGiver)
{ {
order.OrderGiver = orderGiver; order.OrderGiver = orderGiver;
} }
// If there's another character operating the same device, make them dismiss themself switch (order?.Category)
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
{ {
foreach (var character in CharacterList) case OrderCategory.Operate when order?.TargetEntity != null:
{ // If there's another character operating the same device, make them dismiss themself
if (character == this) { continue; } foreach (var character in CharacterList)
if (character.TeamID != TeamID) { continue; } {
if (!(character.AIController is HumanAIController)) { continue; } if (character == this) { continue; }
if (!HumanAIController.IsActive(character)) { continue; } if (character.TeamID != TeamID) { continue; }
foreach (var currentOrder in character.CurrentOrders) if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{ {
if (currentOrder.Order == null) { continue; } if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; } orderToReplace = currentOrder;
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break; break;
} }
} if (orderToReplace.HasValue)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
} }
// Prevent adding duplicate orders // Prevent adding duplicate orders
@@ -3112,7 +3129,8 @@ namespace Barotrauma
if (orderGiver != null) if (orderGiver != null)
{ {
orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, this); var abilityOrderedCharacter = new AbilityCharacter(this);
orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter);
} }
if (AIController is HumanAIController humanAI) if (AIController is HumanAIController humanAI)
@@ -3504,11 +3522,12 @@ namespace Barotrauma
public void RecordKill(Character target) public void RecordKill(Character target)
{ {
var abilityCharacter = new AbilityCharacter(target);
foreach (Character attackerCrewmember in GetFriendlyCrew(this)) foreach (Character attackerCrewmember in GetFriendlyCrew(this))
{ {
attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, target); attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, abilityCharacter);
} }
CheckTalents(AbilityEffectType.OnKillCharacter, target); CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacter);
if (!IsOnPlayerTeam) { return; } if (!IsOnPlayerTeam) { return; }
if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; }
@@ -3604,8 +3623,11 @@ namespace Barotrauma
ApplyStatusEffects(ActionType.OnDamaged, 1.0f); ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
} }
if (attacker != null)
attacker?.CheckTalents(AbilityEffectType.OnAttackResult, attackResult); {
var abilityAttackResult = new AbilityAttackResult(attackResult);
attacker.CheckTalents(AbilityEffectType.OnAttackResult, abilityAttackResult);
}
return attackResult; return attackResult;
} }
@@ -3828,7 +3850,8 @@ namespace Barotrauma
causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource);
OnDeath?.Invoke(this, CauseOfDeath); OnDeath?.Invoke(this, CauseOfDeath);
CheckTalents(AbilityEffectType.OnDieToCharacter, CauseOfDeath.Killer); var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller);
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{ {
@@ -4347,11 +4370,16 @@ namespace Barotrauma
return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead);
} }
public void CheckTalents(AbilityEffectType abilityEffectType, object abilityData) public bool HasTalents()
{
return characterTalents.Any();
}
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{ {
foreach (var characterTalent in characterTalents) foreach (var characterTalent in characterTalents)
{ {
characterTalent.CheckTalent(abilityEffectType, abilityData); characterTalent.CheckTalent(abilityEffectType, abilityObject);
} }
} }
@@ -4359,7 +4387,7 @@ namespace Barotrauma
{ {
foreach (var characterTalent in characterTalents) foreach (var characterTalent in characterTalents)
{ {
characterTalent.CheckTalent(abilityEffectType, (object)null); characterTalent.CheckTalent(abilityEffectType, null);
} }
} }
@@ -4421,7 +4449,8 @@ namespace Barotrauma
//replace by updating the character wearable stat values when equipping or unequipping wearables //replace by updating the character wearable stat values when equipping or unequipping wearables
for (int i = 0; i < Inventory.Capacity; i++) for (int i = 0; i < Inventory.Capacity; i++)
{ {
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable) if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
&& Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
{ {
if (wearable.WearableStatValues.TryGetValue(statType, out float wearableValue)) if (wearable.WearableStatValues.TryGetValue(statType, out float wearableValue))
{ {
@@ -220,7 +220,7 @@ namespace Barotrauma
public HashSet<string> UnlockedTalents { get; private set; } = new HashSet<string>(); public HashSet<string> UnlockedTalents { get; private set; } = new HashSet<string>();
public int AdditionalTalentPoints { get; private set; } public int AdditionalTalentPoints { get; set; }
private Sprite headSprite; private Sprite headSprite;
public Sprite HeadSprite public Sprite HeadSprite
@@ -541,6 +541,7 @@ namespace Barotrauma
Salary = infoElement.GetAttributeInt("salary", 1000); Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<string>(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); UnlockedTalents = new HashSet<string>(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true));
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race); Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race);
Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender); Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender);
_speciesName = infoElement.GetAttributeString("speciesname", null); _speciesName = infoElement.GetAttributeString("speciesname", null);
@@ -985,11 +986,13 @@ namespace Barotrauma
if ((int)newLevel > (int)prevLevel) if ((int)newLevel > (int)prevLevel)
{ {
Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, skillIdentifier); // assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilityValueStringCharacter(increaseSinceLastSkillPoint, skillIdentifier, Character);
Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain);
foreach (Character character in Character.GetFriendlyCrew(Character)) foreach (Character character in Character.GetFriendlyCrew(Character))
{ {
var abilityStringCharacter = new AbilityStringCharacter(skillIdentifier, Character); character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, abilitySkillGain);
character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, abilityStringCharacter);
} }
} }
@@ -1138,6 +1141,7 @@ namespace Barotrauma
new XAttribute("salary", Salary), new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints), new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("headspriteid", HeadSpriteId), new XAttribute("headspriteid", HeadSpriteId),
new XAttribute("hairindex", HairIndex), new XAttribute("hairindex", HairIndex),
new XAttribute("beardindex", BeardIndex), new XAttribute("beardindex", BeardIndex),
@@ -176,6 +176,21 @@ namespace Barotrauma
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
} }
public float GetAfflictionOverlayMultiplier()
{
//If the overlay's alpha progresses linearly, then don't worry about affliction effects.
if (Prefab.AfflictionOverlayAlphaIsLinear) { return (Strength / Prefab.MaxStrength); }
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxAfflictionOverlayAlphaMultiplier - currentEffect.MinAfflictionOverlayAlphaMultiplier < 0.0f) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinAfflictionOverlayAlphaMultiplier,
currentEffect.MaxAfflictionOverlayAlphaMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetScreenBlurStrength() public float GetScreenBlurStrength()
{ {
if (Strength < Prefab.ActivationThreshold) { return 0.0f; } if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
@@ -344,6 +359,8 @@ namespace Barotrauma
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>(); private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
{ {
if (type == ActionType.OnDamaged && !statusEffect.HasRequiredAfflictions(characterHealth.Character.LastDamage)) { return; }
statusEffect.SetUser(Source); statusEffect.SetUser(Source);
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{ {
@@ -21,6 +21,8 @@ namespace Barotrauma
private Character character; private Character character;
private bool stun = true;
private readonly List<Affliction> huskInfection = new List<Affliction>(); private readonly List<Affliction> huskInfection = new List<Affliction>();
[Serialize(0f, true), Editable] [Serialize(0f, true), Editable]
@@ -34,6 +36,7 @@ namespace Barotrauma
float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1; float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
float max = Math.Max(threshold, previousValue); float max = Math.Max(threshold, previousValue);
_strength = Math.Clamp(value, 0, max); _strength = Math.Clamp(value, 0, max);
stun = GameMain.GameSession?.IsRunning ?? true;
if (previousValue > 0.0f && value <= 0.0f) if (previousValue > 0.0f && value <= 0.0f)
{ {
DeactivateHusk(); DeactivateHusk();
@@ -60,6 +63,8 @@ namespace Barotrauma
private float TransitionThreshold => (Prefab as AfflictionPrefabHusk)?.TransitionThreshold ?? Prefab.MaxStrength * 0.75f; private float TransitionThreshold => (Prefab as AfflictionPrefabHusk)?.TransitionThreshold ?? Prefab.MaxStrength * 0.75f;
private float TransformThresholdOnDeath => (Prefab as AfflictionPrefabHusk)?.TransformThresholdOnDeath ?? ActiveThreshold;
public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { } public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { }
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
@@ -91,7 +96,7 @@ namespace Barotrauma
} }
else if (Strength < TransitionThreshold) else if (Strength < TransitionThreshold)
{ {
if (State != InfectionState.Active) if (State != InfectionState.Active && stun)
{ {
character.SetStun(Rand.Range(2, 4)); character.SetStun(Rand.Range(2, 4));
} }
@@ -167,7 +172,7 @@ namespace Barotrauma
private void CharacterDead(Character character, CauseOfDeath causeOfDeath) private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{ {
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < ActiveThreshold || character.Removed) if (Strength < TransformThresholdOnDeath || character.Removed)
{ {
UnsubscribeFromDeathEvent(); UnsubscribeFromDeathEvent();
return; return;
@@ -101,6 +101,7 @@ namespace Barotrauma
DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f); DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f);
ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f); ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f);
TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength); TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength);
TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold);
} }
// Use any of these to define which limb the appendage is attached to. // Use any of these to define which limb the appendage is attached to.
@@ -110,6 +111,7 @@ namespace Barotrauma
public readonly LimbType AttachLimbType; public readonly LimbType AttachLimbType;
public float ActiveThreshold, DormantThreshold, TransitionThreshold; public float ActiveThreshold, DormantThreshold, TransitionThreshold;
public float TransformThresholdOnDeath;
public readonly string HuskedSpeciesName; public readonly string HuskedSpeciesName;
public readonly string[] TargetSpecies; public readonly string[] TargetSpecies;
@@ -182,6 +184,12 @@ namespace Barotrauma
[Serialize(0.0f, false)] [Serialize(0.0f, false)]
public float ScreenEffectFluctuationFrequency { get; private set; } public float ScreenEffectFluctuationFrequency { get; private set; }
[Serialize(1.0f, false)]
public float MinAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, false)]
public float MaxAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, false)] [Serialize(1.0f, false)]
public float MinBuffMultiplier { get; private set; } public float MinBuffMultiplier { get; private set; }
@@ -358,6 +366,9 @@ namespace Barotrauma
public readonly Sprite Icon; public readonly Sprite Icon;
public readonly Color[] IconColors; public readonly Color[] IconColors;
public readonly Sprite AfflictionOverlay;
public readonly bool AfflictionOverlayAlphaIsLinear;
private readonly List<Effect> effects = new List<Effect>(); private readonly List<Effect> effects = new List<Effect>();
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>(); private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
@@ -645,6 +656,7 @@ namespace Barotrauma
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
IconColors = element.GetAttributeColorArray("iconcolors", null); IconColors = element.GetAttributeColorArray("iconcolors", null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", ""); AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
foreach (XElement subElement in element.Elements()) foreach (XElement subElement in element.Elements())
@@ -654,6 +666,9 @@ namespace Barotrauma
case "icon": case "icon":
Icon = new Sprite(subElement); Icon = new Sprite(subElement);
break; break;
case "afflictionoverlay":
AfflictionOverlay = new Sprite(subElement);
break;
} }
} }
@@ -6,6 +6,7 @@ using System.Xml.Linq;
using Barotrauma.Networking; using Barotrauma.Networking;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System.Globalization; using System.Globalization;
using Barotrauma.Abilities;
namespace Barotrauma namespace Barotrauma
{ {
@@ -483,9 +484,12 @@ namespace Barotrauma
{ {
var matchingAffliction = matchingAfflictions[i]; var matchingAffliction = matchingAfflictions[i];
// kind of bad to create a tuple every time, but I can't think of another way to easily do this // this logic runs very often, so culling unnecessary object creation and talent checking with this method
var afflictionReduction = (matchingAffliction, reduceAmount); if (Character.HasTalents())
Character.CheckTalents(AbilityEffectType.OnReduceAffliction, afflictionReduction); {
var afflictionReduction = new AbilityValueAffliction(reduceAmount, matchingAffliction);
Character.CheckTalents(AbilityEffectType.OnReduceAffliction, afflictionReduction);
}
if (matchingAffliction.Strength < reduceAmount) if (matchingAffliction.Strength < reduceAmount)
{ {
@@ -11,6 +11,7 @@ using System.Xml.Linq;
using Barotrauma.Networking; using Barotrauma.Networking;
using LimbParams = Barotrauma.RagdollParams.LimbParams; using LimbParams = Barotrauma.RagdollParams.LimbParams;
using JointParams = Barotrauma.RagdollParams.JointParams; using JointParams = Barotrauma.RagdollParams.JointParams;
using Barotrauma.Abilities;
namespace Barotrauma namespace Barotrauma
{ {
@@ -741,7 +742,11 @@ namespace Barotrauma
{ {
newAffliction.SetStrength(affliction.NonClampedStrength); newAffliction.SetStrength(affliction.NonClampedStrength);
} }
attacker?.CheckTalents(AbilityEffectType.OnAddDamageAffliction, newAffliction); if (attacker != null)
{
var abilityAffliction = new AbilityAffliction(newAffliction);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction);
}
if (applyAffliction) if (applyAffliction)
{ {
afflictionsCopy.Add(newAffliction); afflictionsCopy.Add(newAffliction);
@@ -20,7 +20,7 @@ namespace Barotrauma.Abilities
character = characterTalent.Character; character = characterTalent.Character;
invert = conditionElement.GetAttributeBool("invert", false); invert = conditionElement.GetAttributeBool("invert", false);
} }
public abstract bool MatchesCondition(object abilityData); public abstract bool MatchesCondition(AbilityObject abilityObject);
public abstract bool MatchesCondition(); public abstract bool MatchesCondition();
@@ -31,9 +31,9 @@ namespace Barotrauma.Abilities
} }
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is AbilityAttackData attackData) if (abilityObject is AbilityAttackData attackData)
{ {
Item item = attackData?.SourceAttack?.SourceItem; Item item = attackData?.SourceAttack?.SourceItem;
@@ -71,7 +71,7 @@ namespace Barotrauma.Abilities
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(AbilityAttackData)); LogAbilityConditionError(abilityObject, typeof(AbilityAttackData));
return false; return false;
} }
} }
@@ -15,9 +15,9 @@ namespace Barotrauma.Abilities
afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true);
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is AttackResult attackResult) if ((abilityObject as IAbilityAttackResult)?.AttackResult is AttackResult attackResult)
{ {
if (!IsViableTarget(targetTypes, attackResult.HitLimb?.character)) { return false; } if (!IsViableTarget(targetTypes, attackResult.HitLimb?.character)) { return false; }
@@ -30,7 +30,7 @@ namespace Barotrauma.Abilities
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(AbilityAttackData)); LogAbilityConditionError(abilityObject, typeof(IAbilityAttackResult));
return false; return false;
} }
} }
@@ -12,9 +12,9 @@ namespace Barotrauma.Abilities
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true));
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is Character character) if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{ {
if (!IsViableTarget(targetTypes, character)) { return false; } if (!IsViableTarget(targetTypes, character)) { return false; }
@@ -22,7 +22,7 @@ namespace Barotrauma.Abilities
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(Character)); LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter));
return false; return false;
} }
} }
@@ -16,21 +16,21 @@ namespace Barotrauma.Abilities
/// </summary> /// </summary>
public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected void LogAbilityConditionError<T>(T abilityData, Type expectedData) protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{ {
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityData}"); DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}");
} }
protected abstract bool MatchesConditionSpecific(object abilityData); protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition() public override bool MatchesCondition()
{ {
DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed."); DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed.");
return false; return false;
} }
public override bool MatchesCondition(object abilityData) public override bool MatchesCondition(AbilityObject abilityObject)
{ {
if (abilityData is null) { return invert; } if (abilityObject is null) { return invert; }
return invert ? !MatchesConditionSpecific(abilityData) : MatchesConditionSpecific(abilityData); return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject);
} }
} }
} }
@@ -6,15 +6,15 @@ namespace Barotrauma.Abilities
{ {
public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is Submarine submarine) if ((abilityObject as IAbilitySubmarine)?.Submarine is Submarine submarine && (abilityObject as IAbilityCharacter)?.Character is Character attackingCharacter)
{ {
return submarine.TeamID == character.TeamID && character.Submarine == submarine; return submarine.TeamID == character.TeamID && character.Submarine == submarine && attackingCharacter.TeamID != character.TeamID;
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(Submarine)); LogAbilityConditionError(abilityObject, typeof(IAbilitySubmarine));
return false; return false;
} }
} }
@@ -15,33 +15,33 @@ namespace Barotrauma.Abilities
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true); tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
ItemPrefab item = null; ItemPrefab itemPrefab = null;
if (abilityData is Item tempItem) if ((abilityObject as IAbilityItemPrefab)?.ItemPrefab is ItemPrefab abilityItemPrefab)
{ {
item = tempItem.Prefab; itemPrefab = abilityItemPrefab;
} }
else if (abilityData is IAbilityItemPrefab abilityItemPrefab) else if ((abilityObject as IAbilityItem)?.Item is Item abilityItem)
{ {
item = abilityItemPrefab.ItemPrefab; itemPrefab = abilityItem.Prefab;
} }
if (item != null) if (itemPrefab != null)
{ {
if (!string.IsNullOrEmpty(identifier)) if (!string.IsNullOrEmpty(identifier))
{ {
if (item.Identifier != identifier) if (itemPrefab.Identifier != identifier)
{ {
return false; return false;
} }
} }
return tags.Any(t => item.Tags.Any(p => t == p)); return tags.Any(t => itemPrefab.Tags.Any(p => t == p));
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(Item)); LogAbilityConditionError(abilityObject, typeof(IAbilityItemPrefab));
return false; return false;
} }
} }
@@ -13,19 +13,19 @@ namespace Barotrauma.Abilities
identifier = conditionElement.GetAttributeString("identifier", ""); identifier = conditionElement.GetAttributeString("identifier", "");
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is IAbilityAffliction abilityAffliction) if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
{ {
if (allowedTypes.Find(c => c == abilityAffliction.Affliction.Prefab.AfflictionType) == null) { return false; } if (allowedTypes.Find(c => c == affliction.Prefab.AfflictionType) == null) { return false; }
if (!string.IsNullOrEmpty(identifier) && abilityAffliction.Affliction.Prefab.Identifier != identifier) { return false; } if (!string.IsNullOrEmpty(identifier) && affliction.Prefab.Identifier != identifier) { return false; }
return true; return true;
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(IAbilityAffliction)); LogAbilityConditionError(abilityObject, typeof(IAbilityAffliction));
return false; return false;
} }
} }
@@ -4,17 +4,18 @@ namespace Barotrauma.Abilities
{ {
class AbilityConditionScavenger : AbilityConditionData class AbilityConditionScavenger : AbilityConditionData
{ {
public AbilityConditionScavenger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } public AbilityConditionScavenger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is Item item) if ((abilityObject as IAbilityItem)?.Item is Item item)
{ {
return item.Submarine != character.Submarine; return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID;
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(Item)); LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false; return false;
} }
} }
@@ -0,0 +1,23 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionScrounger : AbilityConditionData
{
public AbilityConditionScrounger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine?.Info?.IsWreck ?? false;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -16,15 +16,15 @@ namespace Barotrauma.Abilities
return this.skillIdentifier == skillIdentifier; return this.skillIdentifier == skillIdentifier;
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if ((abilityData as string ?? (abilityData as IAbilityString)?.String) is string skillIdentifier) if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
{ {
return MatchesConditionSpecific(skillIdentifier); return MatchesConditionSpecific(skillIdentifier);
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(string)); LogAbilityConditionError(abilityObject, typeof(IAbilityString));
return false; return false;
} }
} }
@@ -12,7 +12,7 @@ namespace Barotrauma.Abilities
return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific(); return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific();
} }
public override bool MatchesCondition(object abilityData) public override bool MatchesCondition(AbilityObject abilityObject)
{ {
return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific(); return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific();
} }
@@ -22,15 +22,15 @@ namespace Barotrauma.Abilities
} }
} }
protected override bool MatchesConditionSpecific(object abilityData) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityData is IAbilityMission abilityMission) if ((abilityObject as IAbilityMission)?.Mission is Mission mission)
{ {
return abilityMission.Mission.Prefab.Type == missionType; return mission.Prefab.Type == missionType;
} }
else else
{ {
LogAbilityConditionError(abilityData, typeof(IAbilityMission)); LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false; return false;
} }
} }
@@ -5,6 +5,11 @@
public ItemPrefab ItemPrefab { get; set; } public ItemPrefab ItemPrefab { get; set; }
} }
interface IAbilityItem
{
public Item Item { get; set; }
}
interface IAbilityValue interface IAbilityValue
{ {
public float Value { get; set; } public float Value { get; set; }
@@ -29,4 +34,14 @@
{ {
public Affliction Affliction { get; set; } public Affliction Affliction { get; set; }
} }
interface IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
}
interface IAbilitySubmarine
{
public Submarine Submarine { get; set; }
}
} }
@@ -2,8 +2,30 @@
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
class AbilityObject
{
// kept as blank for now, as we are using a composition and only using this object to enforce parameter types
}
class AbilityValue : IAbilityValue class AbilityCharacter : AbilityObject, IAbilityCharacter
{
public AbilityCharacter(Character character)
{
Character = character;
}
public Character Character { get; set; }
}
class AbilityItem : AbilityObject, IAbilityItem
{
public AbilityItem(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
class AbilityValue : AbilityObject, IAbilityValue
{ {
public AbilityValue(float value) public AbilityValue(float value)
{ {
@@ -12,7 +34,16 @@ namespace Barotrauma.Abilities
public float Value { get; set; } public float Value { get; set; }
} }
class AbilityValueItem : IAbilityValue, IAbilityItemPrefab class AbilityAffliction : AbilityObject, IAbilityAffliction
{
public AbilityAffliction(Affliction affliction)
{
Affliction = affliction;
}
public Affliction Affliction { get; set; }
}
class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab
{ {
public AbilityValueItem(float value, ItemPrefab itemPrefab) public AbilityValueItem(float value, ItemPrefab itemPrefab)
{ {
@@ -23,7 +54,7 @@ namespace Barotrauma.Abilities
public ItemPrefab ItemPrefab { get; set; } public ItemPrefab ItemPrefab { get; set; }
} }
class AbilityValueString : IAbilityValue, IAbilityString class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString
{ {
public AbilityValueString(float value, string abilityString) public AbilityValueString(float value, string abilityString)
{ {
@@ -34,7 +65,20 @@ namespace Barotrauma.Abilities
public string String { get; set; } public string String { get; set; }
} }
class AbilityStringCharacter : IAbilityCharacter, IAbilityString class AbilityValueStringCharacter : AbilityObject, IAbilityValue, IAbilityString
{
public AbilityValueStringCharacter(float value, string abilityString, Character character)
{
Value = value;
String = abilityString;
Character = character;
}
public Character Character { get; set; }
public float Value { get; set; }
public string String { get; set; }
}
class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString
{ {
public AbilityStringCharacter(string abilityString, Character character) public AbilityStringCharacter(string abilityString, Character character)
{ {
@@ -45,7 +89,7 @@ namespace Barotrauma.Abilities
public string String { get; set; } public string String { get; set; }
} }
class AbilityValueAffliction : IAbilityValue, IAbilityAffliction class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction
{ {
public AbilityValueAffliction(float value, Affliction affliction) public AbilityValueAffliction(float value, Affliction affliction)
{ {
@@ -56,7 +100,7 @@ namespace Barotrauma.Abilities
public Affliction Affliction { get; set; } public Affliction Affliction { get; set; }
} }
class AbilityValueMission : IAbilityValue, IAbilityMission class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission
{ {
public AbilityValueMission(float value, Mission mission) public AbilityValueMission(float value, Mission mission)
{ {
@@ -67,7 +111,8 @@ namespace Barotrauma.Abilities
public Mission Mission { get; set; } public Mission Mission { get; set; }
} }
class AbilityAttackData : IAbilityCharacter // this is an exception class that should only be passed in this form, so classes that use it should cast into it directly
class AbilityAttackData : AbilityObject, IAbilityCharacter
{ {
public float DamageMultiplier { get; set; } = 1f; public float DamageMultiplier { get; set; } = 1f;
public float AddedPenetration { get; set; } = 0f; public float AddedPenetration { get; set; } = 0f;
@@ -82,4 +127,26 @@ namespace Barotrauma.Abilities
Character = character; Character = character;
} }
} }
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
public AbilityAttackResult(AttackResult attackResult)
{
AttackResult = attackResult;
}
}
class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine
{
public AbilityCharacterSubmarine(Character character, Submarine submarine)
{
Character = character;
Submarine = submarine;
}
public Character Character { get; set; }
public Submarine Submarine { get; set; }
}
} }
@@ -65,15 +65,15 @@ namespace Barotrauma.Abilities
DebugConsole.ThrowError($"Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); DebugConsole.ThrowError($"Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
} }
public void ApplyAbilityEffect(object abilityData) public void ApplyAbilityEffect(AbilityObject abilityObject)
{ {
if (abilityData is null) if (abilityObject is null)
{ {
ApplyEffect(); ApplyEffect();
} }
else else
{ {
ApplyEffect(abilityData); ApplyEffect(abilityObject);
} }
} }
@@ -82,12 +82,12 @@ namespace Barotrauma.Abilities
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect"); DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect");
} }
protected virtual void ApplyEffect(object abilityData) protected virtual void ApplyEffect(AbilityObject abilityObject)
{ {
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect"); DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect");
} }
protected void LogAbilityDataMismatch() protected void LogabilityObjectMismatch()
{ {
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type."); DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type.");
} }
@@ -34,32 +34,33 @@ namespace Barotrauma.Abilities
{ {
targets.Clear(); targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets));
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
} }
else if (statusEffect.HasTargetType(StatusEffect.TargetType.This)) else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
else
{ {
statusEffect.SetUser(Character); statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
} }
else
{
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
} }
} }
protected override void ApplyEffect() protected override void ApplyEffect()
{ {
ApplyEffectSpecific(Character); ApplyEffectSpecific(Character);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (applyToSelected && Character.SelectedCharacter is Character selectedCharacter) if (applyToSelected && Character.SelectedCharacter is Character selectedCharacter)
{ {
ApplyEffectSpecific(selectedCharacter); ApplyEffectSpecific(selectedCharacter);
} }
else if ((abilityData as Character ?? (abilityData as IAbilityCharacter)?.Character) is Character targetCharacter) else if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
{ {
ApplyEffectSpecific(targetCharacter); ApplyEffectSpecific(targetCharacter);
} }
@@ -9,9 +9,9 @@ namespace Barotrauma.Abilities
{ {
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if ((abilityData as AbilityAttackData)?.Attacker is Character attacker) if ((abilityObject as AbilityAttackData)?.Attacker is Character attacker)
{ {
ApplyEffectSpecific(attacker); ApplyEffectSpecific(attacker);
} }
@@ -0,0 +1,27 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{
private string skillIdentifier;
public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityValue)?.Value is float skillIncrease)
{
Character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease, Character.Position + Vector2.UnitY * 175.0f);
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -27,9 +27,9 @@ namespace Barotrauma.Abilities
targetCharacter.GiveMoney((int)(multiplier * amount)); targetCharacter.GiveMoney((int)(multiplier * amount));
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if ((abilityData as Character ?? (abilityData as IAbilityCharacter)?.Character) is Character targetCharacter) if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
{ {
ApplyEffectSpecific(targetCharacter); ApplyEffectSpecific(targetCharacter);
} }
@@ -12,6 +12,8 @@ namespace Barotrauma.Abilities
private readonly bool targetAllies; private readonly bool targetAllies;
private readonly bool removeOnDeath; private readonly bool removeOnDeath;
private readonly bool removeAfterRound; private readonly bool removeAfterRound;
private readonly bool giveOnAddingFirstTime;
//private readonly float maximumValue; //private readonly float maximumValue;
public override bool AppliesEffectOnIntervalUpdate => true; public override bool AppliesEffectOnIntervalUpdate => true;
@@ -25,10 +27,19 @@ namespace Barotrauma.Abilities
targetAllies = abilityElement.GetAttributeBool("targetallies", false); targetAllies = abilityElement.GetAttributeBool("targetallies", false);
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true);
removeAfterRound = abilityElement.GetAttributeBool("removeafterround", false); removeAfterRound = abilityElement.GetAttributeBool("removeafterround", false);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", false);
//maximumValue = abilityElement.GetAttributeFloat("maximumvalue", float.MaxValue); //maximumValue = abilityElement.GetAttributeFloat("maximumvalue", float.MaxValue);
} }
protected override void ApplyEffect(object abilityData) public override void InitializeAbility(bool addingFirstTime)
{
if (giveOnAddingFirstTime && addingFirstTime)
{
ApplyEffectSpecific();
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{ {
ApplyEffectSpecific(); ApplyEffectSpecific();
} }
@@ -0,0 +1,23 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveTalentPoints : CharacterAbility
{
private readonly int amount;
public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (addingFirstTime && Character.Info != null)
{
Character.Info.AdditionalTalentPoints += amount;
}
}
}
}
@@ -21,9 +21,9 @@ namespace Barotrauma.Abilities
ApplyEffectSpecific(Character); ApplyEffectSpecific(Character);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is Character character) if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{ {
ApplyEffectSpecific(character); ApplyEffectSpecific(character);
} }
@@ -15,9 +15,9 @@ namespace Barotrauma.Abilities
addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is Affliction affliction) if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
{ {
foreach (string afflictionIdentifier in afflictionIdentifiers) foreach (string afflictionIdentifier in afflictionIdentifiers)
{ {
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
} }
else else
{ {
LogAbilityDataMismatch(); LogabilityObjectMismatch();
} }
} }
} }
@@ -22,9 +22,9 @@ namespace Barotrauma.Abilities
implode = abilityElement.GetAttributeBool("implode", false); implode = abilityElement.GetAttributeBool("implode", false);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is AbilityAttackData attackData) if (abilityObject is AbilityAttackData attackData)
{ {
if (attackData.Afflictions == null) if (attackData.Afflictions == null)
{ {
@@ -46,7 +46,7 @@ namespace Barotrauma.Abilities
} }
else else
{ {
LogAbilityDataMismatch(); LogabilityObjectMismatch();
} }
} }
} }
@@ -12,15 +12,15 @@ namespace Barotrauma.Abilities
addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f); addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is (Affliction affliction, float reduceAmount)) if (abilityObject is AbilityValueAffliction afflictionReduceAmount)
{ {
affliction.Strength -= addedAmountMultiplier * reduceAmount; afflictionReduceAmount.Affliction.Strength -= addedAmountMultiplier * afflictionReduceAmount.Value;
} }
else else
{ {
LogAbilityDataMismatch(); LogabilityObjectMismatch();
} }
} }
} }
@@ -13,9 +13,9 @@ namespace Barotrauma.Abilities
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is IAbilityValue abilityValue) if (abilityObject is IAbilityValue abilityValue)
{ {
abilityValue.Value += addedValue; abilityValue.Value += addedValue;
abilityValue.Value *= multiplyValue; abilityValue.Value *= multiplyValue;
@@ -11,7 +11,7 @@ namespace Barotrauma.Abilities
{ {
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
ApplyEffectSpecific(); ApplyEffectSpecific();
} }
@@ -21,7 +21,7 @@ namespace Barotrauma.Abilities
ApplyEffectSpecific(); ApplyEffectSpecific();
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
ApplyEffectSpecific(); ApplyEffectSpecific();
} }
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilitySpawnItemsToContainer : CharacterAbility
{
// currently used only for spawning items to containers
private readonly List<StatusEffect> statusEffects;
private readonly List<Item> openedContainers = new List<Item>();
private readonly float randomChance;
public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
randomChance = abilityElement.GetAttributeFloat("randomchance", 1f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
if (openedContainers.Contains(item)) { return; }
openedContainers.Add(item);
if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; }
foreach (var statusEffect in statusEffects)
{
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, item, item);
}
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -18,9 +18,9 @@ namespace Barotrauma.Abilities
tags = abilityElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true); tags = abilityElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is AbilityAttackData attackData) if (abilityObject is AbilityAttackData attackData)
{ {
float totalAddedDamageMultiplier = 0f; float totalAddedDamageMultiplier = 0f;
foreach (Item item in Character.Inventory.AllItems) foreach (Item item in Character.Inventory.AllItems)
@@ -34,7 +34,7 @@ namespace Barotrauma.Abilities
} }
else else
{ {
LogAbilityDataMismatch(); LogabilityObjectMismatch();
} }
} }
} }
@@ -10,11 +10,11 @@ namespace Barotrauma.Abilities
{ {
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is AbilityStringCharacter abilityStringCharacter && abilityStringCharacter.Character != Character) if ((abilityObject as IAbilityString)?.String is string skillIdentifier && (abilityObject as IAbilityCharacter)?.Character is Character character)
{ {
Character.Info?.IncreaseSkillLevel(abilityStringCharacter.String, 1.0f, abilityStringCharacter.Character.Position + Vector2.UnitY * 175.0f); Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, character.Position + Vector2.UnitY * 175.0f);
} }
} }
} }
@@ -12,9 +12,9 @@ namespace Barotrauma.Abilities
vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f); vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is Character character) if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{ {
Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality)); Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality));
} }
@@ -11,9 +11,9 @@ namespace Barotrauma.Abilities
{ {
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is string skillIdentifier) if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
{ {
if (skillIdentifier != lastSkillIdentifier) if (skillIdentifier != lastSkillIdentifier)
{ {
@@ -8,19 +8,26 @@ namespace Barotrauma.Abilities
{ {
class CharacterAbilityRegenerateLoot : CharacterAbility class CharacterAbilityRegenerateLoot : CharacterAbility
{ {
// separate random chance used for the ability itself to prevent the player
// from opening/reopening a container until it spawns loot
private readonly float randomChance;
// not maintained through death, so it's possible for players to respawn and re-loot chests // not maintained through death, so it's possible for players to respawn and re-loot chests
// seems like a minor issue for now // seems like a minor issue for now
List<Item> openedContainers = new List<Item>(); private readonly List<Item> openedContainers = new List<Item>();
public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
randomChance = abilityElement.GetAttributeFloat("randomchance", 1f);
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is Item item && !openedContainers.Contains(item)) if ((abilityObject as IAbilityItem)?.Item is Item item)
{ {
if (openedContainers.Contains(item)) { return; }
openedContainers.Add(item); openedContainers.Add(item);
if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; }
if (item.GetComponent<ItemContainer>() is ItemContainer itemContainer) if (item.GetComponent<ItemContainer>() is ItemContainer itemContainer)
{ {
@@ -16,9 +16,9 @@ namespace Barotrauma.Abilities
statusEffectsRemove = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsremove")); statusEffectsRemove = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsremove"));
} }
protected override void ApplyEffect(object abilityData) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityData is Character targetCharacter) if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
{ {
if (targetCharacter == Character) { return; } if (targetCharacter == Character) { return; }
@@ -10,25 +10,25 @@ namespace Barotrauma.Abilities
{ {
public CharacterAbilityGroupEffect(CharacterTalent characterTalent, XElement abilityElementGroup) : base(characterTalent, abilityElementGroup) { } public CharacterAbilityGroupEffect(CharacterTalent characterTalent, XElement abilityElementGroup) : base(characterTalent, abilityElementGroup) { }
public void CheckAbilityGroup(object abilityData) public void CheckAbilityGroup(AbilityObject abilityObject)
{ {
if (!IsActive) { return; } if (!IsActive) { return; }
if (IsApplicable(abilityData)) if (IsApplicable(abilityObject))
{ {
foreach (var characterAbility in characterAbilities) foreach (var characterAbility in characterAbilities)
{ {
if (characterAbility.IsViable()) if (characterAbility.IsViable())
{ {
characterAbility.ApplyAbilityEffect(abilityData); characterAbility.ApplyAbilityEffect(abilityObject);
} }
} }
} }
} }
private bool IsApplicable(object abilityData) private bool IsApplicable(AbilityObject abilityObject)
{ {
if (timesTriggered >= maxTriggerCount) { return false; } if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition(abilityData)); return abilityConditions.All(c => c.MatchesCondition(abilityObject));
} }
} }
} }
@@ -60,13 +60,13 @@ namespace Barotrauma
} }
} }
public void CheckTalent(AbilityEffectType abilityEffectType, object abilityData) public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{ {
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups)) if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups))
{ {
foreach (var characterAbilityGroup in characterAbilityGroups) foreach (var characterAbilityGroup in characterAbilityGroups)
{ {
characterAbilityGroup.CheckAbilityGroup(abilityData); characterAbilityGroup.CheckAbilityGroup(abilityObject);
} }
} }
} }
@@ -420,7 +420,9 @@ namespace Barotrauma
default: default:
try try
{ {
XDocument.Load(file.Path); using FileStream stream = File.Open(file.Path, System.IO.FileMode.Open, System.IO.FileAccess.Read);
using var reader = XMLExtensions.CreateReader(stream);
XDocument.Load(reader);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -57,6 +57,7 @@
OnGainMissionMoney, OnGainMissionMoney,
OnItemDeconstructed, OnItemDeconstructed,
OnItemDeconstructedMaterial, OnItemDeconstructedMaterial,
OnStopTinkering,
AfterSubmarineAttacked, AfterSubmarineAttacked,
} }
@@ -89,6 +90,7 @@
// Utility // Utility
RepairSpeed, RepairSpeed,
DeconstructorSpeedMultiplier, DeconstructorSpeedMultiplier,
TinkeringDuration,
// Misc // Misc
ReputationGainMultiplier, ReputationGainMultiplier,
MissionMoneyGainMultiplier, MissionMoneyGainMultiplier,
@@ -108,6 +110,8 @@
IgnoredByEnemyAI, IgnoredByEnemyAI,
MoveNormallyWhileDragging, MoveNormallyWhileDragging,
CanTinker, CanTinker,
CanTinkerFabricatorsAndDeconstructors,
TinkeringPowersDevices,
GainSkillPastMaximum, GainSkillPastMaximum,
RetainExperienceForNewCharacter RetainExperienceForNewCharacter
} }
@@ -122,7 +122,7 @@ namespace Barotrauma
{ {
npcOrItem = npc; npcOrItem = npc;
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine; npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
npc.RequireConsciousnessForCustomInteract = false; npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated;
#if CLIENT #if CLIENT
npc.SetCustomInteract( npc.SetCustomInteract(
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
@@ -103,7 +103,7 @@ namespace Barotrauma
selectedEvents.Clear(); selectedEvents.Clear();
activeEvents.Clear(); activeEvents.Clear();
pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false); pathFinder = new PathFinder(WayPoint.WayPointList, false);
totalPathLength = 0.0f; totalPathLength = 0.0f;
if (level != null) if (level != null)
{ {
@@ -140,6 +140,12 @@ namespace Barotrauma
public override int GetReward(Submarine sub) public override int GetReward(Submarine sub)
{ {
// If we are not at the location of the mission, skip the calculation of the reward
if (GameMain.GameSession?.StartLocation != Locations[0])
{
return calculatedReward;
}
bool missionsChanged = false; bool missionsChanged = false;
if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) if (GameMain.GameSession?.StartLocation?.SelectedMissions != null)
{ {
@@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using Barotrauma.IO;
using System.Linq; using System.Linq;
namespace Barotrauma namespace Barotrauma
@@ -16,7 +16,7 @@ namespace Barotrauma
{ {
forbiddenWords = File.ReadAllLines(fileListPath).Select(s => s.ToLowerInvariant()).ToHashSet(); forbiddenWords = File.ReadAllLines(fileListPath).Select(s => s.ToLowerInvariant()).ToHashSet();
} }
catch (IOException e) catch (System.IO.IOException e)
{ {
DebugConsole.ThrowError($"Failed to load the list of forbidden words from {fileListPath}.", e); DebugConsole.ThrowError($"Failed to load the list of forbidden words from {fileListPath}.", e);
} }
@@ -1,12 +1,11 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; 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.Networking;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -17,11 +16,17 @@ namespace Barotrauma
// Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down
public static CampaignSettings Unsure = Empty; public static CampaignSettings Unsure = Empty;
public bool RadiationEnabled { get; set; } public bool RadiationEnabled { get; set; }
public int MaxMissionCount { get; set; }
public int AddedMissionCount { get; set; } public int AddedMissionCount { get; set; }
public int TotalMaxMissionCount => MaxMissionCount + AddedMissionCount; public int TotalMaxMissionCount => MaxMissionCount + AddedMissionCount;
private int maxMissionCount;
public int MaxMissionCount
{
get { return maxMissionCount; }
set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); }
}
public const int DefaultMaxMissionCount = 2; public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10; public const int MaxMissionCountLimit = 10;
@@ -29,16 +34,18 @@ namespace Barotrauma
public CampaignSettings(IReadMessage inc) public CampaignSettings(IReadMessage inc)
{ {
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = inc.ReadBoolean(); RadiationEnabled = inc.ReadBoolean();
MaxMissionCount = inc.ReadInt32();
AddedMissionCount = inc.ReadInt32(); AddedMissionCount = inc.ReadInt32();
MaxMissionCount = inc.ReadInt32();
} }
public CampaignSettings(XElement element) public CampaignSettings(XElement element)
{ {
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true); RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true);
MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount);
AddedMissionCount = element.GetAttributeInt(nameof(AddedMissionCount).ToLowerInvariant(), 0); AddedMissionCount = element.GetAttributeInt(nameof(AddedMissionCount).ToLowerInvariant(), 0);
MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount);
} }
public void Serialize(IWriteMessage msg) public void Serialize(IWriteMessage msg)
@@ -659,9 +659,9 @@ namespace Barotrauma
public static IEnumerable<Character> GetSessionCrewCharacters() public static IEnumerable<Character> GetSessionCrewCharacters()
{ {
#if SERVER #if SERVER
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c.Info != null); return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null);
#else #else
return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c.Info != null); return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null);
#endif #endif
} }
@@ -291,11 +291,25 @@ namespace Barotrauma
{ {
currentSlot = i; currentSlot = i;
if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
inSuitableSlot = true; {
if ((SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) && !allowedSlots.Contains(SlotTypes[i]))
{
//allowed slot = InvSlotType.RightHand | InvSlotType.LeftHand
// -> make sure the item is in both hand slots
inSuitableSlot = IsInLimbSlot(item, InvSlotType.RightHand) && IsInLimbSlot(item, InvSlotType.LeftHand);
}
else
{
inSuitableSlot = true;
}
}
else if (!allowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) else if (!allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
{
inWrongSlot = true; inWrongSlot = true;
}
} }
} }
//all good //all good
if (inSuitableSlot && !inWrongSlot) { return true; } if (inSuitableSlot && !inWrongSlot) { return true; }
@@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components
private bool isBroken; private bool isBroken;
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck; public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable;
public bool IsBroken public bool IsBroken
{ {
@@ -117,6 +117,10 @@ namespace Barotrauma.Items.Components
item.CreateServerEvent(this); item.CreateServerEvent(this);
#endif #endif
} }
foreach (Item containedItem in item.ContainedItems)
{
containedItem.GetComponent<GeneticMaterial>()?.Equip(character);
}
} }
public override void Update(float deltaTime, Camera cam) public override void Update(float deltaTime, Camera cam)
@@ -124,12 +128,16 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam); base.Update(deltaTime, cam);
if (targetCharacter != null) if (targetCharacter != null)
{ {
var rootContainer = item.GetRootContainer();
if (!targetCharacter.HasEquippedItem(item) && if (!targetCharacter.HasEquippedItem(item) &&
(item.Container == null || !targetCharacter.HasEquippedItem(item.Container) || !(item.Container.GetComponent<ItemContainer>()?.AutoInject ?? false))) (rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{ {
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter); item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter);
var currentEffect = tainted ? selectedTaintedEffect : selectedEffect; targetCharacter.CharacterHealth.ReduceAffliction(null, selectedEffect.Identifier, selectedEffect.MaxStrength);
targetCharacter.CharacterHealth.ReduceAffliction(null, currentEffect.Identifier, currentEffect.MaxStrength); if (tainted)
{
targetCharacter.CharacterHealth.ReduceAffliction(null, selectedTaintedEffect.Identifier, selectedTaintedEffect.MaxStrength);
}
targetCharacter = null; targetCharacter = null;
IsActive = false; IsActive = false;
} }
@@ -1,4 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Abilities;
using Barotrauma.Networking;
using FarseerPhysics; using FarseerPhysics;
using FarseerPhysics.Collision; using FarseerPhysics.Collision;
using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics;
@@ -158,10 +159,14 @@ namespace Barotrauma.Items.Components
if (currentChargeTime < MaxChargeTime) { return false; } if (currentChargeTime < MaxChargeTime) { return false; }
IsActive = true; IsActive = true;
ReloadTimer = reload / (1 + character.GetStatValue(StatTypes.RangedAttackSpeed)); ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
currentChargeTime = 0f; currentChargeTime = 0f;
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, item); if (character != null)
{
var abilityItem = new AbilityItem(item);
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityItem);
}
if (item.AiTarget != null) if (item.AiTarget != null)
{ {
@@ -6,6 +6,7 @@ using System.Xml.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using FarseerPhysics; using FarseerPhysics;
using System.Collections.Immutable; using System.Collections.Immutable;
using Barotrauma.Abilities;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
@@ -114,6 +115,13 @@ namespace Barotrauma.Items.Components
set; set;
} }
[Serialize(true, false)]
public bool AllowSwappingContainedItems
{
get;
set;
}
[Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] [Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
public bool AutoInteractWithContained public bool AutoInteractWithContained
{ {
@@ -414,7 +422,8 @@ namespace Barotrauma.Items.Components
} }
} }
} }
character.CheckTalents(AbilityEffectType.OnOpenItemContainer, item); var abilityItem = new AbilityItem(item);
character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem);
return base.Select(character); return base.Select(character);
} }
@@ -588,6 +597,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded() public override void OnItemLoaded()
{ {
Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems;
containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<string>()).ToImmutableHashSet(); containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<string>()).ToImmutableHashSet();
if (item.Submarine == null || !item.Submarine.Loading) if (item.Submarine == null || !item.Submarine.Loading)
{ {
@@ -616,7 +626,21 @@ namespace Barotrauma.Items.Components
} }
itemIds = null; itemIds = null;
} }
SpawnAlwaysContainedItems();
//outpost and ruins are loaded in multiple stages (each module is loaded separately)
//spawning items at this point during the generation will cause ID overlaps with the entities in the modules loaded afterwards
//so let's not spawn them at this point, but in the 1st Update()
if (item.Submarine?.Info != null && (item.Submarine.Info.IsOutpost || item.Submarine.Info.IsRuin))
{
if (SpawnWithId.Length > 0)
{
IsActive = true;
}
}
else
{
SpawnAlwaysContainedItems();
}
} }
private void SpawnAlwaysContainedItems() private void SpawnAlwaysContainedItems()

Some files were not shown because too many files have changed in this diff Show More