Unstable v0.10.6.0 (October 13th 2020)

This commit is contained in:
Juan Pablo Arce
2020-10-13 12:59:45 -03:00
parent 768f516e7c
commit 6b36bf809d
59 changed files with 1036 additions and 421 deletions

View File

@@ -593,7 +593,7 @@ namespace Barotrauma
}
}
if (info != null || Vitality < MaxVitality * 0.98f)
if (info != null || Vitality < MaxVitality * 0.98f || IsPet)
{
hudInfoTimer -= deltaTime;
if (hudInfoTimer <= 0.0f)
@@ -772,49 +772,68 @@ namespace Barotrauma
MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) :
1.0f;
if (!GUI.DisableCharacterNames && hudInfoVisible && info != null &&
(controlled == null || this != controlled.FocusedCharacter) && cam.Zoom > 0.4f)
if (!GUI.DisableCharacterNames && hudInfoVisible &&
(controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f)
{
string name = Info.DisplayName;
if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); }
Vector2 nameSize = GUI.Font.MeasureString(name);
Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom;
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height);
namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y;
namePos *= screenSize / viewportSize;
namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y);
namePos *= viewportSize / screenSize;
namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y;
Color nameColor = Color.White;
if (Controlled != null && TeamID != Controlled.TeamID)
if (info != null)
{
nameColor = TeamID == TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red;
string name = Info.DisplayName;
if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); }
Vector2 nameSize = GUI.Font.MeasureString(name);
Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom;
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height);
namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y;
namePos *= screenSize / viewportSize;
namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y);
namePos *= viewportSize / screenSize;
namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y;
Color nameColor = Color.White;
if (Controlled != null && TeamID != Controlled.TeamID)
{
nameColor = TeamID == TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red;
}
if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract)
{
var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType);
if (iconStyle != null)
{
Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f;
Vector2 iconPos = headPos;
iconPos.Y = -iconPos.Y;
nameColor = iconStyle.Color;
var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom;
icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
}
}
GUI.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f);
GUI.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f);
if (GameMain.DebugDraw)
{
GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White);
}
}
if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract)
var petBehavior = (AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null && !IsDead && !IsUnconscious)
{
var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType);
var petStatus = petBehavior.GetCurrentStatusIndicatorType();
var iconStyle = GUI.Style.GetComponentStyle("PetIcon." + petStatus);
if (iconStyle != null)
{
Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f;
Vector2 iconPos = headPos;
iconPos.Y = -iconPos.Y;
nameColor = iconStyle.Color;
var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom;
float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom;
icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
}
}
GUI.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f);
GUI.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f);
if (GameMain.DebugDraw)
{
GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White);
}
}
if (IsDead) { return; }

View File

@@ -98,6 +98,9 @@ namespace Barotrauma
case "pendingupgrades":
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: true);
break;
case "pets":
petsElement = subElement;
break;
}
}
@@ -213,6 +216,10 @@ namespace Barotrauma
crewDead = false;
endTimer = 5.0f;
CrewManager.InitSinglePlayerRound();
if (petsElement != null)
{
PetBehavior.LoadPets(petsElement);
}
}
protected override void LoadInitialLevel()
@@ -705,6 +712,10 @@ namespace Barotrauma
}
}
XElement petsElement = new XElement("pets");
PetBehavior.SavePets(petsElement);
modeElement.Add(petsElement);
CrewManager.Save(modeElement);
CampaignMetadata.Save(modeElement);
Map.Save(modeElement);

View File

@@ -186,8 +186,7 @@ namespace Barotrauma.Tutorials
// TODO: Rework order highlighting for new command UI
// GameMain.GameSession.CrewManager.HighlightOrderButton(captain_mechanic, "repairsystems", highlightColor, new Vector2(5, 5));
//HighlightOrderOption("jobspecific");
}
while (!HasOrder(captain_mechanic, "repairsystems"));
} while (!HasOrder(captain_mechanic, "repairsystems") && !HasOrder(captain_mechanic, "repairmechanical") && !HasOrder(captain_mechanic, "repairelectrical"));
RemoveCompletedObjective(segments[1]);
yield return new WaitForSeconds(2f, false);
TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Command));

View File

@@ -123,7 +123,7 @@ namespace Barotrauma.Tutorials
// Room 3
engineer_reactorObjectiveSensor = Item.ItemList.Find(i => i.HasTag("engineer_reactorobjectivesensor")).GetComponent<MotionSensor>();
tutorial_oxygenGenerator = Item.ItemList.Find(i => i.HasTag("tutorial_oxygengenerator")).GetComponent<Powered>();
tutorial_oxygenGenerator = Item.ItemList.Find(i => i.HasTag("tutorial_oxygengenerator")).GetComponent<OxygenGenerator>();
engineer_reactor = Item.ItemList.Find(i => i.HasTag("engineer_reactor")).GetComponent<Reactor>();
engineer_reactor.FireDelay = engineer_reactor.MeltdownDelay = float.PositiveInfinity;
engineer_reactor.FuelConsumptionRate = 0.0f;
@@ -380,7 +380,7 @@ namespace Barotrauma.Tutorials
yield return null;
} while (engineer_brokenJunctionBox.Condition < repairableJunctionBoxComponent.RepairThreshold); // Wait until repaired
SetHighlight(engineer_brokenJunctionBox, false);
RemoveCompletedObjective(segments[2]);
RemoveCompletedObjective(segments[3]);
SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true);
for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++)
{
@@ -398,7 +398,7 @@ namespace Barotrauma.Tutorials
{
SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, false);
}
RemoveCompletedObjective(segments[3]);
RemoveCompletedObjective(segments[4]);
do { yield return null; } while (engineer_workingPump.Item.CurrentHull.WaterPercentage > waterVolumeBeforeOpening); // Wait until drained
wiringActive = false;
SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, true);
@@ -424,7 +424,7 @@ namespace Barotrauma.Tutorials
// Remove highlights when each individual machine is repaired
do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (engineer_submarineJunctionBox_1.Condition < repairableJunctionBoxComponent1.RepairThreshold || engineer_submarineJunctionBox_2.Condition < repairableJunctionBoxComponent2.RepairThreshold || engineer_submarineJunctionBox_3.Condition < repairableJunctionBoxComponent3.RepairThreshold);
CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3);
RemoveCompletedObjective(segments[4]);
RemoveCompletedObjective(segments[5]);
yield return new WaitForSeconds(2f, false);
TriggerTutorialSegment(6); // Powerup reactor
@@ -433,7 +433,7 @@ namespace Barotrauma.Tutorials
do { yield return null; } while (!IsReactorPoweredUp(engineer_submarineReactor)); // Wait until ~matches load
engineer.RemoveActiveObjectiveEntity(engineer_submarineReactor.Item);
SetHighlight(engineer_submarineReactor.Item, false);
RemoveCompletedObjective(segments[5]);
RemoveCompletedObjective(segments[6]);
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Complete"), ChatMessageType.Radio, null);
yield return new WaitForSeconds(4f, false);

View File

@@ -144,17 +144,13 @@ namespace Barotrauma
protected override ItemInventory GetActiveEquippedSubInventory(int slotIndex)
{
var item = Items[slotIndex];
if (item == null) return null;
if (item == null) { return null; }
var container = item.GetComponent<ItemContainer>();
if (container == null ||
!character.CanAccessInventory(container.Inventory) ||
!container.KeepOpenWhenEquipped ||
!character.HasEquippedItem(container.Item))
if (container == null || !container.KeepOpenWhenEquippedBy(character))
{
return null;
}
return container.Inventory;
}
@@ -626,7 +622,7 @@ namespace Barotrauma
{
var itemContainer = item.GetComponent<ItemContainer>();
if (itemContainer != null &&
itemContainer.KeepOpenWhenEquipped &&
itemContainer.KeepOpenWhenEquippedBy(character) &&
character.CanAccessInventory(itemContainer.Inventory) &&
!highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory))
{

View File

@@ -70,6 +70,7 @@ namespace Barotrauma.Items.Components
[Serialize(false, false, description: "Should the inventory of this item be kept open when the item is equipped by a character.")]
public bool KeepOpenWhenEquipped { get; set; }
[Serialize(false, false, description: "Can the inventory of this item be moved around on the screen by the player.")]
public bool MovableFrame { get; set; }
@@ -162,6 +163,30 @@ namespace Barotrauma.Items.Components
}
}
public bool KeepOpenWhenEquippedBy(Character character)
{
if (!character.CanAccessInventory(Inventory) ||
!KeepOpenWhenEquipped ||
!character.HasEquippedItem(Item))
{
return false;
}
//if holding 2 different "always open" items in different hands, don't force them to stay open
if (character.SelectedItems[0] != null &&
character.SelectedItems[1] != null &&
character.SelectedItems[0] != character.SelectedItems[1])
{
if ((character.SelectedItems[0].GetComponent<ItemContainer>()?.KeepOpenWhenEquipped ?? false) &&
(character.SelectedItems[1].GetComponent<ItemContainer>()?.KeepOpenWhenEquipped ?? false))
{
return false;
}
}
return true;
}
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1)
{
if (hideItems || (item.body != null && !item.body.Enabled)) { return; }

View File

@@ -342,7 +342,8 @@ namespace Barotrauma.Items.Components
}
else
{
if (Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X || PlayerInput.IsShiftDown())
if ((highlightedNodeIndex.HasValue && Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X) ||
PlayerInput.IsShiftDown())
{
selectedNodeIndex = highlightedNodeIndex;
}

View File

@@ -286,7 +286,7 @@ namespace Barotrauma.Items.Components
rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth));
if (!editing || GUI.DisableHUD) { return; }
if (!editing || GUI.DisableHUD || !item.IsSelected) { return; }
float widgetRadius = 60.0f;
@@ -305,8 +305,6 @@ namespace Barotrauma.Items.Components
drawPos + new Vector2((float)Math.Cos((maxRotation + minRotation) / 2), (float)Math.Sin((maxRotation + minRotation) / 2)) * widgetRadius,
Color.LightGreen);
if (!item.IsSelected) { return; }
Widget minRotationWidget = GetWidget("minrotation", spriteBatch, size: 10, initMethod: (widget) =>
{
widget.Selected += () =>

View File

@@ -404,8 +404,8 @@ namespace Barotrauma
container = (this as ItemInventory).Container;
}
if (container == null) return false;
return owner.SelectedCharacter != null || !container.KeepOpenWhenEquipped || (!(owner is Character)) || !owner.HasEquippedItem(container.Item);
if (container == null) { return false; }
return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item);
}
protected virtual bool HideSlot(int i)

