Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -1,4 +1,5 @@
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
@@ -44,7 +45,12 @@ namespace Barotrauma.Items.Components
private bool attachable, attached, attachedByDefault;
private Voronoi2.VoronoiCell attachTargetCell;
private PhysicsBody body;
/// <summary>
/// The item's original physics body (if one exists). When the item is attached to a wall, it's <see cref="Item.body"/> gets set to null,
/// and we use this field to keep track of the original body.
/// </summary>
private PhysicsBody originalBody;
public readonly ImmutableDictionary<StatTypes, float> HoldableStatValues;
@@ -62,7 +68,7 @@ namespace Barotrauma.Items.Components
public PhysicsBody Body
{
get { return item.body ?? body; }
get { return item.body ?? originalBody; }
}
[Serialize(false, IsPropertySaveable.Yes, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")]
@@ -84,6 +90,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, IsPropertySaveable.Yes, description: "Camera offset to apply when aiming this item. Only valid if Aimable is set to true.")]
public float CameraAimOffset { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")]
public bool ControlPose
{
@@ -119,6 +128,32 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "When enabled, the item can only be attached to a position where it touches the floor.")]
public bool AttachesToFloor
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can the item be attached inside doors?")]
public bool AllowAttachInsideDoors
{
get;
set;
}
private HashSet<Identifier> disallowAttachingOverTags = new HashSet<Identifier>();
[Editable, Serialize("", IsPropertySaveable.Yes)]
public string DisallowAttachingOverTags
{
get => disallowAttachingOverTags.ConvertToString();
set
{
disallowAttachingOverTags = value.ToIdentifiers().ToHashSet();
}
}
[Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
@@ -275,7 +310,7 @@ namespace Barotrauma.Items.Components
public Holdable(Item item, ContentXElement element)
: base(item, element)
{
body = item.body;
originalBody = item.body;
Pusher = null;
if (element.GetAttributeBool("blocksplayers", false))
@@ -411,9 +446,9 @@ namespace Barotrauma.Items.Components
if (attachable)
{
if (body != null)
if (originalBody != null)
{
item.body = body;
item.body = originalBody;
}
DeattachFromWall();
}
@@ -514,9 +549,9 @@ namespace Barotrauma.Items.Components
if (character != null) { item.Submarine = character.Submarine; }
if (item.body == null)
{
if (body != null)
if (originalBody != null)
{
item.body = body;
item.body = originalBody;
}
else
{
@@ -565,13 +600,45 @@ namespace Barotrauma.Items.Components
public bool CanBeAttached(Character user)
{
return CanBeAttached(user, out _);
}
private static List<Item> tempOverlappingItems = new List<Item>();
private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems)
{
tempOverlappingItems.Clear();
overlappingItems = tempOverlappingItems;
if (!attachable || !Reattachable) { return false; }
//can be attached anywhere in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
if (AttachesToFloor && item.CurrentHull == null) { return false; }
Vector2 attachPos = user == null ? item.WorldPosition : GetAttachPosition(user, useWorldCoordinates: true);
if (disallowAttachingOverTags.Any() || !AllowAttachInsideDoors)
{
var connectedHulls = item.CurrentHull?.GetConnectedHulls(includingThis: true, searchDepth: 5, ignoreClosedGaps: true);
Vector2 size = item.Rect.Size.ToVector2() / 2;
foreach (Item otherItem in Item.ItemList)
{
if (otherItem == item || otherItem.body is { BodyType: BodyType.Dynamic, Enabled: true }) { continue; }
if (connectedHulls != null && !connectedHulls.Contains(otherItem.CurrentHull)) { continue; }
if (disallowAttachingOverTags.None(tag => otherItem.HasTag(tag)) &&
(otherItem.GetComponent<Door>() == null || AllowAttachInsideDoors))
{
continue;
}
Rectangle worldRect = otherItem.WorldRect;
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
tempOverlappingItems.Add(otherItem);
}
if (tempOverlappingItems.Any()) { return false; }
}
//can be attached anywhere inside hulls
if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; }
@@ -670,14 +737,19 @@ namespace Barotrauma.Items.Components
{
if (!attachable) { return; }
if (body == null)
if (originalBody == null)
{
throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
}
body.Enabled = false;
body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
item.body = null;
originalBody.Enabled = false;
originalBody.SetTransformIgnoreContacts(originalBody.SimPosition, rotation: 0.0f);
if (item.body != null)
{
item.body.Dir = 1;
item.body = null;
}
item.GetComponents<LightComponent>().ForEach(static light => light.SetLightSourceTransform());
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
if (item.CurrentHull == null && item.Submarine == null)
@@ -689,7 +761,7 @@ namespace Barotrauma.Items.Components
{
//set to submarine-relative position
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
body.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
originalBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
}
item.Submarine = attachTarget.Submarine;
}
@@ -833,6 +905,9 @@ namespace Barotrauma.Items.Components
{
item.Drop(character);
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false);
//don't find the new hull in SetTransform, because that'd also potentially change the submarine (teleport the item outside if it's attached outside)
//instead just find the hull, so the item is considered to be in the right hull
item.CurrentHull = Hull.FindHull(item.WorldPosition, item.CurrentHull);
//the light source won't get properly updated if lighting is disabled (even though the light sprite is still drawn when lighting is disabled)
//so let's ensure the light source is up-to-date
RefreshLightSources(item);
@@ -869,34 +944,60 @@ namespace Barotrauma.Items.Components
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
Vector2 submarinePos = useWorldCoordinates && user.Submarine != null ? user.Submarine.Position : Vector2.Zero;
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
Vector2 attachPos = userPos + mouseDiff;
Vector2 halfSize = new Vector2(item.Rect.Width, item.Rect.Height) / 2;
//offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor
//in the sub editor, we align the top-left corner of the item with the grid
//but here the origin of the item is placed at the attach position, so we need to offset it
Vector2 offset = new Vector2(
-(item.Rect.Width / 2) % Submarine.GridSize.X,
(item.Rect.Height / 2) % Submarine.GridSize.Y);
-halfSize.X % Submarine.GridSize.X,
halfSize.Y % Submarine.GridSize.Y);
if (user.Submarine != null)
{
//we must add some "padding" to the raycast to ensure it reaches all the way to a wall
//otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside
Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
Vector2 padding = halfSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(user.Position),
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null)
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall,
/*don't ignore sensors so the raycast can hit open doors or broken walls*/
ignoreSensors: AllowAttachInsideDoors, customPredicate: (Fixture fixture) =>
{
if (fixture.UserData is Door) { return false; }
return true;
}) != null)
{
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
Vector2 pickedPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset - submarinePos;
//round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
return
attachPos =
new Vector2(
(mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X,
(mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y)
- offset;
RoundToGrid(pickedPos.X, Submarine.GridSize.X, roundingDir: -Math.Sign(mouseDiff.X)),
RoundToGrid(pickedPos.Y, Submarine.GridSize.Y, roundingDir: -Math.Sign(mouseDiff.Y)))
- offset + submarinePos;
}
if (AttachesToFloor)
{
//if attaching to floor, do a raycast down and move the attach pos where it hits
float size = item.Rect.Height / 2.0f;
Vector2 rayStart = attachPos - submarinePos;
Vector2 rayEnd = rayStart - Vector2.UnitY * MaxAttachDistance * 2;
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(rayStart),
ConvertUnits.ToSimUnits(rayEnd), collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform) != null)
{
attachPos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * size + submarinePos;
}
else
{
return Vector2.Zero;
}
}
}
else if (Level.Loaded != null)
@@ -919,9 +1020,30 @@ namespace Barotrauma.Items.Components
}
}
return new Vector2(
MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset;
//subtract the submarine position so we're doing the rounding in the sub's
//internal/local coordinate space regardless if we're using world coordinates
//(otherwise the rounding would behave differently depending on the value of useWorldCoordinates)
Vector2 offsetAttachPos = attachPos + offset - submarinePos;
return
new Vector2(
RoundToGrid(offsetAttachPos.X, Submarine.GridSize.X),
//don't round the vertical position if we're attaching to floor - we want the item to align with the floor, not the grid
AttachesToFloor ? offsetAttachPos.Y : RoundToGrid(offsetAttachPos.Y, Submarine.GridSize.Y))
- offset + submarinePos;
///<param name="roundingDir">If < 0, the method rounds down. If > 0, rounds up. If 0, rounds to the closest integer.</param>
static float RoundToGrid(float position, float gridSize, int roundingDir = 0)
{
if (roundingDir < 0)
{
return MathF.Floor(position / gridSize) * gridSize;
}
else if (roundingDir > 0)
{
return MathF.Ceiling(position / gridSize) * gridSize;
}
return MathUtils.RoundTowardsClosest(position, gridSize);
}
}
private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
@@ -977,7 +1099,7 @@ namespace Barotrauma.Items.Components
return;
}
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && CanBeAttached(picker))
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && attachable && Reattachable)
{
Drawable = true;
}
@@ -1115,11 +1237,11 @@ namespace Barotrauma.Items.Components
}
else
{
if (body != null)
if (originalBody != null)
{
body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
item.body = body;
body.Enabled = item.ParentInventory == null;
originalBody.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
item.body = originalBody;
originalBody.Enabled = item.ParentInventory == null;
}
DeattachFromWall();
}
@@ -1134,7 +1256,7 @@ namespace Barotrauma.Items.Components
Pusher.Remove();
Pusher = null;
}
body = null;
originalBody = null;
}
public override XElement Save(XElement parentElement)
@@ -435,55 +435,65 @@ namespace Barotrauma.Items.Components
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
if (Attack != null)
{
Attack.SetUser(user);
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
bool applyAttack = true;
if (Attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(targetEntity as ISerializableEntity)) ||
Attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(user)))
{
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
applyAttack = false;
}
else if (targetCharacter != null)
if (applyAttack)
{
if (targetCharacter.Removed) { return; }
targetCharacter.LastDamageSource = item;
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if (targetStructure != null)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
{
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
fullColor: GUIStyle.HealthBarColorHigh,
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
}
else if (targetCharacter != null)
{
if (targetCharacter.Removed) { return; }
targetCharacter.LastDamageSource = item;
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if (targetStructure != null)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
fullColor: GUIStyle.HealthBarColorHigh,
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
}
#endif
}
else if (target.UserData is Holdable { CanPush: true } holdable)
{
if (holdable.Item.Removed) { return; }
RestoreCollision();
hitting = false;
User = null;
}
else
{
return;
}
}
else if (target.UserData is Holdable { CanPush: true } holdable)
{
if (holdable.Item.Removed) { return; }
RestoreCollision();
hitting = false;
User = null;
}
else
{
return;
}
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -522,7 +522,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
float barOffset = 10f * GUI.Scale;
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxWater, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
#endif
}
}
@@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
//actual throwing logic is handled in Update
return characterUsable || character == null;
return (characterUsable && !UsageDisabledByRangedWeapon(character)) || character == null;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
@@ -111,24 +111,28 @@ namespace Barotrauma.Items.Components
return;
}
if (throwState != ThrowState.Throwing)
bool aim = false;
if (!UsageDisabledByRangedWeapon(picker))
{
if (picker.IsKeyDown(InputType.Aim))
if (throwState != ThrowState.Throwing)
{
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
if (picker.IsKeyDown(InputType.Aim))
{
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
}
else if (throwState != ThrowState.Initiated)
{
throwAngle = ThrowAngleStart;
}
}
else if (throwState != ThrowState.Initiated)
{
throwAngle = ThrowAngleStart;
}
}
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
}
if (picker.IsDead || !picker.AllowInput)
{
throwState = ThrowState.None;
aim = false;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
//return if the status effect got rid of the picker somehow