v1.0.13.1 (first post-1.0 patch)

This commit is contained in:
Regalis11
2023-05-10 15:07:17 +03:00
parent 96fb49ba14
commit ee1db852b1
272 changed files with 5738 additions and 2413 deletions
@@ -24,7 +24,7 @@ namespace Barotrauma
}
public static readonly List<InvSlotType> anySlot = new List<InvSlotType>() { InvSlotType.Any };
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
protected bool[] IsEquipped;
@@ -226,7 +226,7 @@ namespace Barotrauma
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
var wearable = item.GetComponent<Wearable>();
if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1)
if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item))
{
return false;
}
@@ -336,7 +336,7 @@ namespace Barotrauma
//try to place the item in a LimbSlot.Any slot if that's allowed
if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any))
{
int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot);
int freeIndex = GetFreeAnySlot(item, inWrongSlot);
if (freeIndex > -1)
{
PutItem(item, freeIndex, user, true, createNetworkEvent);
@@ -393,7 +393,9 @@ namespace Barotrauma
return placedInSlot > -1;
}
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
private int GetFreeAnySlot(Item item, bool inWrongSlot)
{
//attempt to stack first
for (int i = 0; i < capacity; i++)
@@ -561,6 +561,12 @@ namespace Barotrauma.Items.Components
gap.ConnectedDoor = null;
}
}
if (OutsideSubmarineFixture != null)
{
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
OutsideSubmarineFixture = null;
}
//no need to remove the gap if we're unloading the whole submarine
//otherwise the gap will be removed twice and cause console warnings
@@ -600,7 +600,7 @@ namespace Barotrauma.Items.Components
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
//no further data needed, the event just triggers the discharge
msg.WriteUInt16(user?.ID ?? Entity.NullEntityID);
}
}
}
@@ -700,7 +700,8 @@ namespace Barotrauma.Items.Components
}
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
if (maxAttachableCount == 0)
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
{
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Projectile.GetSpreadFromPool(projectile.SpreadCounter);
float spread = GetSpread(character) * projectile.GetSpreadFromPool();
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
@@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool(projectile.SpreadCounter));
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
}
Item.RemoveContained(projectile.Item);
}
@@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
public bool RepairMultiple { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")]
public bool RepairMultipleWalls { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
@@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
}
if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent<Door>() != null)) { break; }
Character hitCharacter = null;
if (body.UserData is Limb limb)
@@ -236,7 +236,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")]
public float CombatPriority { get; private set; }
/// <summary>
@@ -687,9 +687,10 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
/// <summary>
/// Shorthand for !HasRequiredContainedItems()
/// </summary>
public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false);
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
{
@@ -511,25 +511,28 @@ namespace Barotrauma.Items.Components
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
effect.Apply(ActionType.OnContaining, deltaTime, item, character);
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
effect.AddNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
}
}
@@ -267,7 +267,7 @@ namespace Barotrauma.Items.Components
itemList.Enabled = true;
if (amountInput != null)
{
amountInput.Enabled = true;
amountInput.Enabled = amountTextMax.Enabled;
}
RefreshActivateButtonText();
#endif
@@ -14,19 +14,18 @@ namespace Barotrauma.Items.Components
{
partial class Projectile : ItemComponent, IServerSerializable
{
const int SpreadCounterWrapAround = 256;
private static readonly ImmutableArray<float> spreadPool;
static Projectile()
{
MTRandom random = new MTRandom(0);
spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
}
public static float GetSpreadFromPool(int seed)
public static byte SpreadCounter { get; private set; }
public static void ResetSpreadCounter()
{
if (seed < 0) { seed = -seed; }
return spreadPool[seed % SpreadCounterWrapAround];
SpreadCounter = 0;
}
struct HitscanResult
@@ -63,7 +62,7 @@ namespace Barotrauma.Items.Components
private bool removePending;
public byte SpreadCounter { get; private set; }
private byte spreadIndex;
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -212,7 +211,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")]
[Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")]
public bool StaticSpread
{
get;
@@ -300,7 +299,8 @@ namespace Barotrauma.Items.Components
return;
}
SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround);
spreadIndex = SpreadCounter;
SpreadCounter++;
InitProjSpecific(element);
}
@@ -329,6 +329,12 @@ namespace Barotrauma.Items.Components
originalCollisionTargets = item.body.CollidesWith;
}
public float GetSpreadFromPool()
{
spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
return spreadPool[spreadIndex];
}
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
if (Item.body == null) { return; }
@@ -380,8 +386,8 @@ namespace Barotrauma.Items.Components
if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
#if SERVER
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1)));
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
#endif
}
}
@@ -390,24 +396,22 @@ namespace Barotrauma.Items.Components
{
if (character != null && !characterUsable) { return false; }
if (item.body == null) { return false; }
//can't launch if already launched
if (StickTarget != null || IsActive) { return false; }
float initialRotation = item.body.Rotation;
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle;
if (StaticSpread)
{
float staticSpread = Spread / (HitScanCount - 1);
// because the position of the item changes as hitscan are fired, we will set an
// initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired
float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f;
launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset);
launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
}
else
{
launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * GetSpreadFromPool(SpreadCounter));
launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
}
SpreadCounter++;
spreadIndex++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
@@ -243,14 +243,7 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < labels.Length; i++)
{
labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label;
if (Screen.Selected != GameMain.SubEditorScreen)
{
customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value;
}
else
{
customInterfaceElementList[i].Label = labels[i];
}
customInterfaceElementList[i].Label = labels[i];
}
UpdateLabelsProjSpecific();
}
@@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components
Position = item.Position,
CastShadows = castShadows,
IsBackground = drawBehindSubs,
SpriteScale = Vector2.One * item.Scale,
SpriteScale = Vector2.One * item.Scale * LightSpriteScale,
Range = range
};
Light.LightSourceParams.Flicker = flicker;
@@ -309,13 +309,14 @@ namespace Barotrauma.Items.Components
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
if (item.Container != null && item.GetRootInventoryOwner() is not Character)
var ownerCharacter = item.GetRootInventoryOwner() as Character;
if ((item.Container != null && ownerCharacter == null) ||
(ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f))
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
return;
}
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
@@ -56,11 +56,9 @@ namespace Barotrauma.Items.Components
private float resetUserTimer;
private float aiTargetingGraceTimer;
private float aiFindTargetTimer;
private ISpatialEntity currentTarget;
private const float CrewAiFindTargetMaxInterval = 3.0f;
private const float CrewAiFindTargetMaxInterval = 1.0f;
private const float CrewAIFindTargetMinInverval = 0.2f;
private int currentLoaderIndex;
@@ -299,7 +297,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
[Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
public float AIRange
{
get;
@@ -422,10 +420,7 @@ namespace Barotrauma.Items.Components
// Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it.
if (lc?.Parent == this)
{
if (lightComponents == null)
{
lightComponents = new List<LightComponent>();
}
lightComponents ??= new List<LightComponent>();
lightComponents.Add(lc);
}
}
@@ -439,6 +434,8 @@ namespace Barotrauma.Items.Components
light.Parent = null;
light.Rotation = Rotation - item.RotationRad;
light.Light.Rotation = -rotation;
//turret lights are high-prio (don't want the lights to disappear when you're fighting something)
light.Light.PriorityMultiplier *= 10.0f;
}
}
#endif
@@ -590,10 +587,6 @@ namespace Barotrauma.Items.Components
angularVelocity *= -0.5f;
}
if (aiTargetingGraceTimer > 0f)
{
aiTargetingGraceTimer -= deltaTime;
}
if (aiFindTargetTimer > 0.0f)
{
aiFindTargetTimer -= deltaTime;
@@ -620,10 +613,18 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime);
private bool isUseBeingCalled;
public override bool Use(float deltaTime, Character character = null)
{
if (!characterUsable && character != null) { return false; }
return TryLaunch(deltaTime, character);
//prevent an infinite loop if launching triggers a StatusEffect that Uses this item
if (isUseBeingCalled) { return false; }
isUseBeingCalled = true;
bool wasSuccessful = TryLaunch(deltaTime, character);
isUseBeingCalled = false;
return wasSuccessful;
}
public float GetPowerRequiredToShoot()
@@ -1015,6 +1016,7 @@ namespace Barotrauma.Items.Components
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDist) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
target = targetItem;
closestDist = dist / priority;
@@ -1130,9 +1132,12 @@ namespace Barotrauma.Items.Components
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 10.0f);
if (previousTarget.LastAttacker == null || previousTarget.LastAttacker == character)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 5.0f);
}
character.AIController.SelectTarget(null);
}
@@ -1265,18 +1270,27 @@ namespace Barotrauma.Items.Components
Vector2? targetPos = null;
float maxDistance = 10000;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
// use full range only if we're actively firing
if (aiTargetingGraceTimer <= 0f)
{
shootDistance *= 0.75f;
}
float closestDistance = maxDistance * maxDistance;
bool hadCurrentTarget = currentTarget != null;
if (hadCurrentTarget)
{
if (!IsValidTarget(currentTarget))
bool isValidTarget = IsValidTarget(currentTarget);
if (isValidTarget)
{
float dist = Vector2.DistanceSquared(item.WorldPosition, currentTarget.WorldPosition);
if (dist > closestDistance)
{
isValidTarget = false;
}
else if (currentTarget is Item targetItem)
{
if (!IsTargetItemCloseEnough(targetItem, dist))
{
isValidTarget = false;
}
}
}
if (!isValidTarget)
{
currentTarget = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
@@ -1292,7 +1306,11 @@ namespace Barotrauma.Items.Components
if (character.Submarine != null)
{
if (enemy.Submarine == character.Submarine) { continue; }
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.Submarine != null)
{
if (enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.Submarine.Info.IsOutpost) { continue; }
}
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
@@ -1318,6 +1336,7 @@ namespace Barotrauma.Items.Components
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
targetPos = targetItem.WorldPosition;
closestDistance = dist / priority;
@@ -1325,14 +1344,7 @@ namespace Barotrauma.Items.Components
closestEnemy = null;
currentTarget = targetItem;
}
if (currentTarget == null)
{
aiFindTargetTimer = CrewAIFindTargetMinInverval;
}
else
{
aiFindTargetTimer = CrewAiFindTargetMaxInterval;
}
aiFindTargetTimer = currentTarget == null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval;
}
else if (currentTarget != null)
{
@@ -1346,6 +1358,10 @@ namespace Barotrauma.Items.Components
if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
{
targetPos = targetCharacter.CurrentHull.WorldPosition;
if (closestDistance > maxDistance * maxDistance)
{
ResetTarget();
}
}
else
{
@@ -1365,12 +1381,17 @@ namespace Barotrauma.Items.Components
}
if (closestDist > shootDistance * shootDistance)
{
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
ResetTarget();
}
}
void ResetTarget()
{
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
}
}
else if (targetPos == null && item.Submarine != null && Level.Loaded != null)
{
@@ -1520,10 +1541,11 @@ namespace Barotrauma.Items.Components
}
character.SetInput(InputType.Shoot, true, true);
}
aiTargetingGraceTimer = 5f;
return false;
}
private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
/// <summary>
/// Turret doesn't consume grid power, directly takes from the batteries on its grid instead.
/// </summary>
@@ -1553,6 +1575,10 @@ namespace Barotrauma.Items.Components
{
return false;
}
if (targetItem.ParentInventory != null)
{
return false;
}
}
return true;
}
@@ -1568,6 +1594,7 @@ namespace Barotrauma.Items.Components
{
if (item.Submarine != null)
{
if (item.Submarine.Info.IsOutpost) { return false; }
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans;
}
@@ -1660,7 +1687,7 @@ namespace Barotrauma.Items.Components
return angle >= minRotation && angle <= maxRotation;
}
private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
protected override void RemoveComponentSpecific()
{
@@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components
public bool AutoEquipWhenFull { get; private set; }
public bool DisplayContainedStatus { get; private set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
[Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn.")]
public bool AllowUseWhenWorn { get; set; }
public readonly int Variants;
@@ -742,13 +742,13 @@ namespace Barotrauma
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent))
&&
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent));
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent));
}
else
{
swapSuccessful =
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent))
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent))
&&
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
@@ -651,11 +651,13 @@ namespace Barotrauma
}
}
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool AllowStealing
private bool allowStealing;
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true,
description: $"Determined by where/how the item originally spawned. If ItemPrefab.AllowStealing is true, stealing the item is always allowed.")]
public bool AllowStealing
{
get;
set;
get { return allowStealing || Prefab.AllowStealingAlways; }
set { allowStealing = value; }
}
private string originalOutpost;
@@ -1585,6 +1587,17 @@ namespace Barotrauma
return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
}
public bool HasIdentifierOrTags(IEnumerable<Identifier> identifiersOrTags)
{
if (identifiersOrTags == null) { return false; }
if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; }
foreach (Identifier tag in identifiersOrTags)
{
if (HasTag(tag)) { return true; }
}
return false;
}
public void ReplaceTag(string tag, string newTag)
{
ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier());
@@ -1756,7 +1769,7 @@ namespace Barotrauma
return new AttackResult(damageAmount, null);
}
private void SetCondition(float value, bool isNetworkEvent)
private void SetCondition(float value, bool isNetworkEvent, bool executeEffects = true)
{
if (!isNetworkEvent)
{
@@ -1779,16 +1792,22 @@ namespace Barotrauma
//Flag connections to be updated as device is broken
flagChangedConnections(connections);
#if CLIENT
foreach (ItemComponent ic in components)
{
ic.PlaySound(ActionType.OnBroken);
ic.StopSounds(ActionType.OnActive);
if (executeEffects)
{
foreach (ItemComponent ic in components)
{
ic.PlaySound(ActionType.OnBroken);
ic.StopSounds(ActionType.OnActive);
}
}
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
#endif
// Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow.
SetPreviousCondition();
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
if (executeEffects)
{
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
}
}
else if (condition > 0.0f && prevCondition <= 0.0f)
{
@@ -1872,15 +1891,15 @@ namespace Barotrauma
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
if (!conditionUpdatePending) { return; }
CreateStatusEvent();
CreateStatusEvent(loadingRound: false);
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
conditionUpdatePending = false;
}
public void CreateStatusEvent()
public void CreateStatusEvent(bool loadingRound)
{
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData());
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
}
private bool isActive = true;
@@ -71,9 +71,9 @@ namespace Barotrauma
{
public EventType EventType => EventType.ItemStat;
public readonly Dictionary<ItemStatManager.TalentStatIdentifier, float> Stats;
public readonly Dictionary<TalentStatIdentifier, float> Stats;
public SetItemStatEventData(Dictionary<ItemStatManager.TalentStatIdentifier, float> stats)
public SetItemStatEventData(Dictionary<TalentStatIdentifier, float> stats)
{
Stats = stats;
}
@@ -82,6 +82,13 @@ namespace Barotrauma
private readonly struct ItemStatusEventData : IEventData
{
public EventType EventType => EventType.Status;
public readonly bool LoadingRound;
public ItemStatusEventData(bool loadingRound)
{
LoadingRound = loadingRound;
}
}
private readonly struct AssignCampaignInteractionEventData : IEventData
@@ -332,6 +332,8 @@ namespace Barotrauma
public readonly bool TransferOnlyOnePerContainer;
public readonly bool AllowTransfersHere = true;
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
public PreferredContainer(XElement element)
{
Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty<Identifier>()).ToImmutableHashSet();
@@ -347,6 +349,9 @@ namespace Barotrauma
TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer);
AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere);
MinLevelDifficulty = element.GetAttributeFloat(nameof(MinLevelDifficulty), float.MinValue);
MaxLevelDifficulty = element.GetAttributeFloat(nameof(MaxLevelDifficulty), float.MaxValue);
if (element.GetAttribute("spawnprobability") == null)
{
//if spawn probability is not defined but amount is, assume the probability is 1
@@ -670,6 +675,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool AllowSellingWhenBroken { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AllowStealingAlways { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool Indestructible { get; private set; }
@@ -806,6 +814,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
public float AISlowTurretPriority { get; private set; }
[Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
public float AITurretTargetingMaxDistance { get; private set; }
protected override Identifier DetermineIdentifier(XElement element)
{
Identifier identifier = base.DetermineIdentifier(element);
@@ -5,29 +5,48 @@ using System.Collections.Generic;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId) : INetSerializableStruct
{
/// <summary>
/// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist.
/// </summary>
public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId)
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId));
/// <summary>
/// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied.
/// <see cref="ItemStatManager.ApplyStat"/> will always use the highest value for unstackable stats.
/// </summary>
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier)
=> new(stat, talentIdentifier, Option.None);
}
internal sealed class ItemStatManager
{
private Item item;
public ItemStatManager(Item item)
{
this.item = item;
}
[NetworkSerialize]
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct;
private readonly Dictionary<TalentStatIdentifier, float> talentStats = new();
private readonly Item item;
public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent)
public ItemStatManager(Item item) => this.item = item;
public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent)
{
if (talent.Character?.ID is not { } characterId ||
talent.Prefab?.Identifier is not { } talentIdentifier)
talent.Prefab?.Identifier is not { } talentIdentifier) { return; }
var identifier = stackable
? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId)
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier);
if (!stackable)
{
return;
if (talentStats.TryGetValue(identifier, out float existingValue))
{
// Always use the highest value for non-stackable stats
if (existingValue > value) { return; }
}
}
TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId);
talentStats[identifier] = value;
#if SERVER
@@ -38,21 +57,19 @@ namespace Barotrauma
#endif
}
// Used for getting the value value from network packet
public void ApplyStat(TalentStatIdentifier identifier, float value)
{
talentStats[identifier] = value;
}
/// <summary>
/// Used for setting the value value from network packet; bypassing all validity checks.
/// </summary>
public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value;
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
foreach (var (key, value) in talentStats)
{
if (key.Stat == stat)
{
total *= value;
}
if (key.Stat != stat) { continue; }
total *= value;
}
return total;
@@ -8,44 +8,85 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
/// <summary>
/// Used by various features to define different kinds of relations between items:
/// for example, which item a character must have equipped to interact with some item in some way,
/// which items can go inside a container, or which kind of item the target of a status effect must have for the effect to execute.
/// </summary>
class RelatedItem
{
public enum RelationType
{
None,
/// <summary>
/// The item must be contained inside the item this relation is defined in.
/// Can for example by used to make an item usable only when there's a specific kind of item inside it.
/// </summary>
Contained,
/// <summary>
/// The user must have equipped the item (i.e. held or worn).
/// </summary>
Equipped,
/// <summary>
/// The user must have picked up the item (i.e. the item needs to be in the user's inventory).
/// </summary>
Picked,
/// <summary>
/// The item this relation is defined in must be inside a specific kind of container.
/// Can for example by used to make an item do something when it's inside some other type of item.
/// </summary>
Container
}
public bool IsOptional { get; set; }
/// <summary>
/// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
/// </summary>
public bool MatchOnEmpty { get; set; }
/// <summary>
/// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
/// </summary>
public bool RequireEmpty { get; set; }
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor,
/// making it easier to for example make rewire things that require some special tool to rewire.
/// </summary>
public bool IgnoreInEditor { get; set; }
/// <summary>
/// Identifier(s) or tag(s) of the items that are NOT considered valid.
/// Can be used to, for example, exclude some specific items when using tags that apply to multiple items.
/// </summary>
public ImmutableHashSet<Identifier> ExcludedIdentifiers { get; private set; }
private RelationType type;
public List<StatusEffect> statusEffects;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
/// </summary>
public LocalizedString Msg;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. The localization tag of a message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
/// </summary>
public Identifier MsgTag;
/// <summary>
/// Should broken (0 condition) items be excluded
/// Should broken (0 condition) items be excluded?
/// </summary>
public bool ExcludeBroken { get; private set; }
/// <summary>
/// Should full condition (100%) items be excluded
/// Should full condition (100%) items be excluded?
/// </summary>
public bool ExcludeFullCondition { get; private set; }
/// <summary>
/// Are item variants considered valid?
/// </summary>
public bool AllowVariants { get; private set; } = true;
public RelationType Type
@@ -59,19 +100,34 @@ namespace Barotrauma
public int TargetSlot = -1;
/// <summary>
/// Overrides the position defined in ItemContainer.
/// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer.
/// </summary>
public Vector2? ItemPos;
/// <summary>
/// Only affects when ItemContainer.hideItems is false. Doesn't override the value.
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Only affects when ItemContainer.hideItems is false. Doesn't override the value.
/// </summary>
public bool Hide;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Can be used to override the rotation of specific items in the container.
/// </summary>
public float Rotation;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Can be used to force specific items to stay active inside the container (such as flashlights attached to a gun).
/// </summary>
public bool SetActive;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional,
/// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door).
/// </summary>
public bool IsOptional { get; set; }
public string JoinedIdentifiers
{
get { return string.Join(",", Identifiers); }
@@ -83,6 +139,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Identifier(s) or tag(s) of the items that are considered valid.
/// </summary>
public ImmutableHashSet<Identifier> Identifiers { get; private set; }
public string JoinedExcludedIdentifiers