View File

@@ -10,11 +10,30 @@ namespace Barotrauma
{
partial class Hull : MapEntity, ISerializableEntity, IServerSerializable, IClientSerializable
{
private class RemoteDecal
{
public readonly UInt32 DecalId;
public readonly int SpriteIndex;
public Vector2 NormalizedPos;
public readonly float Scale;
public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale)
{
DecalId = decalId;
SpriteIndex = spriteIndex;
NormalizedPos = normalizedPos;
Scale = scale;
}
}
private float serverUpdateDelay;
private float remoteWaterVolume, remoteOxygenPercentage;
private List<Vector3> remoteFireSources;
private readonly List<BackgroundSection> remoteBackgroundSections = new List<BackgroundSection>();
private readonly List<RemoteDecal> remoteDecals = new List<RemoteDecal>();
private readonly HashSet<Decal> pendingDecalUpdates = new HashSet<Decal>();
private double lastAmbientLightEditTime;
public override bool SelectableInEditor
@@ -126,10 +145,14 @@ namespace Barotrauma
networkUpdateTimer += deltaTime;
if (networkUpdateTimer > 0.2f)
{
if (!pendingSectionUpdates.Any())
if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any())
{
GameMain.NetworkMember?.CreateEntityEvent(this);
}
foreach (Decal decal in pendingDecalUpdates)
{
GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal });
}
foreach (int pendingSectionUpdate in pendingSectionUpdates)
{
GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate });
@@ -538,9 +561,9 @@ namespace Barotrauma
public void ClientWrite(IWriteMessage msg, object[] extraData = null)
{
msg.Write(extraData != null);
if (extraData == null)
{
msg.WriteRangedInteger(0, 0, 2);
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
msg.Write(FireSources.Count > 0);
@@ -560,8 +583,16 @@ namespace Barotrauma
}
}
}
else if (extraData[0] is Decal decal)
{
msg.WriteRangedInteger(1, 0, 2);
int decalIndex = decals.IndexOf(decal);
msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex));
msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8);
}
else
{
msg.WriteRangedInteger(2, 0, 2);
int sectorToUpdate = (int)extraData[0];
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
@@ -622,22 +653,16 @@ namespace Barotrauma
else
{
int decalCount = message.ReadRangedInteger(0, MaxDecalsPerHull);
decals.Clear();
if (decalCount == 0) { decals.Clear(); }
remoteDecals.Clear();
for (int i = 0; i < decalCount; i++)
{
UInt32 decalId = message.ReadUInt32();
int spriteIndex = message.ReadByte();
float normalizedXPos = message.ReadRangedSingle(0.0f, 1.0f, 8);
float normalizedYPos = message.ReadRangedSingle(0.0f, 1.0f, 8);
float decalPosX = MathHelper.Lerp(rect.X, rect.Right, normalizedXPos);
float decalPosY = MathHelper.Lerp(rect.Y - rect.Height, rect.Y, normalizedYPos);
float decalScale = message.ReadRangedSingle(0.0f, 2.0f, 12);
if (Submarine != null)
{
decalPosX += Submarine.Position.X;
decalPosY += Submarine.Position.Y;
}
AddDecal(decalId, new Vector2(decalPosX, decalPosY), decalScale, isNetworkEvent: true, spriteIndex: spriteIndex);
remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale));
}
}
}
@@ -658,6 +683,23 @@ namespace Barotrauma
}
remoteBackgroundSections.Clear();
if (remoteDecals.Any())
{
decals.Clear();
foreach (RemoteDecal remoteDecal in remoteDecals)
{
float decalPosX = MathHelper.Lerp(rect.X, rect.Right, remoteDecal.NormalizedPos.X);
float decalPosY = MathHelper.Lerp(rect.Y - rect.Height, rect.Y, remoteDecal.NormalizedPos.Y);
if (Submarine != null)
{
decalPosX += Submarine.Position.X;
decalPosY += Submarine.Position.Y;
}
AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex);
}
remoteDecals.Clear();
}
if (remoteFireSources == null) { return; }
WaterVolume = remoteWaterVolume;

View File

@@ -44,6 +44,12 @@ namespace Barotrauma.Lights
}
}
[Serialize(1f, true), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)]
public float Scale { get; set; }
[Serialize("0, 0", true), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)]
public Vector2 Offset { get; set; }
public float TextureRange
{
get;
@@ -241,11 +247,13 @@ namespace Barotrauma.Lights
}
}
private Vector2 _spriteScale = Vector2.One;
public Vector2 SpriteScale
{
get;
set;
} = Vector2.One;
get { return _spriteScale * lightSourceParams.Scale; }
set { _spriteScale = value; }
}
public float? OverrideLightSpriteAlpha
{
@@ -280,7 +288,9 @@ namespace Barotrauma.Lights
{
get { return lightSourceParams.LightSprite; }
}
private Vector2 OverrideLightTextureOrigin => OverrideLightTexture.Origin + LightSourceParams.Offset;
public Color Color
{
get { return lightSourceParams.Color; }
@@ -564,7 +574,7 @@ namespace Barotrauma.Lights
var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
Vector2 origin = OverrideLightTexture.Origin;
Vector2 origin = OverrideLightTextureOrigin;
origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
origin -= Vector2.One * 0.5f;
@@ -873,7 +883,7 @@ namespace Barotrauma.Lights
{
overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
Vector2 origin = OverrideLightTexture.Origin;
Vector2 origin = OverrideLightTextureOrigin;
if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; }
if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; }
uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f);
@@ -1060,7 +1070,7 @@ namespace Barotrauma.Lights
{
var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
Vector2 origin = OverrideLightTexture.Origin;
Vector2 origin = OverrideLightTextureOrigin;
origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
origin *= TextureRange;
@@ -1088,13 +1098,12 @@ namespace Barotrauma.Lights
if (DeformableLightSprite != null)
{
Vector2 origin = DeformableLightSprite.Origin;
Vector2 origin = DeformableLightSprite.Origin + LightSourceParams.Offset;
Vector2 drawPos = position;
if (ParentSub != null)
{
drawPos += ParentSub.DrawPosition;
}
if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
{
origin.X = DeformableLightSprite.Sprite.SourceRect.Width - origin.X;
@@ -1113,7 +1122,7 @@ namespace Barotrauma.Lights
if (LightSprite != null)
{
Vector2 origin = LightSprite.Origin;
Vector2 origin = LightSprite.Origin + LightSourceParams.Offset;
if ((LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally)
{
origin.X = LightSprite.SourceRect.Width - origin.X;

View File

@@ -1023,7 +1023,10 @@ namespace Barotrauma.Networking
if (Enum.TryParse(splitMsg[0], out disconnectReason)) { disconnectReasonIncluded = true; }
}
if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage)
if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage ||
disconnectReason == DisconnectReason.Banned ||
disconnectReason == DisconnectReason.Kicked ||
disconnectReason == DisconnectReason.TooManyFailedLogins)
{
allowReconnect = false;
}
@@ -2138,6 +2141,7 @@ namespace Barotrauma.Networking
return;
}
entities.Add(entity);
if (entity != null && (entity is Item || entity is Character || entity is Submarine))
{
entity.ClientRead(objHeader.Value, inc, sendingTime);
@@ -2188,7 +2192,8 @@ namespace Barotrauma.Networking
errorLines.Add(ex.StackTrace.CleanupStackTrace());
errorLines.Add(" ");
if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL ||
objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL)
objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL ||
objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION)
{
foreach (IServerSerializable ent in entities)
{

View File

@@ -373,7 +373,17 @@ namespace Barotrauma.Networking
info.GameMode = element.GetAttributeString("GameMode", "");
info.GameVersion = element.GetAttributeString("GameVersion", "");
info.MaxPlayers = element.GetAttributeInt("MaxPlayers", 0);
int maxPlayersElement = element.GetAttributeInt("MaxPlayers", 0);
if (maxPlayersElement > NetConfig.MaxPlayers)
{
DebugConsole.IsOpen = true;
DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red);
maxPlayersElement = NetConfig.MaxPlayers;
}
info.MaxPlayers = maxPlayersElement;
if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; }
if (bool.TryParse(element.GetAttributeString("UsingWhiteList", ""), out bool whitelistTemp)) { info.UsingWhiteList = whitelistTemp; }

View File

@@ -306,7 +306,7 @@ namespace Barotrauma.CharacterEditor
var lastJoint = selectedJoints.LastOrDefault();
if (lastJoint != null)
{
lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbA : lastJoint.LimbB;
lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbB : lastJoint.LimbA;
}
}
if (lastLimb != null)
@@ -941,7 +941,7 @@ namespace Barotrauma.CharacterEditor
var lastJoint = selectedJoints.LastOrDefault();
if (lastJoint != null)
{
lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbA : lastJoint.LimbB;
lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbB : lastJoint.LimbA;
}
}
if (lastLimb != null)
@@ -2389,7 +2389,7 @@ namespace Barotrauma.CharacterEditor
IEnumerable<Limb> limbs = selectedLimbs;
if (limbs.None())
{
limbs = selectedJoints.Select(j => PlayerInput.KeyDown(Keys.LeftAlt) ? j.LimbA : j.LimbB);
limbs = selectedJoints.Select(j => PlayerInput.KeyDown(Keys.LeftAlt) ? j.LimbB : j.LimbA);
}
foreach (var limb in limbs)
{
@@ -4452,11 +4452,11 @@ namespace Barotrauma.CharacterEditor
}
if (editJoints)
{
if (!altDown && joint.BodyA == limb.body.FarseerBody)
if (altDown && joint.BodyA == limb.body.FarseerBody)
{
continue;
}
if (altDown && joint.BodyB == limb.body.FarseerBody)
if (!altDown && joint.BodyB == limb.body.FarseerBody)
{
continue;
}

View File

@@ -1104,7 +1104,16 @@ namespace Barotrauma
{
port = settingsDoc.Root.GetAttributeInt("port", port);
queryPort = settingsDoc.Root.GetAttributeInt("queryport", queryPort);
maxPlayers = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers);
int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers);
if (maxPlayersElement > NetConfig.MaxPlayers)
{
DebugConsole.IsOpen = true;
DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red);
maxPlayersElement = NetConfig.MaxPlayers;
}
maxPlayers = maxPlayersElement;
karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true);
selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default");
string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual");

