(18eb9f712) Fix bots stealing items if they happen to target them before somebody equips them.

This commit is contained in:
Joonas Rikkonen
2019-05-16 06:19:10 +03:00
parent df89114850
commit e8ee3f9d18
27 changed files with 338 additions and 421 deletions
@@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.9.0.0")] [assembly: AssemblyVersion("0.8.10.0")]
[assembly: AssemblyFileVersion("0.9.0.0")] [assembly: AssemblyFileVersion("0.8.10.0")]
@@ -61,14 +61,6 @@ namespace Barotrauma
var path = pathSteering.CurrentPath; var path = pathSteering.CurrentPath;
if (path != null) if (path != null)
{ {
if (path.CurrentNode != null)
{
GUI.DrawLine(spriteBatch, pos,
new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y),
Color.BlueViolet, 0, 3);
GUI.DrawString(spriteBatch, pos + textOffset - new Vector2(0, 20), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f);
}
for (int i = 1; i < path.Nodes.Count; i++) for (int i = 1; i < path.Nodes.Count; i++)
{ {
var previousNode = path.Nodes[i - 1]; var previousNode = path.Nodes[i - 1];
@@ -83,6 +75,14 @@ namespace Barotrauma
new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30),
Color.Blue); Color.Blue);
} }
if (path.CurrentNode != null)
{
GUI.DrawLine(spriteBatch, pos,
new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y),
Color.BlueViolet, 0, 3);
GUI.DrawString(spriteBatch, pos + textOffset - new Vector2(0, 20), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f);
}
} }
} }
GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2); GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2);
@@ -121,90 +121,7 @@ namespace Barotrauma
MainLimb.PullJointWorldAnchorB = Collider.SimPosition; MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true; MainLimb.PullJointEnabled = true;
} }
character.SelectedConstruction = character.MemState[0].SelectedItem;
} }
if (character.MemState[0].Animation == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
else if (character.AnimController.Anim == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.None;
}
Vector2 newVelocity = Collider.LinearVelocity;
Vector2 newPosition = Collider.SimPosition;
float newRotation = Collider.Rotation;
float newAngularVelocity = Collider.AngularVelocity;
Collider.CorrectPosition(character.MemState, out newPosition, out newVelocity, out newRotation, out newAngularVelocity);
newVelocity = newVelocity.ClampLength(100.0f);
if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; }
overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero;
Collider.LinearVelocity = newVelocity;
Collider.AngularVelocity = newAngularVelocity;
float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition);
float errorTolerance = character.AllowInput ? 0.01f : 0.2f;
if (distSqrd > errorTolerance)
{
if (distSqrd > 10.0f || !character.AllowInput)
{
Collider.TargetRotation = newRotation;
SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false);
}
else
{
Collider.TargetRotation = newRotation;
Collider.TargetPosition = newPosition;
Collider.MoveToTargetPosition(true);
}
}
//unconscious/dead characters can't correct their position using AnimController movement
// -> we need to correct it manually
if (!character.AllowInput)
{
float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, Collider.SimPosition);
float mainLimbErrorTolerance = 0.1f;
//if the main limb is roughly at the correct position and the collider isn't moving (much at least),
//don't attempt to correct the position.
if (mainLimbDistSqrd > mainLimbErrorTolerance || Collider.LinearVelocity.LengthSquared() > 0.05f)
{
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true;
}
}
}
character.MemLocalState.Clear();
}
else
{
//remove states with a timestamp (there may still timestamp-based states
//in the list if the controlled character switches from timestamp-based interpolation to ID-based)
character.MemState.RemoveAll(m => m.Timestamp > 0.0f);
for (int i = 0; i < character.MemLocalState.Count; i++)
{
if (character.Submarine == null)
{
//transform in-sub coordinates to outside coordinates
if (character.MemLocalState[i].Position.Y > lowestSubPos)
{
character.MemLocalState[i].TransformInToOutside();
}
}
else if (currentHull?.Submarine != null)
{
//transform outside coordinates to in-sub coordinates
if (character.MemLocalState[i].Position.Y < lowestSubPos)
{
character.MemLocalState[i].TransformOutToInside(currentHull.Submarine);
}
}
} }
character.MemLocalState.Clear(); character.MemLocalState.Clear();
} }
@@ -74,17 +74,12 @@ namespace Barotrauma
public CrewManager(XElement element, bool isSinglePlayer) public CrewManager(XElement element, bool isSinglePlayer)
: this(isSinglePlayer) : this(isSinglePlayer)
{ {
if (GameMain.Client != null) if (!isSinglePlayer)
{ {
//let the server create random conversations in MP DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace);
return; return;
} }
List<Character> availableSpeakers = Character.CharacterList.FindAll(c => if (string.IsNullOrEmpty(text)) { return; }
c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
}
var characterInfo = new CharacterInfo(subElement); var characterInfo = new CharacterInfo(subElement);
characterInfos.Add(characterInfo); characterInfos.Add(characterInfo);
@@ -95,6 +90,7 @@ namespace Barotrauma
break; break;
} }
} }
ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender));
} }
partial void InitProjectSpecific() partial void InitProjectSpecific()
@@ -243,24 +239,27 @@ namespace Barotrauma
public IEnumerable<Character> GetCharacters() public IEnumerable<Character> GetCharacters()
{ {
if (characterInfos.Contains(characterInfo)) if (character?.Inventory == null) return null;
{
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace);
return;
}
characterInfos.Add(characterInfo); var radioItem = character.Inventory.Items.FirstOrDefault(it => it != null && it.GetComponent<WifiComponent>() != null);
if (radioItem == null) return null;
if (requireEquipped && !character.HasEquippedItem(radioItem)) return null;
return radioItem.GetComponent<WifiComponent>();
} }
public IEnumerable<CharacterInfo> GetCharacterInfos() public IEnumerable<CharacterInfo> GetCharacterInfos()
{ {
if (character == null) if (GameMain.Client != null)
{ {
DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace); //let the server create random conversations in MP
return; return;
} }
characters.Remove(character); List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
if (removeInfo) characterInfos.Remove(character.Info); c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
} }
public void AddCharacter(Character character) public void AddCharacter(Character character)
@@ -634,183 +633,9 @@ namespace Barotrauma
{ {
characterListBox.BarScroll = roundedPos; characterListBox.BarScroll = roundedPos;
} }
var characterArea = new GUIButton(new RectTransform(new Point(characterInfoWidth, frame.Rect.Height), frame.RectTransform, Anchor.CenterLeft), style: "GUITextBox") soundIcon.Visible = !muted && !mutedLocally;
{ soundIconDisabled.Visible = muted || mutedLocally;
UserData = character, soundIconDisabled.ToolTip = TextManager.Get(mutedLocally ? "MutedLocally" : "MutedGlobally");
Color = frame.Color,
SelectedColor = frame.SelectedColor,
HoverColor = frame.HoverColor,
ToolTip = characterToolTip
};
var soundIcon = new GUIImage(new RectTransform(new Point((int)(characterArea.Rect.Height * 0.5f)), characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) },
"GUISoundIcon")
{
UserData = "soundicon",
CanBeFocused = false,
Visible = true
};
soundIcon.Color = new Color(soundIcon.Color, 0.0f);
new GUIImage(new RectTransform(new Point((int)(characterArea.Rect.Height * 0.5f)), characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) },
"GUISoundIconDisabled")
{
UserData = "soundicondisabled",
CanBeFocused = true,
Visible = false
};
if (isSinglePlayer)
{
characterArea.OnClicked = CharacterClicked;
}
else
{
characterArea.CanBeFocused = false;
characterArea.CanBeSelected = false;
}
var characterImage = new GUICustomComponent(new RectTransform(new Point(characterArea.Rect.Height), characterArea.RectTransform, Anchor.CenterLeft),
onDraw: (sb, component) => character.Info.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2()))
{
CanBeFocused = false,
HoverColor = Color.White,
SelectedColor = Color.White,
ToolTip = characterToolTip
};
var characterName = new GUITextBlock(new RectTransform(new Point(characterArea.Rect.Width - characterImage.Rect.Width - soundIcon.Rect.Width - 10, characterArea.Rect.Height),
characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(soundIcon.Rect.Width + 10, 0) },
character.Name, textColor: frame.Color, font: GUI.SmallFont, wrap: true)
{
Color = frame.Color,
HoverColor = Color.Transparent,
SelectedColor = Color.Transparent,
CanBeFocused = false,
ToolTip = characterToolTip,
AutoScale = true
};
//---------------- order buttons ----------------
var orderButtonFrame = new GUILayoutGroup(new RectTransform(new Point(100, frame.Rect.Height), frame.RectTransform)
{ AbsoluteOffset = new Point(characterInfoWidth + spacing, 0) },
isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
AbsoluteSpacing = (int)(10 * GUI.Scale),
UserData = "orderbuttons",
CanBeFocused = false
};
//listbox for holding the orders inappropriate for this character
//(so we can easily toggle their visibility)
var wrongOrderList = new GUIListBox(new RectTransform(new Point(50, orderButtonFrame.Rect.Height), orderButtonFrame.RectTransform), isHorizontal: true, style: null)
{
ScrollBarEnabled = false,
ScrollBarVisible = false,
Enabled = false,
Spacing = spacing,
ClampMouseRectToParent = false
};
wrongOrderList.Content.ClampMouseRectToParent = false;
for (int i = 0; i < orders.Count; i++)
{
var order = orders[i];
if (order.TargetAllCharacters) continue;
RectTransform btnParent = (i >= correctOrderCount + neutralOrderCount) ?
wrongOrderList.Content.RectTransform :
orderButtonFrame.RectTransform;
var btn = new GUIButton(new RectTransform(new Point(iconSize, iconSize), btnParent, Anchor.CenterLeft),
style: null)
{
UserData = order
};
new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlow")
{
Color = Color.Lerp(order.Color, frame.Color, 0.5f) * 0.8f,
HoverColor = Color.Lerp(order.Color, frame.Color, 0.5f) * 1.0f,
PressedColor = Color.Lerp(order.Color, frame.Color, 0.5f) * 0.6f,
UserData = "selected",
CanBeFocused = false,
Visible = false
};
var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), order.Prefab.SymbolSprite);
img.Scale = iconSize / (float)img.SourceRect.Width;
img.Color = Color.Lerp(order.Color, frame.Color, 0.5f);
img.ToolTip = order.Name;
img.HoverColor = Color.Lerp(img.Color, Color.White, 0.5f);
btn.OnClicked += (GUIButton button, object userData) =>
{
if (Character.Controlled == null || Character.Controlled.SpeechImpediment >= 100.0f) return false;
if (btn.GetChildByUserData("selected").Visible)
{
SetCharacterOrder(character, Order.PrefabList.Find(o => o.AITag == "dismissed"), null, Character.Controlled);
}
else
{
if (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0 || order.Options.Length > 1)
{
CreateOrderTargetFrame(button, character, order);
}
else
{
SetCharacterOrder(character, order, null, Character.Controlled);
}
}
return true;
};
btn.UserData = order;
btn.ToolTip = order.Name;
//divider between different groups of orders
if (i == correctOrderCount - 1 || i == correctOrderCount + neutralOrderCount - 1)
{
//TODO: divider sprite
new GUIFrame(new RectTransform(new Point(8, iconSize), orderButtonFrame.RectTransform), style: "GUIButton");
}
}
var toggleWrongOrderBtn = new GUIButton(new RectTransform(new Point((int)(30 * GUI.Scale), wrongOrderList.Rect.Height), wrongOrderList.Content.RectTransform),
"", style: "UIToggleButton")
{
UserData = "togglewrongorder",
CanBeFocused = false
};
wrongOrderList.RectTransform.NonScaledSize = new Point(
wrongOrderList.Content.Children.Sum(c => c.Rect.Width + wrongOrderList.Spacing),
wrongOrderList.RectTransform.NonScaledSize.Y);
wrongOrderList.RectTransform.SetAsLastChild();
new GUIFrame(new RectTransform(new Point(
wrongOrderList.Rect.Width - toggleWrongOrderBtn.Rect.Width - wrongOrderList.Spacing * 2,
wrongOrderList.Rect.Height), wrongOrderList.Content.RectTransform),
style: null)
{
CanBeFocused = false
};
//scale to fit the content
orderButtonFrame.RectTransform.NonScaledSize = new Point(
orderButtonFrame.Children.Sum(c => c.Rect.Width + orderButtonFrame.AbsoluteSpacing),
orderButtonFrame.RectTransform.NonScaledSize.Y);
frame.RectTransform.NonScaledSize = new Point(
characterInfoWidth + spacing + (orderButtonFrame.Rect.Width - wrongOrderList.Rect.Width),
frame.RectTransform.NonScaledSize.Y);
characterListBox.RectTransform.NonScaledSize = new Point(
characterListBox.Content.Children.Max(c => c.Rect.Width) + wrongOrderList.Rect.Width,
characterListBox.RectTransform.NonScaledSize.Y);
characterListBox.Content.RectTransform.NonScaledSize = characterListBox.RectTransform.NonScaledSize;
characterListBox.UpdateScrollBarSize();
return frame;
} }
private IEnumerable<object> KillCharacterAnim(GUIComponent component) private IEnumerable<object> KillCharacterAnim(GUIComponent component)
@@ -954,12 +779,6 @@ namespace Barotrauma
} }
return; return;
} }
List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
}
character.SetOrder(order, option, orderGiver, speak: orderGiver != character); character.SetOrder(order, option, orderGiver, speak: orderGiver != character);
if (IsSinglePlayer) if (IsSinglePlayer)
@@ -1017,23 +836,19 @@ namespace Barotrauma
} }
} }
} }
//only one target (or an order with no particular targets), just show options
character.SetOrder(order, option, orderGiver, speak: orderGiver != character); else
if (IsSinglePlayer)
{ {
orderGiver?.Speak( orderTargetFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.2f + order.Options.Length * 0.1f, 0.18f), GUI.Canvas)
order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); { AbsoluteOffset = new Point(orderButton.Rect.Center.X, orderButton.Rect.Bottom) },
} isHorizontal: true, childAnchor: Anchor.BottomLeft)
else if (orderGiver != null)
{
OrderChatMessage msg = new OrderChatMessage(order, option, order.TargetItemComponent?.Item, character, orderGiver);
if (GameMain.Client != null)
{ {
GameMain.Client.SendChatMessage(msg); UserData = character,
} Stretch = true
} };
DisplayCharacterOrder(character, order); //line connecting the order button to the option buttons
} //TODO: sprite
new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), orderTargetFrame.RectTransform), style: null);
/// <summary> /// <summary>
/// Create the UI panel that's used to select the target and options for a given order /// Create the UI panel that's used to select the target and options for a given order
@@ -478,6 +478,8 @@ namespace Barotrauma
private GUILayoutGroup subPreviewContainer; private GUILayoutGroup subPreviewContainer;
private GUILayoutGroup subPreviewContainer;
private GUIButton loadGameButton; private GUIButton loadGameButton;
public Action<Submarine, string, string> StartNewGame; public Action<Submarine, string, string> StartNewGame;
@@ -789,10 +789,6 @@ namespace Barotrauma
GUI.Draw(Cam, spriteBatch); GUI.Draw(Cam, spriteBatch);
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable);
GUI.Draw(Cam, spriteBatch);
GUI.Draw(Cam, spriteBatch); GUI.Draw(Cam, spriteBatch);
#if DEBUG #if DEBUG
@@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.9.0.0")] [assembly: AssemblyVersion("0.8.10.0")]
[assembly: AssemblyFileVersion("0.9.0.0")] [assembly: AssemblyFileVersion("0.8.10.0")]
+35
View File
@@ -31,6 +31,41 @@ Game port (used to communicate with clients)
Internal Port: The port you have selected for your server (27015 by default) Internal Port: The port you have selected for your server (27015 by default)
Protocol: UDP Protocol: UDP
Query port (used to communicate with Steam)
Service/Application: barotrauma
External Port: The port you have selected for your server (27016 by default)
Internal Port: The port you have selected for your server (27016 by default)
Protocol: UDP
------------------------------------------------------------------------
------------------------------------------------------------------------
Credits:
------------------------------------------------------------------------
Programming, graphics, sounds, game design - Joonas Rikkonen ("Regalis")
Graphics - James Bear ("Moonsaber99")
Programming - Sebastian Broberg
Programming - Juan Pablo Arce ("juanjp600")
------------------------------------------------------------------------
"Road to Hell"
Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 3.0
http://creativecommons.org/licenses/by/3.0/
------------------------------------------------------------------------
Lidgren.Network networking library - Michael Lidgren
------------------------------------------------------------------------
Sound effects:
metal whining and screeching - ScottFerguson1
metal gate 10 - thencamenow
foil_expanding_multiple - markb
Bug Zapper_Many medium zaps - CGEffex
Query port (used to communicate with Steam) Query port (used to communicate with Steam)
Service/Application: barotrauma Service/Application: barotrauma
External Port: The port you have selected for your server (27016 by default) External Port: The port you have selected for your server (27016 by default)
@@ -493,20 +493,25 @@ namespace Barotrauma
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage))); item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
} }
/// <summary> public static void DoForEachCrewMember(Character character, Action<HumanAIController> action)
/// Updates the hull safety for all ai characters in the team.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{ {
foreach (var c in Character.CharacterList) foreach (var c in Character.CharacterList)
{ {
if (c.AIController is HumanAIController humanAi && humanAi.IsFriendly(character)) if (c.AIController is HumanAIController humanAi && humanAi.IsFriendly(character))
{ {
humanAi.RefreshHullSafety(hull); action(humanAi);
} }
} }
} }
/// <summary>
/// Updates the hull safety for all ai characters in the team.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
private void RefreshHullSafety(Hull hull) private void RefreshHullSafety(Hull hull)
{ {
if (GetHullSafety(hull) > HULL_SAFETY_THRESHOLD) if (GetHullSafety(hull) > HULL_SAFETY_THRESHOLD)
@@ -577,21 +582,26 @@ namespace Barotrauma
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2> private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{ {
bool targetAdded = false; bool targetAdded = false;
foreach (var c in Character.CharacterList) DoForEachCrewMember(caller, humanAI =>
{ {
if (IsFriendly(caller, c) && c.AIController is HumanAIController humanAI) var objective = humanAI.ObjectiveManager.GetObjective<T1>();
if (objective != null)
{ {
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
if (objective == null) { continue; }
if (!targetAdded && objective.AddTarget(target)) if (!targetAdded && objective.AddTarget(target))
{ {
targetAdded = true; targetAdded = true;
} }
} }
} });
return targetAdded; return targetAdded;
} }
public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
DoForEachCrewMember(caller, humanAI =>
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
public float GetHullSafety(Hull hull) public float GetHullSafety(Hull hull)
{ {
if (hull == null) { return 0; } if (hull == null) { return 0; }
@@ -71,6 +71,7 @@ namespace Barotrauma
#if DEBUG #if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed."); DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.");
#endif #endif
subObjective.OnCompleted();
subObjectives.Remove(subObjective); subObjectives.Remove(subObjective);
} }
else if (!subObjective.CanBeCompleted) else if (!subObjective.CanBeCompleted)
@@ -93,9 +94,9 @@ namespace Barotrauma
bool wasCompleted = IsCompleted(); bool wasCompleted = IsCompleted();
Act(deltaTime); Act(deltaTime);
if (!wasCompleted && IsCompleted()) if (IsCompleted())
{ {
Completed?.Invoke(); OnCompleted();
} }
} }
@@ -213,6 +214,15 @@ namespace Barotrauma
//} //}
} }
protected virtual void OnCompleted()
{
if (Completed != null)
{
Completed();
Completed = null;
}
}
public virtual void Reset() { } public virtual void Reset() { }
protected abstract void Act(float deltaTime); protected abstract void Act(float deltaTime);
@@ -65,5 +65,8 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(PowerContainer battery) protected override AIObjective ObjectiveConstructor(PowerContainer battery)
=> new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false }; => new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false };
protected override void OnObjectiveCompleted(AIObjective objective, PowerContainer target)
=> HumanAIController.RemoveTargets<AIObjectiveChargeBatteries, PowerContainer>(character, target);
} }
} }
@@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System;
namespace Barotrauma namespace Barotrauma
{ {
@@ -20,7 +21,11 @@ namespace Barotrauma
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFires; public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFires;
protected override IEnumerable<Hull> GetList() => Hull.hullList; protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override AIObjective ObjectiveConstructor(Hull target) => new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier); protected override AIObjective ObjectiveConstructor(Hull target)
=> new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Hull target)
=> HumanAIController.RemoveTargets<AIObjectiveExtinguishFires, Hull>(character, target);
public static bool IsValidTarget(Hull hull, Character character) public static bool IsValidTarget(Hull hull, Character character)
{ {
@@ -21,14 +21,18 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList; protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
protected override float TargetEvaluation() protected override float TargetEvaluation()
{ {
// TODO: sorting criteria // TODO: sorting criteria
return 100; return 100;
} }
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
public static bool IsValidTarget(Character target, Character character) public static bool IsValidTarget(Character target, Character character)
{ {
if (target == null || target.IsDead || target.Removed) { return false; } if (target == null || target.IsDead || target.Removed) { return false; }
@@ -773,6 +773,32 @@ namespace Barotrauma
{ {
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character, objectiveManager) { CloseEnough = reach * 0.75f }); TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character, objectiveManager) { CloseEnough = reach * 0.75f });
} }
if (subObjectives.Any()) { return; }
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
abandon = true;
return;
}
Vector2 gapDiff = Leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
float reach = ConvertUnits.ToSimUnits(repairTool.Range);
bool canOperate = ConvertUnits.ToSimUnits(gapDiff.Length()) < reach * 1.5f;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak));
}
else
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character, objectiveManager) { CloseEnough = reach * 0.75f });
}
} }
private Vector2 GetStandPosition() private Vector2 GetStandPosition()
@@ -27,7 +27,11 @@ namespace Barotrauma
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks; public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks;
protected override float TargetEvaluation() => Targets.Max(t => GetLeakSeverity(t)); protected override float TargetEvaluation() => Targets.Max(t => GetLeakSeverity(t));
protected override IEnumerable<Gap> GetList() => Gap.GapList; protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap) => new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier); protected override AIObjective ObjectiveConstructor(Gap gap)
=> new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
public static bool IsValidTarget(Gap gap, Character character) public static bool IsValidTarget(Gap gap, Character character)
{ {
@@ -103,37 +103,44 @@ namespace Barotrauma
if (moveToTarget.CurrentHull == character.CurrentHull && if (moveToTarget.CurrentHull == character.CurrentHull &&
Vector2.DistanceSquared(character.Position, moveToTarget.Position) < MathUtils.Pow(targetItem.InteractDistance, 2)) Vector2.DistanceSquared(character.Position, moveToTarget.Position) < MathUtils.Pow(targetItem.InteractDistance, 2))
{ {
int targetSlot = -1; if (IsTakenBySomeone(targetItem))
if (equip)
{ {
var pickable = targetItem.GetComponent<Pickable>(); abandon = true;
if (pickable == null) }
else
{
int targetSlot = -1;
if (equip)
{ {
abandon = true; var pickable = targetItem.GetComponent<Pickable>();
return; if (pickable == null)
}
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{ {
//slot not needed by the item, continue abandon = true;
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; } return;
targetSlot = i; }
//slot free, continue //check if all the slots required by the item are free
if (character.Inventory.Items[i] == null) { continue; } foreach (InvSlotType slots in pickable.AllowedSlots)
//try to move the existing item to LimbSlot.Any and continue if successful {
if (character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any })) { continue; } if (slots.HasFlag(InvSlotType.Any)) { continue; }
//if everything else fails, simply drop the existing item for (int i = 0; i < character.Inventory.Items.Length; i++)
character.Inventory.Items[i].Drop(character); {
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
if (character.Inventory.Items[i] == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any })) { continue; }
//if everything else fails, simply drop the existing item
character.Inventory.Items[i].Drop(character);
}
} }
} }
} targetItem.TryInteract(character, false, true);
targetItem.TryInteract(character, false, true); if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
if (targetSlot > -1 && !character.HasEquippedItem(targetItem)) {
{ character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character); }
} }
} }
else else
@@ -185,23 +192,7 @@ namespace Barotrauma
{ {
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
} }
if (IsTakenBySomeone(item)) { continue; }
// TODO: need to exclude items that already are in the inventory?
//// Don't allow to get items in rooms that have a fire (except fire extinguishers) or an enemy inside (except weapons)
//if (!itemIdentifiers.Contains("extinguisher") && item.CurrentHull.FireSources.Count > 0 ||
// item.GetComponent<MeleeWeapon>() == null && item.GetComponent<RangedWeapon>() == null && Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { continue; }
//if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory)
{
if (item.ParentInventory.Owner is Character owner && !owner.IsDead) { continue; }
}
//if the item is inside an item, which is inside a character's inventory, don't steal it
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
if (rootContainer.ParentInventory.Owner is Character owner && !owner.IsDead) { continue; }
}
float itemPriority = 0.0f; float itemPriority = 0.0f;
if (GetItemPriority != null) if (GetItemPriority != null)
{ {
@@ -209,6 +200,7 @@ namespace Barotrauma
itemPriority = GetItemPriority(item); itemPriority = GetItemPriority(item);
if (itemPriority <= 0.0f) { continue; } if (itemPriority <= 0.0f) { continue; }
} }
Item rootContainer = item.GetRootContainer();
itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f; itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f;
//ignore if the item has a lower priority than the currently selected one //ignore if the item has a lower priority than the currently selected one
if (moveToTarget != null && itemPriority < currItemPriority) { continue; } if (moveToTarget != null && itemPriority < currItemPriority) { continue; }
@@ -271,10 +263,29 @@ namespace Barotrauma
{ {
bool isEquipped = !equip || character.HasEquippedItem(item); bool isEquipped = !equip || character.HasEquippedItem(item);
if (character.Inventory.Items.Contains(item) && isEquipped) { return true; } if (character.Inventory.Items.Contains(item) && isEquipped) { return true; }
if (!equip)
{
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
return rootContainer.ParentInventory.Owner == character;
}
}
return false;
}
public static bool IsTakenBySomeone(Item item)
{
//if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory)
{
if (item.ParentInventory.Owner is Character owner && !owner.IsDead) { return true; }
}
//if the item is inside an item, which is inside a character's inventory, don't steal it unless the character is dead
Item rootContainer = item.GetRootContainer(); Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory) if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{ {
return rootContainer.ParentInventory.Owner == character; if (rootContainer.ParentInventory.Owner is Character owner && !owner.IsDead) { return true; }
} }
return false; return false;
} }
@@ -73,21 +73,6 @@ namespace Barotrauma
} }
} }
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
{
if (randomTimer > 0)
{
randomTimer -= deltaTime;
}
else
{
SetRandom();
}
}
}
public override bool IsCompleted() => false; public override bool IsCompleted() => false;
public override bool CanBeCompleted => true; public override bool CanBeCompleted => true;
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@@ -87,7 +88,7 @@ namespace Barotrauma
} }
} }
// the timer is set between 1 and 10 seconds, depending on the priority // the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1); private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1);
public override void Reset() public override void Reset()
@@ -158,13 +159,18 @@ namespace Barotrauma
if (!Objectives.TryGetValue(target, out AIObjective objective)) if (!Objectives.TryGetValue(target, out AIObjective objective))
{ {
objective = ObjectiveConstructor(target); objective = ObjectiveConstructor(target);
objective.Completed += () => ReportedTargets.Remove(target); objective.Completed += () => OnObjectiveCompleted(objective, target);
Objectives.Add(target, objective); Objectives.Add(target, objective);
AddSubObjective(objective); if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
} }
} }
} }
protected abstract void OnObjectiveCompleted(AIObjective objective, T target);
/// <summary> /// <summary>
/// List of all possible items of the specified type. Used for filtering the removed objectives. /// List of all possible items of the specified type. Used for filtering the removed objectives.
/// </summary> /// </summary>
@@ -358,6 +358,10 @@ namespace Barotrauma
{ {
isCompleted = true; isCompleted = true;
} }
if (component.AIOperate(deltaTime, character, this))
{
isCompleted = true;
}
} }
else else
{ {
@@ -54,7 +54,6 @@ namespace Barotrauma
return pumpList; return pumpList;
} }
protected override AIObjective ObjectiveConstructor(Pump pump) => new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { IsLoop = false };
protected override float TargetEvaluation() protected override float TargetEvaluation()
{ {
if (Option == "stoppumping") if (Option == "stoppumping")
@@ -66,5 +65,11 @@ namespace Barotrauma
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(-t.FlowPercentage / 100))); return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(-t.FlowPercentage / 100)));
} }
} }
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { IsLoop = false };
protected override void OnObjectiveCompleted(AIObjective objective, Pump target)
=> HumanAIController.RemoveTargets<AIObjectivePumpWater, Pump>(character, target);
} }
} }
@@ -54,7 +54,12 @@ namespace Barotrauma
protected override float TargetEvaluation() => Targets.Max(t => 100 - t.ConditionPercentage); protected override float TargetEvaluation() => Targets.Max(t => 100 - t.ConditionPercentage);
protected override IEnumerable<Item> GetList() => Item.ItemList; protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
public static bool IsValidTarget(Item item, Character character) public static bool IsValidTarget(Item item, Character character)
{ {
@@ -22,12 +22,16 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList; protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)) * 100; protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)) * 100;
public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality; public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality;
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character) public static bool IsValidTarget(Character target, Character character)
{ {
if (target == null || target.IsDead || target.Removed) { return false; } if (target == null || target.IsDead || target.Removed) { return false; }
@@ -734,6 +734,25 @@ namespace Barotrauma.Items.Components
} }
} }
if (targetItem.Prefab.DeconstructItems.Any())
{
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddToRemoveQueue(targetItem);
MoveInputQueue();
PutItemsToLinkedContainer();
}
else
{
if (outputContainer.Inventory.Items.All(i => i != null))
{
targetItem.Drop(dropper: null);
}
else
{
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
}
}
if (targetItem.Prefab.DeconstructItems.Any()) if (targetItem.Prefab.DeconstructItems.Any())
{ {
inputContainer.Inventory.RemoveItem(targetItem); inputContainer.Inventory.RemoveItem(targetItem);
@@ -212,6 +212,33 @@ namespace Barotrauma.Items.Components
} }
} }
public Vector2? PosToMaintain
{
get { return posToMaintain; }
set { posToMaintain = value; }
}
struct ObstacleDebugInfo
{
public Vector2 Point1;
public Vector2 Point2;
public Vector2? Intersection;
public float Dot;
public Vector2 AvoidStrength;
public ObstacleDebugInfo(GraphEdge edge, Vector2? intersection, float dot, Vector2 avoidStrength)
{
Point1 = edge.Point1;
Point2 = edge.Point2;
Intersection = intersection;
Dot = dot;
AvoidStrength = avoidStrength;
}
}
//edge point 1, edge point 2, avoid strength //edge point 1, edge point 2, avoid strength
private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>(); private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>();
@@ -1128,6 +1128,10 @@ namespace Barotrauma
{ {
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
} }
if (!broken)
{
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
}
if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { return; } if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { return; }
@@ -1172,6 +1176,27 @@ namespace Barotrauma
} }
} }
public void UpdateTransform()
{
Submarine prevSub = Submarine;
FindHull();
if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { return; }
Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
rect.X = (int)(displayPos.X - rect.Width / 2.0f);
rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
if (Math.Abs(body.LinearVelocity.X) > NetConfig.MaxPhysicsBodyVelocity ||
Math.Abs(body.LinearVelocity.Y) > NetConfig.MaxPhysicsBodyVelocity)
{
body.LinearVelocity = new Vector2(
MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity),
MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity));
}
}
public void UpdateTransform() public void UpdateTransform()
{ {
Submarine prevSub = Submarine; Submarine prevSub = Submarine;
@@ -607,6 +607,25 @@ namespace Barotrauma
} }
} }
public string DisplayName
{
get;
private set;
}
private string roomName;
[Editable, Serialize("", true, translationTextTag: "RoomName.")]
public string RoomName
{
get { return roomName; }
set
{
if (roomName == value) { return; }
roomName = value;
DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName;
}
}
public override Rectangle Rect public override Rectangle Rect
{ {
get get
+3 -23
View File
@@ -4,47 +4,27 @@ v0.9.0.0
Additions and changes: Additions and changes:
- Translations to German, Russian, French, Brazilian Portuguese, Traditional Chinese and Simplified Chinese. - Translations to German, Russian, French, Brazilian Portuguese, Traditional Chinese and Simplified Chinese.
- New logo and main menu art.
- 2 new outposts.
- New docking system.
- Numerous fixes and improvements to the crew AI. The biggest change is that the AI characters can now do - Numerous fixes and improvements to the crew AI. The biggest change is that the AI characters can now do
tasks by themselves even when they haven't been assigned an order. tasks by themselves even when they haven't been assigned an order.
- One new outpost.
- New docking system.
- New logo and main menu art.
- Added a credits tab to the main menu.
- Some new item and structure sprites.
- Some new environment sprites.
- Increased the amount of credits the player starts with in the campaign.
- Balanced location and mission distribution.
- Balanced mission rewards.
- AI characters can be ordered to fight intruders.
- Lock the inventory when aiming with a railgun or a coilgun. - Lock the inventory when aiming with a railgun or a coilgun.
- Swap the order of the starting location & destination in nav terminals when playing in a mirrored sub. - Swap the order of the starting location & destination in nav terminals when playing in a mirrored sub.
- Option to automatically pause the game when the window loses focus.
Bugfixes: Bugfixes:
- Fixed splash screens and tutorial videos crashing the Linux version. - Fixed splash screens and tutorial videos crashing the Linux version.
- Fixed damaged item sprites not updating client-side after being repaired. - Fixed damaged item sprites not updating client-side after being repaired.
- Fixed characters occasionally taking impact damage when hitting walls while climbing. - Fixed characters occasionally taking impact damage when hitting walls while climbing.
- Fixes crashing when attempting to use symbols such as <, > or | in the save file name.
- UI layout/scale fixes on resolutions larger than 1080p.
- Fixed crashing when loading the same sub twice in the sub editor.
- Fixed submarines occasionally being rendered at an incorrect position when viewing them in the sub editor
after they've been used in-game.
- Improved the syncing of ragdolled characters. - Improved the syncing of ragdolled characters.
- Fixed characters not receiving impact damage when ragdolled.
- Fixed characters' arms occasionally spinning around when standing still.
- Fixed tutorial level generation parameters being used in some normal campaign levels (leading to extremely
small levels and submarines overlapping with the outposts).
- Fixed characters running more slowly when their torso is in a different hull than the feet (for example - Fixed characters running more slowly when their torso is in a different hull than the feet (for example
in Humpback's bilge). in Humpback's bilge).
- Fixed character's feet getting stuck to platforms when climbing ladders while holding A/D.
- Fixed monsters being able to drag characters through walls.
- Fixed items "vanishing" if they move directly from sub to another without going outside first. - Fixed items "vanishing" if they move directly from sub to another without going outside first.
- Fixed content package hash calculation failing if the package is not enabled and contains new monster files. - Fixed content package hash calculation failing if the package is not enabled and contains new monster files.
- Fixed inability to enable content packages if some of the files included in the package are already in - Fixed inability to enable content packages if some of the files included in the package are already in
the game folder (which may happen, for example, if enabling a content package fails). the game folder (which may happen, for example, if enabling a content package fails).
- Fixed AllowRagdollButton settings not being synced with clients, leading to strange ragdolling behavior - Fixed AllowRagdollButton settings not being synced with clients, leading to strange ragdolling behavior
client-side if the server has disabled ragdolling. client-side if the server has disabled ragdolling.
- Fixed incorrect keybinds being displayed in a couple of tutorial texts.
--------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------
v0.8.10.0 v0.8.10.0