Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2025-04-10 10:37:09 -03:00
296 changed files with 8420 additions and 2945 deletions
@@ -589,7 +589,7 @@ namespace Barotrauma
/// <summary>
/// Can be used in conditionals to check if a StatusEffect with a specific tag is currently running. Only relevant for effects with a non-zero duration.
/// </summary>
private readonly HashSet<Identifier> tags;
private readonly HashSet<Identifier> statusEffectTags;
/// <summary>
/// How long _can_ the event run (in seconds). The difference to <see cref="duration"/> is that
@@ -620,12 +620,17 @@ namespace Barotrauma
/// <summary>
/// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s) if the effect is already being applied?
/// </summary>
public readonly bool Stackable = true;
public readonly bool Stackable;
/// <summary>
/// Only valid if the effect is non-stackable and has a duration. If the effect is reapplied while it's already running, should it reset the duration of the existing effect (i.e. keep the existing effect running)?
/// </summary>
public readonly bool ResetDurationWhenReapplied;
/// <summary>
/// The interval at which the effect is executed. The difference between delay and interval is that effects with a delay find the targets, check the conditions, etc
/// immediately when Apply is called, but don't apply the effects until the delay has passed. Effects with an interval check if the interval has passed when Apply is
/// called and apply the effects if it has, otherwise they do nothing.
/// called and apply the effects if it has, otherwise they do nothing. Using this is preferred for performance reasons.
/// </summary>
public readonly float Interval;
@@ -817,19 +822,25 @@ namespace Barotrauma
/// </summary>
public Vector2 Offset { get; private set; }
/// <summary>
/// An random offset (in a random direction) added to the position of the effect is executed at. Only relevant if the effect does something where position matters,
/// for example emitting particles or explosions, spawning something or playing sounds.
/// </summary>
public float RandomOffset { get; private set; }
public string Tags
{
get { return string.Join(",", tags); }
get { return string.Join(",", statusEffectTags); }
set
{
tags.Clear();
statusEffectTags.Clear();
if (value == null) return;
string[] newTags = value.Split(',');
foreach (string tag in newTags)
{
Identifier newTag = tag.Trim().ToIdentifier();
if (!tags.Contains(newTag)) { tags.Add(newTag); };
if (!statusEffectTags.Contains(newTag)) { statusEffectTags.Add(newTag); };
}
}
}
@@ -848,7 +859,7 @@ namespace Barotrauma
protected StatusEffect(ContentXElement element, string parentDebugName)
{
tags = new HashSet<Identifier>(element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()));
statusEffectTags = new HashSet<Identifier>(element.GetAttributeIdentifierArray("statuseffecttags", element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>())));
OnlyInside = element.GetAttributeBool("onlyinside", false);
OnlyOutside = element.GetAttributeBool("onlyoutside", false);
OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false));
@@ -859,6 +870,7 @@ namespace Barotrauma
disableDeltaTime = element.GetAttributeBool("disabledeltatime", false);
setValue = element.GetAttributeBool("setvalue", false);
Stackable = element.GetAttributeBool("stackable", true);
ResetDurationWhenReapplied = element.GetAttributeBool("resetdurationwhenreapplied", true);
lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f);
CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false);
@@ -867,6 +879,7 @@ namespace Barotrauma
Range = element.GetAttributeFloat("range", 0.0f);
Offset = element.GetAttributeVector2("offset", Vector2.Zero);
RandomOffset = element.GetAttributeFloat("randomoffset", 0.0f);
string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null);
if (targetLimbNames != null)
{
@@ -971,7 +984,30 @@ namespace Barotrauma
//a workaround to "tags" possibly meaning either an item's tags or this status effect's tags:
//if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags
propertyAttributes.Add(attribute);
if (targetTypes.HasFlag(TargetType.UseTarget))
{
DebugConsole.AddWarning(
$"Potential error in StatusEffect ({parentDebugName}). " +
"The effect is configured to set the tags of the use target, which will not work on most kinds of targets (only if the target is an item). "+
"If you meant to configure the tags for the StatusEffect itself, please use the attribute 'statuseffecttags'. If you are sure you want to set the tags of the target, use the attribute 'settags'.",
contentPackage: element.ContentPackage);
}
}
else
{
#if DEBUG
//it would be nice to warn modders about this too, but since the effects have always been configured like this before,
//it'd lead to an avalanche of console warnings
DebugConsole.AddWarning(
$"StatusEffect tags defined using the attribute 'tags' in StatusEffect ({parentDebugName}). "+
"Please use the attribute 'statuseffecttags' or 'settags' instead to make it more explicit whether the 'tags' attribute means the status effect's tags, or tags the effect is supposed to set. " +
"The game now assumes it means the status effect's tags.",
contentPackage: element.ContentPackage);
#endif
}
break;
case "settags":
propertyAttributes.Add(attribute);
break;
case "oneshot":
oneShot = attribute.GetAttributeBool(false);
@@ -993,7 +1029,9 @@ namespace Barotrauma
List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>();
foreach (XAttribute attribute in propertyAttributes)
{
propertyEffects.Add((attribute.NameAsIdentifier(), XMLExtensions.GetAttributeObject(attribute)));
Identifier attributeName = attribute.NameAsIdentifier();
if (attributeName == "settags") { attributeName = "tags".ToIdentifier(); }
propertyEffects.Add((attributeName, XMLExtensions.GetAttributeObject(attribute)));
}
PropertyEffects = propertyEffects.ToImmutableArray();
@@ -1087,6 +1125,8 @@ namespace Barotrauma
// Could probably be solved by using the NonClampedStrength or by bypassing the clamping, but ran out of time and played it safe here.
afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, nameof(afflictionInstance.Probability));
afflictionInstance.MultiplyByMaxVitality = subElement.GetAttributeBool(nameof(afflictionInstance.MultiplyByMaxVitality), false);
afflictionInstance.DivideByLimbCount = subElement.GetAttributeBool(nameof(afflictionInstance.DivideByLimbCount), false);
afflictionInstance.Penetration = subElement.GetAttributeFloat(0.0f, nameof(Attack.Penetration));
Afflictions.Add(afflictionInstance);
break;
case "reduceaffliction":
@@ -1566,7 +1606,10 @@ namespace Barotrauma
DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target);
if (existingEffect != null)
{
existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user);
if (ResetDurationWhenReapplied)
{
existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user);
}
return;
}
}
@@ -1633,7 +1676,7 @@ namespace Barotrauma
return hull;
}
private Vector2 GetPosition(Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
protected Vector2 GetPosition(Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
{
Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition);
if (worldPosition == null)
@@ -1675,6 +1718,7 @@ namespace Barotrauma
}
position += Offset;
position += Rand.Vector(Rand.Range(0.0f, RandomOffset));
return position;
}
@@ -1882,10 +1926,8 @@ namespace Barotrauma
character.LastDamageSource = entity;
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.Removed) { continue; }
if (limb.IsSevered) { continue; }
if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; }
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue);
if (!IsValidTargetLimb(limb)) { continue; }
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, penetration: newAffliction.Penetration, allowStacking: !setValue);
limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
//only apply non-limb-specific afflictions to the first limb
@@ -1894,11 +1936,9 @@ namespace Barotrauma
}
else if (target is Limb limb)
{
if (limb.IsSevered) { continue; }
if (limb.character.Removed || limb.Removed) { continue; }
if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; }
if (!IsValidTargetLimb(limb)) { continue; }
newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality);
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue);
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, penetration: newAffliction.Penetration, allowStacking: !setValue);
limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
}
@@ -2010,8 +2050,12 @@ namespace Barotrauma
#if SERVER
GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, targetCharacter);
#elif CLIENT
AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType);
targetCharacter.SendSinglePlayerMessage(message, canUseRadio, radio);
//no need to create the message when playing as a client, the server will send it to us
if (isNotClient)
{
AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType);
targetCharacter.SendSinglePlayerMessage(message, canUseRadio, radio);
}
#endif
}
}
@@ -2238,7 +2282,11 @@ namespace Barotrauma
//ApplyAffliction modified the strength based on max vitality, let's undo that before transferring the affliction
//(otherwise e.g. a character with 1000 vitality would only get a tenth of the strength)
float afflictionStrength = affliction.Strength * (newCharacter.MaxVitality / 100.0f);
newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(afflictionStrength));
Limb afflictionLimb = character.CharacterHealth.GetAfflictionLimb(affliction) ?? character.AnimController.MainLimb;
Limb newAfflictionLimb = newCharacter.AnimController.GetLimb(afflictionLimb.type) ?? newCharacter.AnimController.MainLimb;
newCharacter.CharacterHealth.ApplyAffliction(newAfflictionLimb, affliction.Prefab.Instantiate(afflictionStrength));
}
}
if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned.
@@ -2340,6 +2388,14 @@ namespace Barotrauma
intervalTimers[entity] = Interval;
}
}
private bool IsValidTargetLimb(Limb limb)
{
if (limb == null || limb.Removed) { return false; }
if (limb.IsSevered) { return false; }
if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { return false; }
return true;
}
private static Character GetCharacterFromTarget(ISerializableEntity target)
{
Character targetCharacter = target as Character;
@@ -2434,7 +2490,7 @@ namespace Barotrauma
}
float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad);
float rotation = chosenItemSpawnInfo.RotationRad;
Vector2 worldPos;
Vector2 worldPos = position;
if (sourceBody != null)
{
worldPos = sourceBody.Position;
@@ -2443,7 +2499,7 @@ namespace Barotrauma
worldPos += user.Submarine.Position;
}
}
else
else if (!entity.Removed)
{
worldPos = entity.WorldPosition;
}
@@ -2463,7 +2519,10 @@ namespace Barotrauma
}
break;
case ItemSpawnInfo.SpawnRotationType.Target:
rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos);
if (!entity.Removed)
{
rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos);
}
break;
case ItemSpawnInfo.SpawnRotationType.Limb:
if (sourceBody != null)
@@ -2509,6 +2568,7 @@ namespace Barotrauma
{
var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity;
Vector2 spawnPos = sourceEntity.SimPosition;
projectile.Item.Submarine = sourceEntity?.Submarine;
List<Body> ignoredBodies = null;
if (!projectile.DamageUser)
{
@@ -2863,6 +2923,15 @@ namespace Barotrauma
afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier);
}
}
if (affliction.DivideByLimbCount)
{
int limbCount = targetCharacter.AnimController.Limbs.Count(limb => IsValidTargetLimb(limb));
if (limbCount > 0)
{
afflictionMultiplier *= 1.0f / limbCount;
}
}
if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f))
{
return affliction.CreateMultiplied(afflictionMultiplier, affliction);
@@ -2908,14 +2977,14 @@ namespace Barotrauma
public void AddTag(Identifier tag)
{
if (tags.Contains(tag)) { return; }
tags.Add(tag);
if (statusEffectTags.Contains(tag)) { return; }
statusEffectTags.Add(tag);
}
public bool HasTag(Identifier tag)
{
if (tag == null) { return true; }
return tags.Contains(tag);
return statusEffectTags.Contains(tag);
}
}
}