View File

@@ -4375,9 +4375,18 @@ namespace Barotrauma
Submarine.DrawFront(spriteBatch, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false));
if (!WiringMode && !IsMouseOnEditorGUI())
{
MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam);
MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam);
MapEntity.DrawSelecting(spriteBatch, cam);
}
if (dummyCharacter != null && WiringMode)
{
for (int i = 0; i < dummyCharacter.SelectedItems.Length; i++)
{
if (dummyCharacter.SelectedItems[i] == null) { continue; }
if (i > 0 && dummyCharacter.SelectedItems[0] == dummyCharacter.SelectedItems[i]) { continue; }
dummyCharacter.SelectedItems[i].Draw(spriteBatch, editing: false, back: true);
}
}
spriteBatch.End();
if (GameMain.LightManager.LightingEnabled && lightingEnabled)

View File

@@ -74,7 +74,7 @@ namespace Barotrauma
//ambience
private static Sound waterAmbienceIn, waterAmbienceOut, waterAmbienceMoving;
private static SoundChannel[] waterAmbienceChannels = new SoundChannel[3];
private static readonly SoundChannel[] waterAmbienceChannels = new SoundChannel[3];
private static float ambientSoundTimer;
private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max
@@ -85,6 +85,7 @@ namespace Barotrauma
private static Vector2 hullSoundInterval = new Vector2(45.0f, 90.0f); //x = min, y = max
//misc
private static float[] targetFlowLeft, targetFlowRight;
public static List<Sound> FlowSounds = new List<Sound>();
public static List<Sound> SplashSounds = new List<Sound>();
private static SoundChannel[] flowSoundChannels;
@@ -109,6 +110,8 @@ namespace Barotrauma
private static Dictionary<GUISoundType, List<Sound>> guiSounds;
private static bool firstTimeInMainMenu = true;
private static Sound startUpSound;
public static bool Initialized;
@@ -335,11 +338,14 @@ namespace Barotrauma
else { miscSoundList.Add(new KeyValuePair<string, Sound>(g.Key, s)); }
}));
flowSoundChannels?.ForEach(ch => ch?.Dispose());
flowSoundChannels = new SoundChannel[FlowSounds.Count];
flowVolumeLeft = new float[FlowSounds.Count];
flowVolumeRight = new float[FlowSounds.Count];
targetFlowLeft = new float[FlowSounds.Count];
targetFlowRight = new float[FlowSounds.Count];
fireSoundChannels?.ForEach(ch => ch?.Dispose());
fireSoundChannels = new SoundChannel[fireSizes];
fireVolumeLeft = new float[fireSizes];
fireVolumeRight = new float[fireSizes];
@@ -494,9 +500,6 @@ namespace Barotrauma
{
if (FlowSounds.Count == 0) { return; }
float[] targetFlowLeft = new float[FlowSounds.Count];
float[] targetFlowRight = new float[FlowSounds.Count];
Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y);
foreach (Gap gap in Gap.GapList)
{
@@ -931,7 +934,9 @@ namespace Barotrauma
return "editor";
}
if (Screen.Selected != GameMain.GameScreen) { return "menu"; }
if (Screen.Selected != GameMain.GameScreen) { return firstTimeInMainMenu ? "menu" : "default"; }
firstTimeInMainMenu = false;
if (Character.Controlled != null)
@@ -973,12 +978,12 @@ namespace Barotrauma
float totalArea = 0.0f;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine != targetSubmarine) continue;
if (hull.Submarine != targetSubmarine) { continue; }
floodedArea += hull.WaterVolume;
totalArea += hull.Volume;
}
if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) return "flooded";
if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded"; }
}
float enemyDistThreshold = 5000.0f;
@@ -991,7 +996,7 @@ namespace Barotrauma
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || !character.Enabled) continue;
if (!(character.AIController is EnemyAIController enemyAI) || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) continue;
if (!(character.AIController is EnemyAIController enemyAI) || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; }
if (targetSubmarine != null)
{

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.10.601.0</Version>
<Version>0.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.10.601.0</Version>
<Version>0.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.10.601.0</Version>
<Version>0.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.10.601.0</Version>
<Version>0.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.10.601.0</Version>
<Version>0.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -478,7 +478,7 @@ namespace Barotrauma
msg.Write(Info == null);
msg.Write(entityId);
msg.Write(SpeciesName);
msg.Write(seed);
msg.Write(Seed);
if (Removed)
{

View File

@@ -1119,6 +1119,12 @@ namespace Barotrauma
}
else
{
if (maxPlayers > NetConfig.MaxPlayers)
{
NewMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.");
maxPlayers = NetConfig.MaxPlayers;
}
GameMain.Server.ServerSettings.MaxPlayers = maxPlayers;
NewMessage("Set the maximum player count to " + maxPlayers + ".");
}
@@ -1132,6 +1138,12 @@ namespace Barotrauma
}
else
{
if (maxPlayers > NetConfig.MaxPlayers)
{
GameMain.Server.SendConsoleMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", client);
maxPlayers = NetConfig.MaxPlayers;
}
GameMain.Server.ServerSettings.MaxPlayers = maxPlayers;
NewMessage(client.Name + " set the maximum player count to " + maxPlayers + ".");
GameMain.Server.SendConsoleMessage("Set the maximum player count to " + maxPlayers + ".", client);

View File

@@ -147,6 +147,14 @@ namespace Barotrauma
c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign)));
}
public void LoadPets()
{
if (petsElement != null)
{
PetBehavior.LoadPets(petsElement);
}
}
protected override IEnumerable<object> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults)
{
lastUpdateID++;
@@ -229,6 +237,9 @@ namespace Barotrauma
c.Inventory.DeleteAllItems();
}
petsElement = new XElement("pets");
PetBehavior.SavePets(petsElement);
yield return CoroutineStatus.Running;
if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
@@ -768,6 +779,11 @@ namespace Barotrauma
CargoManager?.SavePurchasedItems(modeElement);
UpgradeManager?.SavePendingUpgrades(modeElement, UpgradeManager?.PendingUpgrades);
if (petsElement != null)
{
modeElement.Add(petsElement);
}
// save bots
CrewManager.SaveMultiplayer(modeElement);

View File

@@ -128,28 +128,8 @@ namespace Barotrauma
//used when clients use the water/fire console commands or section / decal updates are received
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
bool hasExtraData = msg.ReadBoolean();
if (hasExtraData)
{
int sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1);
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
for (int i = start; i < end; i++)
{
float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8);
Color color = new Color(msg.ReadUInt32());
//TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches
if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent<Sprayer>() != null))
{
BackgroundSections[i].SetColorStrength(colorStrength);
BackgroundSections[i].SetColor(color);
}
}
//add to pending updates to notify other clients as well
pendingSectionUpdates.Add(sectorToUpdate);
}
else
int messageType = msg.ReadRangedInteger(0, 2);
if (messageType == 0)
{
float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
@@ -205,6 +185,37 @@ namespace Barotrauma
FireSources.RemoveAt(i);
}
}
}
else if (messageType == 1)
{
byte decalIndex = msg.ReadByte();
float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255);
if (decalIndex < 0 || decalIndex >= decals.Count) { return; }
if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent<Sprayer>() != null))
{
decals[decalIndex].BaseAlpha = decalAlpha;
}
decalUpdatePending = true;
}
else
{
int sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1);
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
for (int i = start; i < end; i++)
{
float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8);
Color color = new Color(msg.ReadUInt32());
//TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches
if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent<Sprayer>() != null))
{
BackgroundSections[i].SetColorStrength(colorStrength);
BackgroundSections[i].SetColor(color);
}
}
//add to pending updates to notify other clients as well
pendingSectionUpdates.Add(sectorToUpdate);
}
}
}

View File

@@ -1574,8 +1574,9 @@ namespace Barotrauma.Networking
{
//if docked to a sub with a smaller ID, don't send an update
// (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) continue;
if (!c.PendingPositionUpdates.Contains(sub)) c.PendingPositionUpdates.Enqueue(sub);
if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; }
if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; }
if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
}
foreach (Item item in Item.ItemList)
@@ -2291,6 +2292,8 @@ namespace Barotrauma.Networking
crewManager?.InitRound();
}
campaign?.LoadPets();
foreach (Submarine sub in Submarine.MainSubs)
{
if (sub == null) continue;
@@ -2359,7 +2362,7 @@ namespace Barotrauma.Networking
bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn);
bool outpostAllowRespawn = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost;
msg.Write(missionAllowRespawn || outpostAllowRespawn);
msg.Write(serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn));
msg.Write(serverSettings.AllowDisguises);
msg.Write(serverSettings.AllowRewiring);
msg.Write(serverSettings.AllowRagdollButton);

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.10.601.0</Version>
<Version>0.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2020</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -59,6 +59,7 @@
<Item file="Content/Items/Gardening/growableplants.xml" />
<Item file="Content/Items/Gardening/gardeningtools.xml" />
<Item file="Content/Items/Gardening/plantproducts.xml" />
<Character file="Content/Characters/Balloon/Balloon.xml" />
<Character file="Content/Characters/Carrier/Carrier.xml" />
<Character file="Content/Characters/Charybdis/Charybdis.xml" />
<Character file="Content/Characters/Coelanth/Coelanth.xml" />
@@ -92,6 +93,7 @@
<Character file="Content/Characters/Molochbaby/Molochbaby.xml" />
<Character file="Content/Characters/Peanut/Peanut.xml" />
<Character file="Content/Characters/Psilotoad/Psilotoad.xml" />
<Character file="Content/Characters/Orangeboy/Orangeboy.xml" />
<Character file="Content/Characters/Tigerthresher/Tigerthresher.xml" />
<Character file="Content/Characters/Bonethresher/Bonethresher.xml" />
<Character file="Content/Characters/Leucocyte/Leucocyte.xml" />

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze }
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
abstract partial class AIController : ISteerable
{

View File

@@ -48,13 +48,11 @@ namespace Barotrauma
private float attackLimbResetTimer;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0;
public float CombatStrength => Character.Params.AI.CombatStrength;
private float Sight => Character.Params.AI.Sight;
private float Hearing => Character.Params.AI.Hearing;
private float FleeHealthThreshold => Character.Params.AI.FleeHealthThreshold;
private float AggressionGreed => Character.Params.AI.AggressionGreed;
private float AggressionHurt => Character.Params.AI.AggressionHurt;
private bool AggressiveBoarding => Character.Params.AI.AggressiveBoarding;
public float CombatStrength => AIParams.CombatStrength;
private float Sight => AIParams.Sight;
private float Hearing => AIParams.Hearing;
private float FleeHealthThreshold => AIParams.FleeHealthThreshold;
private bool AggressiveBoarding => AIParams.AggressiveBoarding;
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
@@ -90,7 +88,6 @@ namespace Barotrauma
private readonly float priorityFearIncreasement = 2;
private readonly float memoryFadeTime = 0.5f;
private readonly float avoidTime = 3;
private float avoidTimer;
private float observeTimer;
@@ -241,6 +238,10 @@ namespace Barotrauma
{
targetingTag = "dead";
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
{
targetingTag = "owner";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
@@ -384,7 +385,7 @@ namespace Barotrauma
{
updateTargetsTimer -= deltaTime;
}
else
else if (avoidTimer <= 0)
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
@@ -393,11 +394,7 @@ namespace Barotrauma
UpdateWallTarget();
}
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
if (avoidTimer > 0)
{
State = AIState.Escape;
}
else if (SelectedAiTarget == null)
if (SelectedAiTarget == null)
{
State = AIState.Idle;
}
@@ -502,17 +499,21 @@ namespace Barotrauma
}
break;
case AIState.Protect:
case AIState.Follow:
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead)
if (State == AIState.Protect)
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
return;
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead)
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
return;
}
}
float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
@@ -568,6 +569,7 @@ namespace Barotrauma
}
else
{
// TODO: doesn't work right here
FaceTarget(SelectedAiTarget.Entity);
}
observeTimer -= deltaTime;
@@ -592,7 +594,6 @@ namespace Barotrauma
if (!Character.AnimController.SimplePhysicsEnabled)
{
LatchOntoAI?.Update(this, deltaTime);
PetBehavior?.Update(deltaTime);
}
IsSteeringThroughGap = false;
if (SwarmBehavior != null)
@@ -1454,7 +1455,8 @@ namespace Barotrauma
bool isFriendly = IsFriendly(Character, attacker);
if (wasLatched)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
if (!isFriendly)
{
SelectTarget(attacker.AiTarget);
@@ -1527,7 +1529,7 @@ namespace Barotrauma
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt;
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
@@ -1552,12 +1554,14 @@ namespace Barotrauma
}
else if (avoidGunFire)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
}
if (Character.HealthPercentage <= FleeHealthThreshold)
{
avoidTimer = Rand.Range(15, 30);
State = AIState.Flee;
avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
}
}
@@ -1587,7 +1591,7 @@ namespace Barotrauma
if (damageTarget.Health > 0)
{
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AggressionGreed;
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed;
}
else
{
@@ -1654,10 +1658,13 @@ namespace Barotrauma
if (!item.Removed && item.body != null)
{
float itemBodyExtent = item.body.GetMaxExtent() * 2;
if (limbDiff.LengthSquared() < itemBodyExtent * itemBodyExtent)
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
{
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime);
item.body.ApplyForce(-limbDiff * item.body.Mass);
if (item.Condition <= 0.0f)
{
@@ -1697,15 +1704,40 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
if (!MathUtils.IsValid(dir))
if (Character.CurrentHull != null && PathSteering != null)
{
return;
// Inside
Character targetCharacter = SelectedAiTarget.Entity as Character;
if ((Character.AnimController.InWater || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
{
// Steer towards the target if in the same room and swimming
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
if (MathUtils.IsValid(dir))
{
SteeringManager.SteeringManual(deltaTime, dir);
}
}
else
{
// Use path finding
SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 2);
if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable)
{
// Can't reach
State = AIState.Idle;
return;
}
}
}
SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5);
if (Character.AnimController.InWater)
else
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
// Outside
SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
}
@@ -1748,6 +1780,10 @@ namespace Barotrauma
{
targetingTag = "dead";
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
{
targetingTag = "owner";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
@@ -1992,9 +2028,16 @@ namespace Barotrauma
if (targetingTag == null) { continue; }
var targetParams = GetTargetParams(targetingTag);
if (targetParams == null) { continue; }
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine && targetParams.State == AIState.Observe)
if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
{
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine)
{
// Don't allow to target characters that are inside a different submarine / outside when we are inside.
continue;
}
}
if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null)
{
// Don't allow to observe characters that are inside a different submarine / outside when we are inside.
continue;
}
valueModifier *= targetParams.Priority;
@@ -2066,6 +2109,17 @@ namespace Barotrauma
}
if (targetCharacter != null)
{
if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull)
{
if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe)
{
// Ignore targets that cannot see
if (!VisibleHulls.Contains(targetCharacter.CurrentHull))
{
continue;
}
}
}
if (targetCharacter.Submarine != Character.Submarine)
{
if (targetCharacter.Submarine != null)

View File

@@ -1111,7 +1111,10 @@ namespace Barotrauma
public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
{
int targetSlot = -1;

View File

@@ -190,6 +190,7 @@ namespace Barotrauma
}
}
pathFinder.InsideSubmarine = character.Submarine != null;
pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0;
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;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
@@ -343,7 +344,7 @@ namespace Barotrauma
}
return diff;
}
else if (!canClimb || character.AnimController.InWater)
else if (character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
@@ -370,7 +371,7 @@ namespace Barotrauma
}
}
}
else if (!IsNextLadderSameAsCurrent)
else if (!canClimb || !IsNextLadderSameAsCurrent)
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
@@ -378,6 +379,8 @@ namespace Barotrauma
Vector2 velocity = collider.LinearVelocity;
// If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high.
float minHeight = 1;
// If the character is very thin, without a min value, it would often fail to reach the waypoints, because the horizontal distance is too small.
float minWidth = 0.17f;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
@@ -386,7 +389,7 @@ namespace Barotrauma
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
float targetDistance = collider.radius * margin;
float targetDistance = Math.Max(collider.radius * margin, minWidth);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor)
{
currentPath.SkipToNextNode();

View File

@@ -204,11 +204,15 @@ namespace Barotrauma
{
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
// If the target hull is inside a submarine that is not our main sub, just ignore it normally when it cannot be reached. This happens with outposts.
if (goToObjective != null)
{
if (goToObjective.Target is Hull hull)
{
HumanAIController.UnreachableHulls.Add(hull);
if (currentHull != null || !Submarine.MainSubs.Contains(hull.Submarine))
{
HumanAIController.UnreachableHulls.Add(hull);
}
}
}
RemoveSubObjective(ref goToObjective);

View File

@@ -257,6 +257,10 @@ namespace Barotrauma
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = 1;
if (GetItemPriority != null)
{

View File

@@ -342,7 +342,7 @@ namespace Barotrauma
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 2);
}
}
}
@@ -360,7 +360,10 @@ namespace Barotrauma
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
}

View File

@@ -94,6 +94,7 @@ namespace Barotrauma
private readonly List<PathNode> nodes;
public bool InsideSubmarine { get; set; }
public bool ApplyPenaltyToOutsideNodes { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
{
@@ -178,7 +179,7 @@ namespace Barotrauma
node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine) { node.TempDistance *= 10.0f; }
if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; }
//prefer nodes that are closer to the end position
node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
@@ -231,8 +232,11 @@ namespace Barotrauma
node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
if (ApplyPenaltyToOutsideNodes)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
}
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
//avoid stopping at a ladder

View File

@@ -1,3 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
@@ -9,6 +10,14 @@ namespace Barotrauma
{
class PetBehavior
{
public enum StatusIndicatorType
{
None,
Happy,
Sad,
Hungry
}
public float Hunger { get; set; } = 50.0f;
public float Happiness { get; set; } = 50.0f;
@@ -21,6 +30,7 @@ namespace Barotrauma
public float PlayForce { get; set; }
public float PlayTimer { get; set; }
private float? unstunY { get; set; }
public EnemyAIController AiController { get; private set; } = null;
@@ -126,6 +136,7 @@ namespace Barotrauma
public float Hunger;
public float Happiness;
public float Priority;
public bool IgnoreContained;
public CharacterParams.TargetParams TargetParams = null;
}
@@ -159,7 +170,11 @@ namespace Barotrauma
case "eat":
Food food = new Food
{
Tag = subElement.GetAttributeString("tag", "")
Tag = subElement.GetAttributeString("tag", ""),
Hunger = subElement.GetAttributeFloat("hunger", -1),
Happiness = subElement.GetAttributeFloat("happiness", 1),
Priority = subElement.GetAttributeFloat("priority", 100),
IgnoreContained = subElement.GetAttributeBool("ignorecontained", true)
};
string[] requiredHungerStr = subElement.GetAttributeString("requiredhunger", "0-100").Split('-');
food.HungerRange = new Vector2(0, 100);
@@ -168,15 +183,20 @@ namespace Barotrauma
if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float tempF)) { food.HungerRange.X = tempF; }
if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { food.HungerRange.Y = tempF; }
}
food.Hunger = subElement.GetAttributeFloat("hunger", -1);
food.Happiness = subElement.GetAttributeFloat("happiness", 1);
food.Priority = subElement.GetAttributeFloat("priority", 100);
foods.Add(food);
break;
}
}
}
public StatusIndicatorType GetCurrentStatusIndicatorType()
{
if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; }
if (Happiness > MaxHappiness * 0.75f) { return StatusIndicatorType.Happy; }
if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; }
return StatusIndicatorType.None;
}
public void OnEat(IEnumerable<string> tags, float amount)
{
for (int i = 0; i < foods.Count; i++)
@@ -203,17 +223,19 @@ namespace Barotrauma
}
}
public void Play()
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; }
PlayTimer = 5.0f;
AiController.Character.Stun = 1.0f;
AiController.Character.IsRagdolled = true;
Happiness += 10.0f;
if (Happiness > MaxHappiness) { Happiness = MaxHappiness; }
AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AiController.Character.SimPosition.Y;
}
public string GetName()
public string GetTagName()
{
if (AiController.Character.Inventory != null)
{
@@ -230,14 +252,16 @@ namespace Barotrauma
}
}
return AiController.Character.Name;
return string.Empty;
}
public void Update(float deltaTime)
{
var character = AiController.Character;
if (character?.Removed ?? true || character.IsDead) { return; }
if (GameMain.NetworkMember?.IsClient ?? false) { return; } //TODO: syncing
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; }
Hunger += HungerIncreaseRate * deltaTime;
@@ -245,20 +269,45 @@ namespace Barotrauma
PlayTimer -= deltaTime;
for (int i = 0; i < foods.Count; i++)
if (unstunY.HasValue)
{
if (Hunger >= foods[i].HungerRange.X && Hunger <= foods[i].HungerRange.Y)
if (PlayTimer > 4.0f)
{
if (foods[i].TargetParams == null &&
AiController.AIParams.TryAddNewTarget(foods[i].Tag, AIState.Eat, foods[i].Priority, out CharacterParams.TargetParams targetParams))
float extent = character.AnimController.MainLimb.body.GetMaxExtent();
if (character.SimPosition.Y < (unstunY.Value + extent * 3.0f) &&
character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f)
{
foods[i].TargetParams = targetParams;
character.IsRagdolled = false;
unstunY = null;
}
else
{
character.IsRagdolled = true;
}
}
else if (foods[i].TargetParams != null)
else
{
AiController.AIParams.RemoveTarget(foods[i].TargetParams);
foods[i].TargetParams = null;
character.IsRagdolled = false;
unstunY = null;
}
}
for (int i = 0; i < foods.Count; i++)
{
Food food = foods[i];
if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
{
if (food.TargetParams == null &&
AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams))
{
targetParams.IgnoreContained = food.IgnoreContained;
food.TargetParams = targetParams;
}
}
else if (food.TargetParams != null)
{
AiController.AIParams.RemoveTarget(food.TargetParams);
food.TargetParams = null;
}
}
@@ -279,7 +328,8 @@ namespace Barotrauma
if (character.SelectedBy != null)
{
character.Stun = 1.0f;
character.IsRagdolled = true;
unstunY = character.SimPosition.Y;
}
for (int i = 0; i < itemsToProduce.Count; i++)
@@ -287,5 +337,73 @@ namespace Barotrauma
itemsToProduce[i].Update(this, deltaTime);
}
}
public static void SavePets(XElement petsElement)
{
foreach (Character c in Character.CharacterList)
{
if (!c.IsPet || c.IsDead) { continue; }
if (c.Submarine?.Info.Type != SubmarineType.Player) { continue; }
var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior == null) { continue; }
XElement petElement = new XElement("pet",
new XAttribute("speciesname", c.SpeciesName),
new XAttribute("ownerid", petBehavior.Owner?.ID ?? Entity.NullEntityID),
new XAttribute("seed", c.Seed));
var petBehaviorElement = new XElement("petbehavior",
new XAttribute("hunger", petBehavior.Hunger.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("happiness", petBehavior.Happiness.ToString("G", CultureInfo.InvariantCulture)));
petElement.Add(petBehaviorElement);
var healthElement = new XElement("health");
c.CharacterHealth.Save(healthElement);
petElement.Add(healthElement);
if (c.Inventory != null)
{
var inventoryElement = new XElement("inventory");
c.SaveInventory(c.Inventory, inventoryElement);
petElement.Add(inventoryElement);
}
petsElement.Add(petElement);
}
}
public static void LoadPets(XElement petsElement)
{
foreach (XElement subElement in petsElement.Elements())
{
string speciesName = subElement.GetAttributeString("speciesname", "");
string seed = subElement.GetAttributeString("seed", "123");
ushort ownerID = (ushort)subElement.GetAttributeInt("ownerid", 0);
Vector2 spawnPos = Vector2.Zero;
Character owner = Entity.FindEntityByID(ownerID) as Character;
if (owner != null)
{
spawnPos = owner.WorldPosition;
}
else
{
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
{
petBehavior.Owner = owner;
var petBehaviorElement = subElement.Attribute("petbehavior");
if (petBehaviorElement != null)
{
petBehavior.Hunger = petBehaviorElement.GetAttributeFloat(50.0f);
petBehavior.Happiness = petBehaviorElement.GetAttributeFloat(50.0f);
}
}
}
}
}
}

View File

@@ -47,6 +47,10 @@ namespace Barotrauma
base.Update(deltaTime, cam);
if (!Enabled) { return; }
if (!IsRemotePlayer && AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.Update(deltaTime);
}
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated)
{
//don't enable simple physics on dead/incapacitated characters

View File

@@ -429,113 +429,195 @@ namespace Barotrauma
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
return;
}
Vector2 transformedMovement = reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
mainLimbAngle = TorsoAngle.Value;
}
else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
mainLimbAngle = HeadAngle.Value;
}
mainLimbAngle *= Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
}
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
movementAngle -= MathHelper.TwoPi;
}
if (CurrentSwimParams.RotateTowardsMovement)
{
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
}
if (TailAngle.HasValue)
{
Limb tail = GetLimb(LimbType.Tail);
if (tail != null)
{
float? mainLimbTargetAngle = null;
if (mainLimb.type == LimbType.Torso)
{
mainLimbTargetAngle = TorsoAngle;
}
else if (mainLimb.type == LimbType.Head)
{
mainLimbTargetAngle = HeadAngle;
}
float torque = TailTorque;
float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier;
if (mainLimbTargetAngle.HasValue && maxMultiplier > 1)
{
float diff = Math.Abs(mainLimb.Rotation - tail.Rotation);
float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value);
torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset));
}
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque);
}
}
}
else
{
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
if (reverse)
Vector2 transformedMovement = reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
mainLimbAngle = TorsoAngle.Value;
}
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
mainLimbAngle = HeadAngle.Value;
}
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
mainLimbAngle *= Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
movementAngle += MathHelper.TwoPi;
}
if (TorsoAngle.HasValue)
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
Limb torso = GetLimb(LimbType.Torso);
torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque);
movementAngle -= MathHelper.TwoPi;
}
if (HeadAngle.HasValue)
if (CurrentSwimParams.RotateTowardsMovement)
{
Limb head = GetLimb(LimbType.Head);
head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque);
}
if (TailAngle.HasValue)
{
Limb tail = GetLimb(LimbType.Tail);
tail?.body.SmoothRotate(TailAngle.Value * Dir, TailTorque);
}
}
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
}
if (TailAngle.HasValue)
{
bool isAngleApplied = false;
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
}
if (!isAngleApplied)
{
RotateTail(GetLimb(LimbType.Tail));
}
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier);
if (waveLength > 0 && waveAmplitude > 0)
{
WalkPos -= transformedMovement.Length() / Math.Abs(waveLength);
WalkPos = MathUtils.WrapAngleTwoPi(WalkPos);
void RotateTail(Limb tail)
{
if (tail == null) { return; }
float? mainLimbTargetAngle = null;
if (mainLimb.type == LimbType.Torso)
{
mainLimbTargetAngle = TorsoAngle;
}
else if (mainLimb.type == LimbType.Head)
{
mainLimbTargetAngle = HeadAngle;
}
float torque = TailTorque;
float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier;
if (mainLimbTargetAngle.HasValue && maxMultiplier > 1)
{
float diff = Math.Abs(mainLimb.Rotation - tail.Rotation);
float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value);
torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset));
}
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque);
}
}
}
else
{
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
if (reverse)
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
}
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque);
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque);
}
if (TailAngle.HasValue)
{
bool isAngleApplied = false;
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
}
if (!isAngleApplied)
{
RotateTail(GetLimb(LimbType.Tail));
}
void RotateTail(Limb tail)
{
if (tail != null)
{
tail.body.SmoothRotate(TailAngle.Value * Dir, TailTorque);
}
}
}
}
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier);
if (waveLength > 0 && waveAmplitude > 0)
{
WalkPos -= transformedMovement.Length() / Math.Abs(waveLength);
WalkPos = MathUtils.WrapAngleTwoPi(WalkPos);
}
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
switch (limb.type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier);
}
break;
}
}
for (int i = 0; i < Limbs.Length; i++)
{
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = limb.PullJointWorldAnchorA;
limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
if (CurrentSwimParams.UseSineMovement)
{
mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Vector2.Lerp(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f);
}
}
foreach (var limb in Limbs)
@@ -543,55 +625,15 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
}
switch (limb.type)
if (limb.Params.BlinkFrequency > 0)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude);
}
break;
limb.Blink(deltaTime, MainLimb.Rotation);
}
}
for (int i = 0; i < Limbs.Length; i++)
{
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = limb.PullJointWorldAnchorA;
limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
if (CurrentSwimParams.UseSineMovement)
{
mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Vector2.Lerp(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f);
}
floorY = Limbs[0].SimPosition.Y;
floorY = Limbs[0].SimPosition.Y;
}
void UpdateWalkAnim(float deltaTime)
@@ -686,10 +728,26 @@ namespace Barotrauma
if (TailAngle.HasValue)
{
var tail = GetLimb(LimbType.Tail);
if (tail != null)
bool isAngleApplied = false;
foreach (var limb in Limbs)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque);
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
}
if (!isAngleApplied)
{
RotateTail(GetLimb(LimbType.Tail));
}
void RotateTail(Limb tail)
{
if (tail != null)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque);
}
}
}
@@ -709,7 +767,11 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
}
if (limb.Params.BlinkFrequency > 0)
{
limb.Blink(deltaTime, MainLimb.Rotation);
}
switch (limb.type)
{

View File

@@ -145,7 +145,7 @@ namespace Barotrauma
private set;
}
private LimbJoint shoulder;
private LimbJoint rightShoulder, leftShoulder;
private float upperLegLength = 0.0f, lowerLegLength = 0.0f;
@@ -241,12 +241,13 @@ namespace Barotrauma
Limb rightHand = GetLimb(LimbType.RightHand);
if (rightHand == null) { return; }
shoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm);
rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm);
leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm);
Vector2 localAnchorShoulder = Vector2.Zero;
Vector2 localAnchorElbow = Vector2.Zero;
if (shoulder != null)
if (rightShoulder != null)
{
localAnchorShoulder = shoulder.LimbA.type == LimbType.RightArm ? shoulder.LocalAnchorA : shoulder.LocalAnchorB;
localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB;
}
LimbJoint rightElbow = rightForearm == null ?
GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) :
@@ -1612,7 +1613,7 @@ namespace Barotrauma
pullLimb.PullJointMaxForce = 5000.0f;
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
Vector2 shoulderPos = shoulder.WorldAnchorA;
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
@@ -1757,10 +1758,10 @@ namespace Barotrauma
}
else
{
itemAngle = (torso.body.Rotation + holdAngle * Dir);
itemAngle = torso.body.Rotation + holdAngle * Dir;
}
Vector2 transformedHoldPos = shoulder.WorldAnchorA;
Vector2 transformedHoldPos = rightShoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || isClimbing || usingController)
{
if (character.SelectedItems[0] == item)
@@ -1781,15 +1782,17 @@ namespace Barotrauma
if (character.SelectedItems[0] == item)
{
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightShoulder.WorldAnchorA;
rightHand.Disabled = true;
}
if (character.SelectedItems[1] == item)
{
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftShoulder.WorldAnchorA;
leftHand.Disabled = true;
}
itemPos.X = itemPos.X * Dir;
itemPos.X *= Dir;
transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle));
}
@@ -1858,18 +1861,21 @@ namespace Barotrauma
private void HandIK(Limb hand, Vector2 pos, float force = 1.0f)
{
if (shoulder == null) { return; }
Vector2 shoulderPos = shoulder.WorldAnchorA;
Vector2 shoulderPos;
Limb arm, forearm;
if (hand.type == LimbType.LeftHand)
{
if (leftShoulder == null) { return; }
shoulderPos = leftShoulder.WorldAnchorA;
arm = GetLimb(LimbType.LeftArm);
forearm = GetLimb(LimbType.LeftForearm);
LeftHandIKPos = pos;
}
else
{
if (rightShoulder == null) { return; }
shoulderPos = rightShoulder.WorldAnchorA;
arm = GetLimb(LimbType.RightArm);
forearm = GetLimb(LimbType.RightForearm);
RightHandIKPos = pos;

View File

@@ -216,16 +216,18 @@ namespace Barotrauma
get
{
Limb mainLimb = GetLimb(RagdollParams.MainLimb);
if (mainLimb == null)
if (!IsValid(mainLimb))
{
Limb torso = GetLimb(LimbType.Torso);
Limb head = GetLimb(LimbType.Head);
mainLimb = torso ?? head;
if (mainLimb == null)
if (!IsValid(mainLimb))
{
mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions);
mainLimb = Limbs.FirstOrDefault(l => IsValid(l));
}
}
bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.ignoreCollisions;
return mainLimb;
}
}
@@ -753,7 +755,6 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (connectedLimbs.Contains(limb)) { continue; }
if (!character.IsDead && !limb.CanBeSeveredAlive) { continue; }
limb.IsSevered = true;
if (limb.type == LimbType.RightHand)
{
@@ -765,7 +766,6 @@ namespace Barotrauma
}
}
if (!string.IsNullOrEmpty(character.BloodDecalName))
{
character.CurrentHull?.AddDecal(character.BloodDecalName,

View File

@@ -131,7 +131,7 @@ namespace Barotrauma
protected float oxygenAvailable;
//seed used to generate this character
private readonly string seed;
public readonly string Seed;
protected Item focusedItem;
private Character selectedCharacter, selectedBy;
public Character LastAttacker;
@@ -273,7 +273,8 @@ namespace Barotrauma
{
if (IsPet)
{
return (AIController as EnemyAIController).PetBehavior.GetName();
string petName = (AIController as EnemyAIController).PetBehavior.GetTagName();
if (!string.IsNullOrEmpty(petName)) { return petName; }
}
if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; }
@@ -827,7 +828,7 @@ namespace Barotrauma
{
prefab = CharacterPrefab.FindBySpeciesName(speciesName);
this.seed = seed;
this.Seed = seed;
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
selectedItems = new Item[2];
@@ -2178,7 +2179,7 @@ namespace Barotrauma
}
else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
{
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play();
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
{
@@ -2918,7 +2919,7 @@ namespace Barotrauma
}
#endif
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController);
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker.IsHuman || attacker.IsPlayer);
return attackResult;
}
@@ -2944,7 +2945,8 @@ namespace Barotrauma
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (!joint.CanBeSevered) { continue; }
if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; }
// Limb A is where we usually create the joints from. Let's not allow severing when the "parent" limb is hit, or the head can pop off when we hit the torso, for example.
if (joint.LimbB != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
{
@@ -3225,6 +3227,11 @@ namespace Barotrauma
return;
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
}
IsDead = true;
ApplyStatusEffects(ActionType.OnDeath, 1.0f);

View File

@@ -768,6 +768,18 @@ namespace Barotrauma
{
attack.UpdateCoolDown(deltaTime);
}
if (Params.BlinkFrequency > 0)
{
if (blinkTimer > -TotalBlinkDurationOut)
{
blinkTimer -= deltaTime;
}
else
{
blinkTimer = Params.BlinkFrequency;
}
}
}
partial void UpdateProjSpecific(float deltaTime);
@@ -1039,6 +1051,45 @@ namespace Barotrauma
}
}
private float blinkTimer;
private float blinkPhase;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
public void Blink(float deltaTime, float referenceRotation)
{
if (blinkTimer > -TotalBlinkDurationOut)
{
blinkPhase -= deltaTime;
if (blinkPhase > 0)
{
// in
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
}
else
{
if (Math.Abs(blinkPhase) < Params.BlinkHoldTime)
{
// hold
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
else
{
// out
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
}
}
}
else
{
// out
blinkPhase = Params.BlinkDurationIn;
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
}
public void Remove()
{
body?.Remove();

View File

@@ -305,7 +305,7 @@ namespace Barotrauma
instance.Load(fullPath, speciesName);
anims.Add(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance as T;
return instance;
}
public bool Serialize() => base.Serialize();

View File

@@ -474,6 +474,12 @@ namespace Barotrauma
[Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
[Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
public float AvoidTime { get; private set; }
[Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)]
public float MinFleeTime { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
public bool AggressiveBoarding { get; private set; }
@@ -580,6 +586,9 @@ namespace Barotrauma
[Serialize(0f, true, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable]
public float Timer { get; set; }
[Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable]
public bool IgnoreContained { get; set; }
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }

View File

@@ -77,7 +77,7 @@ namespace Barotrauma
[Serialize(LimbType.Torso, true), Editable]
public LimbType MainLimb { get; set; }
private static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
private readonly static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
@@ -546,14 +546,17 @@ namespace Barotrauma
[Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
public LimbType Type { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360)]
public float SpriteOrientation { get; set; }
/// <summary>
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
/// </summary>
public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation);
[Serialize("", true), Editable]
public string Notes { get; set; }
[Serialize(1f, true), Editable]
public float Scale { get; set; }
[Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()]
public bool Flip { get; set; }
@@ -566,8 +569,8 @@ namespace Barotrauma
[Serialize(false, true, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
@@ -590,6 +593,9 @@ namespace Barotrauma
[Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable]
public float AngularDamping { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()]
public Vector2 PullPos { get; set; }
@@ -602,18 +608,12 @@ namespace Barotrauma
[Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
public Vector2 MouthPos { get; set; }
[Serialize("", true), Editable]
public string Notes { get; set; }
[Serialize(0f, true), Editable]
public float ConstantTorque { get; set; }
[Serialize(0f, true), Editable]
public float ConstantAngle { get; set; }
[Serialize(1f, true), Editable]
public float Scale { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
public float AttackForceMultiplier { get; set; }
@@ -624,6 +624,42 @@ namespace Barotrauma
[Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
public float SeveredFadeOutTime { get; set; } = 10.0f;
[Serialize(false, true, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
public bool ApplyTailAngle { get; set; }
[Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineFrequencyMultiplier { get; set; }
[Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineAmplitudeMultiplier { get; set; }
[Serialize(0f, true), Editable(0, 100, ValueStep = 1, DecimalCount = 1)]
public float BlinkFrequency { get; set; }
[Serialize(0.2f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkDurationIn { get; set; }
[Serialize(0.5f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkDurationOut { get; set; }
[Serialize(0f, true), Editable(0, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkHoldTime { get; set; }
[Serialize(0f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
public float BlinkRotationIn { get; set; }
[Serialize(45f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
public float BlinkRotationOut { get; set; }
[Serialize(50f, true), Editable]
public float BlinkForce { get; set; }
[Serialize(TransitionMode.Linear, true), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
[Serialize(TransitionMode.Linear, true), Editable]
public TransitionMode BlinkTransitionOut { get; private set; }
// Non-editable ->
// TODO: make read-only
[Serialize(0, true)]

View File

@@ -592,13 +592,13 @@ namespace Barotrauma
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
enemyDanger += enemyAI.CombatStrength / 1000.0f;
enemyDanger += enemyAI.CombatStrength / 100.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
//enemy outside and targeting the sub or something in it
//moloch adds 0.24 to enemy danger, a crawler 0.02
enemyDanger += enemyAI.CombatStrength / 2000.0f;
enemyDanger += enemyAI.CombatStrength / 1000.0f;
}
}
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
@@ -654,12 +654,12 @@ namespace Barotrauma
if (targetIntensity > currentIntensity)
{
//25 seconds for intensity to go from 0.0 to 1.0
currentIntensity = MathHelper.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity);
currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity);
}
}

View File

@@ -30,6 +30,8 @@ namespace Barotrauma
public CampaignMetadata CampaignMetadata;
protected XElement petsElement;
public enum TransitionType
{
None,

View File

@@ -282,6 +282,11 @@ namespace Barotrauma
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
return;
}
if (SubmarineInfo.SubmarineElement.Elements().Count() == 0)
{
DebugConsole.ThrowError("Couldn't start game session, saved submarine is empty. The submarine file may be corrupted.");
return;
}
LevelData = levelData;

View File

@@ -6,18 +6,20 @@ namespace Barotrauma.Items.Components
{
class Throwable : Holdable
{
private float throwForce, throwPos;
private float throwPos;
private bool throwing, throwDone;
private bool midAir;
[Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")]
public float ThrowForce
public Character CurrentThrower
{
get { return throwForce; }
set { throwForce = value; }
get;
private set;
}
[Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")]
public float ThrowForce { get; set; }
public Throwable(Item item, XElement element)
: base(item, element)
{
@@ -60,6 +62,21 @@ namespace Barotrauma.Items.Components
{
if (item.body.LinearVelocity.LengthSquared() < 0.01f)
{
CurrentThrower = null;
if (statusEffectLists.ContainsKey(ActionType.OnImpact))
{
foreach (var statusEffect in statusEffectLists[ActionType.OnImpact])
{
statusEffect.SetUser(null);
}
}
if (statusEffectLists.ContainsKey(ActionType.OnBroken))
{
foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
{
statusEffect.SetUser(null);
}
}
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform;
midAir = false;
}
@@ -117,9 +134,24 @@ namespace Barotrauma.Items.Components
#if SERVER
GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction);
#endif
Character thrower = picker;
item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
CurrentThrower = picker;
if (statusEffectLists.ContainsKey(ActionType.OnImpact))
{
foreach (var statusEffect in statusEffectLists[ActionType.OnImpact])
{
statusEffect.SetUser(CurrentThrower);
}
}
if (statusEffectLists.ContainsKey(ActionType.OnBroken))
{
foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
{
statusEffect.SetUser(CurrentThrower);
}
}
item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
//disable platform collisions until the item comes back to rest again
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
@@ -135,12 +167,12 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, thrower.ID });
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, CurrentThrower.ID });
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, thrower, user: thrower);
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
}
throwing = false;
}

View File

@@ -349,6 +349,14 @@ namespace Barotrauma.Items.Components
}
}
public override void OnItemLoaded()
{
if (item.Submarine == null || !item.Submarine.Loading)
{
SpawnAlwaysContainedItems();
}
}
public override void OnMapLoaded()
{
if (itemIds != null)
@@ -361,7 +369,11 @@ namespace Barotrauma.Items.Components
}
itemIds = null;
}
SpawnAlwaysContainedItems();
}
private void SpawnAlwaysContainedItems()
{
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
@@ -375,6 +387,7 @@ namespace Barotrauma.Items.Components
}
}
protected override void ShallowRemoveComponentSpecific()
{
}

View File

@@ -1658,10 +1658,14 @@ namespace Barotrauma
public override void FlipX(bool relativeToSub)
{
if (!Prefab.CanFlipX) { return; }
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
base.FlipX(relativeToSub);
if (!Prefab.CanFlipX)
{
flippedX = false;
return;
}
#if CLIENT
if (Prefab.CanSpriteFlipX)
{
@@ -1677,10 +1681,15 @@ namespace Barotrauma
public override void FlipY(bool relativeToSub)
{
if (!Prefab.CanFlipY) { return; }
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
base.FlipY(relativeToSub);
if (!Prefab.CanFlipY)
{
flippedY = false;
return;
}
#if CLIENT
if (Prefab.CanSpriteFlipY)
{

View File

@@ -1340,6 +1340,9 @@ namespace Barotrauma
decalsCleaned = true;
#if SERVER
decalUpdatePending = true;
#elif CLIENT
pendingDecalUpdates.Add(decal);
networkUpdatePending = true;
#endif
}
}

View File

@@ -833,7 +833,7 @@ namespace Barotrauma
if (location.Discovered)
{
#if CLIENT
RemoveFogOfWar(StartLocation);
RemoveFogOfWar(location);
#endif
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{

View File

@@ -50,7 +50,7 @@ namespace Barotrauma
//observable collection because some entities may need to be notified when the collection is modified
public readonly ObservableCollection<MapEntity> linkedTo = new ObservableCollection<MapEntity>();
private bool flippedX, flippedY;
protected bool flippedX, flippedY;
public bool FlippedX { get { return flippedX; } }
public bool FlippedY { get { return flippedY; } }
@@ -534,7 +534,7 @@ namespace Barotrauma
public virtual void FlipX(bool relativeToSub)
{
flippedX = !flippedX;
if (!relativeToSub || Submarine == null) return;
if (!relativeToSub || Submarine == null) { return; }
Vector2 relative = WorldPosition - Submarine.WorldPosition;
relative.Y = 0.0f;
@@ -548,7 +548,7 @@ namespace Barotrauma
public virtual void FlipY(bool relativeToSub)
{
flippedY = !flippedY;
if (!relativeToSub || Submarine == null) return;
if (!relativeToSub || Submarine == null) { return; }
Vector2 relative = WorldPosition - Submarine.WorldPosition;
relative.X = 0.0f;

View File

@@ -415,7 +415,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e);
DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e);
return false;
}
}

View File

@@ -733,9 +733,10 @@ namespace Barotrauma
}
else
{
if (targets.FirstOrDefault(t => t is MapEntity) is MapEntity targetEntity && !targetEntity.Removed)
var targetLimb = targets.FirstOrDefault(t => t is Limb) as Limb;
if (targetLimb != null && !targetLimb.Removed)
{
position = targetEntity.WorldPosition;
position = targetLimb.WorldPosition;
}
}
}

View File

@@ -1,40 +1,10 @@
---------------------------------------------------------------------------------------------------------
v0.10.601.0 (Unstable)
---------------------------------------------------------------------------------------------------------
- Added 2 pets: Peanut and Psilotoad. Currently only obtainable via console commands ("spawnitem peanutegg", "spawnitem psilotoadegg", "spawn peanut" or "spawn psilotoad").
- Improvements to the Watcher.
- Fixed 2 equipped storage containers from one hand to another causing one of them to get stuck mid-air.
- Fixed a crash caused by humanoid enemies (e.g. husks).
- Moved toolbelt slot to the right side of the generic slots, added inventory icon for the slot.
- Fixed EventManager crashing if there are no event sets configured for the current location type (only affected mods that add new location types without adding any events for them).
- Use player name instead of server name for the server owner when hosting a server.
- Fixed diving suit's low oxygen warning beep not following the player wearing the diving suit.
- Made large monsters immune to paralyzant (mudraptor is the largest affected monster).
- Fixed bots not reacting to player reports in multiplayer.
- Added more copper to chalcopyrite and bornite deconstruct recipe.
- More calcium for aragonite, adjusted prices.
- Fixed fires not damaging characters.
- Set terminal's maximum message length to match maximum chat message length (otherwise chat-linked terminals work differently in multiplayer).
- Fixed inability to unlink hulls in the sub editor.
- Fixed bots "cleaning up" components attached to walls.
- Fixed yet another cause for "missing entity" errors. Occasionally happened in monster missions when a monster happened to get assigned the same ID as an item in a player's inventory.
- Fixed items held in the left hand being drawn in front of the characters.
- Allow closing the splash screens with esc.
- Fixed "inventory sizes don't match" error when a human becomes a husk.
- Fixed ability to drag and drop items into outpost reactors.
- Fixed torso getting hidden when wearing a toolbelt.
- Fixed coilgun ammo only using 90% of the fabrication materials.
- Fixed paints reverting to the dirt color client-side after spraying.
- Added missing dialog for the new "Cleanup Items" order.
- Rebalanced upgrade parameters, allowing for more noticable benefits.
---------------------------------------------------------------------------------------------------------
v0.10.600.0 (Unstable)
v0.10.6.0 (Unstable)
---------------------------------------------------------------------------------------------------------
Changes and additions:
- Reworked Watcher.
- Added pets (can be obtained by buying eggs from outposts). The pets produce items that can be used for crafting if they're kept happy and well-fed.
- Added a new monster behavior: observe.
- Added toolbelt (a wearable container with a capacity of 12 and it's own dedicated slot) as a replacement for the toolbox.
- Improvements to the effects caused by psychosis: the affliction icon is not visible to the psychotic character, the fake fires and floods are a bit more convincing, the affliction plays random sounds and can cause other characters to become invisible.
@@ -66,12 +36,20 @@ Changes and additions:
- A minor change to the status effect condition targeting logic: If "This" and "NearbyCharacters" are both defined as the targets of a status effect, the conditions only apply to "this" entity, even though the effects are applied on all the targets.
- Implement spread, speed, and rotation for the spawn item status effects.
- Minor damage (less than 1 hp) doesn't spawn particles anymore.
- Rebalanced upgrade parameters, allowing for more noticable benefits.
- Allow closing the splash screens with esc.
- Added more copper to chalcopyrite and bornite deconstruct recipe.
- More calcium for aragonite, adjusted prices.
- Made large monsters immune to paralyzant (mudraptor is the largest affected monster).
- Use player name instead of server name for the server owner when hosting a server.
- Don't draw turret range indicators in the sub editor when the turret isn't selected.
Character Editor:
- Fixed a number of issues with the joint limit widgets. Also allowed to set a joint to rotate clockwise, which inverses the widget direction. Useful for heads or other limbs that extrude right from the main body.
- Inversed the default joint ends, because it's more usual case to edit the second limb of the joint than the first.
- The colliders of the hidden limbs are now hidden in the game view.
- Changed the hotkey for toggling the parameter editor from "Tab" to "F1" and fix the inability to toggle the editor when a text field is selected.
- Fixed load and save interfaces being broken on lower resolutions.
Sounds:
- Added 2 new background music tracks
@@ -110,6 +88,8 @@ AI improvements and fixes:
Misc fixes:
- Fixed clients always getting the generic "could not connect" error message when connecting to a server fails, even if there's a specific reason to the connection failing (e.g. disallowed symbols in the player's name, mismatching content packages or game version).
- Fixed yet another cause for "missing entity" errors. Occasionally happened in monster missions when a monster happened to get assigned the same ID as an item in a player's inventory.
- Fixed clients spawning a respawn shuttle in non-campaign missions even if the server has disabled respawning, leading to "missing entity" errors.
- Fixed planters dropping removed seeds after a save is reloaded.
- Fixed plants not updating the health after being fully grown in multiplayer.
- Fixed decal syncing working unreliably in multiplayer.
@@ -145,6 +125,26 @@ Misc fixes:
- OnDamaged status effects now launch only when there's any damage. Not when the damage is zero.
- Fixed health bar pulsating even when no damage is done by an attack/status effect.
- Fixed affliction probability not having any effect when used in status effects.
- Fixed EventManager crashing if there are no event sets configured for the current location type (only affected mods that add new location types without adding any events for them).
- Fixed items held in the left hand being drawn in front of the characters.
- Fixed ability to drag and drop items into outpost reactors.
- Set terminal's maximum message length to match maximum chat message length (otherwise chat-linked terminals work differently in multiplayer).
- Fixed diving suit's low oxygen warning beep not following the player wearing the suit.
- Fixed "propaganda" and "clown outbreak" outpost events never triggering.
- Fixed "clown brutality" event getting stuck after the NPCs have been spawned.
- Fixed "impromptu engineering" event giving only one coilgun ammo box despite the text saying 2.
- Fixed "black market" event always giving the player the alien pistol.
- Fixed incendium bars exploding when pressing the Use key while holding one.
- Fixed outpost security not reacting to players throwing items that explode on impact (e.g. flash powder or nitroglycerin).
- Fixed outpost security reacting to raptor bane extract injections with lethal force.
- Fixed mantis' animation.
- Fixed multiple wire nodes occasionally getting placed with one click when rewiring.
- Fixed fire and water flow sounds staying active when returning from a multiplayer session to the main menu.
- Fixed previously discovered map tiles becoming undiscovered when saving and loading a campaign.
- Fixed 1st shot from SMG magazines spawned with console commands doing nothing.
- Fixed calyxanide not damaging huskified humans or crawlers.
- Fixed explosives exploding when combining them results in one being removed.
- Fixed items that don't flip horizontally being positioned incorrectly in mirrored subs.
---------------------------------------------------------------------------------------------------------
v0.10.5.1