Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -122,7 +122,11 @@ namespace Barotrauma
/// <summary>
/// Last limb of the character the effect is being used on.
/// </summary>
LastLimb = 1024
LastLimb = 1024,
/// <summary>
/// All entities (items, structures) this item is linked to. Only valid on items.
/// </summary>
LinkedEntities = 2048
}
/// <summary>
@@ -210,9 +214,17 @@ namespace Barotrauma
public readonly float Impulse;
public readonly float RotationRad;
/// <summary>
/// How many items to spawn.
/// Minimum number of items to spawn. Use "Count" to spawn a fixed number of items.
/// </summary>
public readonly int Count;
public readonly int MinCount;
/// <summary>
/// Maximum number of items to spawn. Use "Count" to spawn a fixed number of items.
/// </summary>
public readonly int MaxCount;
/// <summary>
/// Probability of spawning the item(s). 0-1.
/// </summary>
public readonly float Probability;
/// <summary>
/// Random offset added to the spawn position in pixels.
/// </summary>
@@ -273,7 +285,16 @@ namespace Barotrauma
Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f);
RotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f));
Count = element.GetAttributeInt("count", 1);
int fixedCount = element.GetAttributeInt("count", 1);
MinCount = element.GetAttributeInt(nameof(MinCount), fixedCount);
MaxCount = element.GetAttributeInt(nameof(MaxCount), fixedCount);
if (MinCount > MaxCount)
{
DebugConsole.AddWarning($"Potential error in a StatusEffect {parentDebugName}: mincount is larger than maxcount.");
}
Probability = element.GetAttributeFloat(nameof(Probability), 1.0f);
Spread = element.GetAttributeFloat("spread", 0f);
AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f));
Equip = element.GetAttributeBool("equip", false);
@@ -291,6 +312,11 @@ namespace Barotrauma
}
InheritEventTags = element.GetAttributeBool(nameof(InheritEventTags), false);
}
public int GetCount(Rand.RandSync randSync)
{
return Rand.Range(MinCount, MaxCount + 1, randSync);
}
}
/// <summary>
@@ -357,6 +383,11 @@ namespace Barotrauma
/// Meaning, the higher the skill level, the less the skill is increased.
/// </summary>
public readonly bool Proportional;
/// <summary>
/// Should the skill increase popup be always shown regardless of how much the skill increases?
/// Normally it's only shown when the skill reaches the next integer value.
/// </summary>
public readonly bool AlwayShowNotification;
public GiveSkill(ContentXElement element, string parentDebugName)
{
@@ -365,6 +396,7 @@ namespace Barotrauma
TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true);
UseDeltaTime = element.GetAttributeBool(nameof(UseDeltaTime), false);
Proportional = element.GetAttributeBool(nameof(Proportional), false);
AlwayShowNotification = element.GetAttributeBool(nameof(AlwayShowNotification), false);
if (SkillIdentifier == Identifier.Empty)
{
@@ -403,7 +435,7 @@ namespace Barotrauma
public bool TransferInventory { get; private set; }
[Serialize(0, IsPropertySaveable.No, description:
"The maximum number of creatures of the given species and team that can exist in the current level before this status effect stops spawning any more.")]
"The maximum number of creatures of the given species and team that can exist per team in the current level before this status effect stops spawning any more.")]
public int TotalMaxCount { get; private set; }
[Serialize(0, IsPropertySaveable.No, description: "Amount of stun to apply on the spawned character.")]
@@ -437,6 +469,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool InheritEventTags { get; private set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the character team be inherited from the entity that owns the status effect?")]
public bool InheritTeam { get; private set; }
public CharacterSpawnInfo(ContentXElement element, string parentDebugName)
{
@@ -554,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
@@ -585,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;
@@ -606,6 +646,11 @@ namespace Barotrauma
private readonly bool removeItem, dropContainedItems, dropItem, removeCharacter, breakLimb, hideLimb;
private readonly float hideLimbTimer;
/// <summary>
/// Identifier of a container the character's items should be moved into when the character is removed. Only valid if the effect removes the character.
/// </summary>
private readonly Identifier containerForItemsOnCharacterRemoval;
public readonly ActionType type = ActionType.OnActive;
private readonly List<Explosion> explosions;
@@ -616,12 +661,27 @@ namespace Barotrauma
private readonly List<ItemSpawnInfo> spawnItems;
/// <summary>
/// If set, the character targeted by this effect will send the corresponding localized text that has the specified identifier in the chat.
/// </summary>
private readonly Identifier forceSayIdentifier = Identifier.Empty;
/// <summary>
/// If set to true, the character targeted by this effect's "forcesay" command will send their message in the radio.
/// </summary>
private readonly bool forceSayInRadio;
/// <summary>
/// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them.
/// </summary>
private readonly bool spawnItemRandomly;
private readonly List<CharacterSpawnInfo> spawnCharacters;
/// <summary>
/// If enabled, the effect removes all talents from the target and refunds the talent points.
/// </summary>
public readonly bool refundTalents;
public readonly List<GiveTalentInfo> giveTalentInfos;
private readonly List<AITrigger> aiTriggers;
@@ -760,19 +820,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); };
}
}
}
@@ -791,7 +857,7 @@ namespace Barotrauma
protected StatusEffect(ContentXElement element, string parentDebugName)
{
tags = new HashSet<Identifier>(element.GetAttributeString("tags", "").Split(',').ToIdentifiers());
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));
@@ -802,6 +868,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);
@@ -810,6 +877,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)
{
@@ -914,7 +982,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);
@@ -936,7 +1027,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();
@@ -966,7 +1059,8 @@ namespace Barotrauma
dropItem = true;
break;
case "removecharacter":
removeCharacter = true;
removeCharacter = true;
containerForItemsOnCharacterRemoval = subElement.GetAttributeIdentifier("moveitemstocontainer", Identifier.Empty);
break;
case "breaklimb":
breakLimb = true;
@@ -986,6 +1080,7 @@ namespace Barotrauma
}
requiredItems.Add(newRequiredItem);
break;
case "requiredafflictions":
case "requiredaffliction":
requiredAfflictions ??= new HashSet<(Identifier, float)>();
Identifier[] ids = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("type", Array.Empty<Identifier>());
@@ -1028,6 +1123,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":
@@ -1093,6 +1190,9 @@ namespace Barotrauma
giveTalentInfos.Add(newGiveTalentInfo);
}
break;
case "refundtalents":
refundTalents = true;
break;
case "aitrigger":
aiTriggers ??= new List<AITrigger>();
aiTriggers.Add(new AITrigger(subElement));
@@ -1133,6 +1233,10 @@ namespace Barotrauma
animationsToTrigger ??= new List<AnimLoadInfo>();
animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray()));
break;
case "forcesay":
forceSayIdentifier = subElement.GetAttributeIdentifier("message", Identifier.Empty);
forceSayInRadio = subElement.GetAttributeBool("sayinradio", false);
break;
}
}
@@ -1150,9 +1254,9 @@ namespace Barotrauma
{
foreach (var (propertyName, value) in PropertyEffects)
{
if (propertyName == "condition" && value.GetType() == typeof(float))
if (ChangesItemCondition(propertyName, value, out float conditionValue))
{
return (float)value < 0.0f || (setValue && (float)value <= 0.0f);
return conditionValue < 0.0f || (setValue && conditionValue <= 0.0f);
}
}
return false;
@@ -1162,14 +1266,32 @@ namespace Barotrauma
{
foreach (var (propertyName, value) in PropertyEffects)
{
if (propertyName == "condition" && value.GetType() == typeof(float))
if (ChangesItemCondition(propertyName, value, out float conditionValue))
{
return (float)value > 0.0f || (setValue && (float)value > 0.0f);
return conditionValue > 0.0f || (setValue && conditionValue > 0.0f);
}
}
return false;
}
private bool ChangesItemCondition(Identifier propertyName, object value, out float conditionValue)
{
if (propertyName == "condition")
{
switch (value)
{
case float f:
conditionValue = f;
return true;
case int i:
conditionValue = i;
return true;
}
}
conditionValue = 0.0f;
return false;
}
public bool MatchesTagConditionals(ItemPrefab itemPrefab)
{
if (itemPrefab == null || !HasConditions)
@@ -1404,7 +1526,7 @@ namespace Barotrauma
{
if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; }
if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; }
if (!TargetItemComponent.IsNullOrEmpty() && itemComponent.Name != TargetItemComponent) { return false; }
if (!TargetItemComponent.IsNullOrEmpty() && !itemComponent.Name.Equals(TargetItemComponent, StringComparison.OrdinalIgnoreCase)) { return false; }
if (TargetIdentifiers == null) { return true; }
if (TargetIdentifiers.Contains("itemcomponent")) { return true; }
if (itemComponent.Item.HasTag(TargetIdentifiers)) { return true; }
@@ -1477,7 +1599,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;
}
}
@@ -1544,7 +1669,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)
@@ -1586,6 +1711,7 @@ namespace Barotrauma
}
position += Offset;
position += Rand.Vector(Rand.Range(0.0f, RandomOffset));
return position;
}
@@ -1676,15 +1802,8 @@ namespace Barotrauma
{
for (int i = 0; i < targets.Count; i++)
{
var target = targets[i];
if (target is Character character)
{
Entity.Spawner?.AddEntityToRemoveQueue(character);
}
else if (target is Limb limb)
{
Entity.Spawner?.AddEntityToRemoveQueue(limb.character);
}
Character targetCharacter = GetCharacterFromTarget(targets[i]);
if (targetCharacter != null) { RemoveCharacter(targetCharacter); }
}
}
if (breakLimb || hideLimb)
@@ -1774,10 +1893,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
@@ -1786,11 +1903,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);
}
@@ -1875,7 +1990,7 @@ namespace Barotrauma
if (talentTriggers != null)
{
Character targetCharacter = CharacterFromTarget(target);
Character targetCharacter = GetCharacterFromTarget(target);
if (targetCharacter != null && !targetCharacter.Removed)
{
foreach (Identifier talentTrigger in talentTriggers)
@@ -1887,6 +2002,31 @@ namespace Barotrauma
TryTriggerAnimation(target, entity);
if (!forceSayIdentifier.IsEmpty)
{
LocalizedString messageToSay = TextManager.Get(forceSayIdentifier).Fallback(forceSayIdentifier.Value);
if (!messageToSay.IsNullOrEmpty() && target is Character targetCharacter && targetCharacter.SpeechImpediment < 100.0f && !targetCharacter.IsDead)
{
ChatMessageType messageType = ChatMessageType.Default;
bool canUseRadio = ChatMessage.CanUseRadio(targetCharacter, out WifiComponent radio);
if (canUseRadio && forceSayInRadio)
{
messageType = ChatMessageType.Radio;
}
#if SERVER
GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, targetCharacter);
#elif CLIENT
//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
}
}
if (isNotClient)
{
// these effects do not need to be run clientside, as they are replicated from server to clients anyway
@@ -1894,7 +2034,7 @@ namespace Barotrauma
{
foreach (int giveExperience in giveExperiences)
{
Character targetCharacter = CharacterFromTarget(target);
Character targetCharacter = GetCharacterFromTarget(target);
if (targetCharacter != null && !targetCharacter.Removed)
{
targetCharacter?.Info?.GiveExperience(giveExperience);
@@ -1904,7 +2044,7 @@ namespace Barotrauma
if (giveSkills != null)
{
Character targetCharacter = CharacterFromTarget(target);
Character targetCharacter = GetCharacterFromTarget(target);
if (targetCharacter is { Removed: false })
{
foreach (GiveSkill giveSkill in giveSkills)
@@ -1914,11 +2054,11 @@ namespace Barotrauma
if (giveSkill.Proportional)
{
targetCharacter.Info?.ApplySkillGain(skillIdentifier, amount, !giveSkill.TriggerTalents);
targetCharacter.Info?.ApplySkillGain(skillIdentifier, amount, !giveSkill.TriggerTalents, forceNotification: giveSkill.AlwayShowNotification);
}
else
{
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, amount, !giveSkill.TriggerTalents);
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, amount, !giveSkill.TriggerTalents, forceNotification: giveSkill.AlwayShowNotification);
}
Identifier GetRandomSkill()
@@ -1929,9 +2069,17 @@ namespace Barotrauma
}
}
if (refundTalents)
{
if (GetCharacterFromTarget(target) is { Removed: false } c)
{
c.Info?.AddRefundPoints(1);
}
}
if (giveTalentInfos != null)
{
Character targetCharacter = CharacterFromTarget(target);
Character targetCharacter = GetCharacterFromTarget(target);
if (targetCharacter?.Info == null) { continue; }
if (!TalentTree.JobTalentTrees.TryGet(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree characterTalentTree)) { continue; }
@@ -2010,11 +2158,38 @@ namespace Barotrauma
foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters)
{
var characters = new List<Character>();
CharacterTeamType? inheritedTeam = null;
if (characterSpawnInfo.InheritTeam)
{
bool isPvP = GameMain.GameSession?.GameMode?.Preset == GameModePreset.PvP;
inheritedTeam = entity switch
{
Character c => c.TeamID,
Item it => it.GetRootInventoryOwner() is Character owner ? owner.TeamID : GetTeamFromSubmarine(it),
MapEntity e => GetTeamFromSubmarine(e),
_ => null
// Default to Team1, when we can't deduce the team (for example when spawning outside the sub AND character inventory).
} ?? (isPvP ? CharacterTeamType.None : CharacterTeamType.Team1);
CharacterTeamType? GetTeamFromSubmarine(MapEntity e)
{
if (e.Submarine == null) { return null; }
// Don't allow team FriendlyNPC in outposts, because if you buy a spawner item (such as husk container) from the store and choose to get it immediately, it will be spawned in the outpost.
return !isPvP && e.Submarine.Info.IsOutpost && e.Submarine.TeamID == CharacterTeamType.FriendlyNPC ?
CharacterTeamType.Team1 : e.Submarine.TeamID;
}
}
for (int i = 0; i < characterSpawnInfo.Count; i++)
{
Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset,
onSpawn: newCharacter =>
{
if (inheritedTeam.HasValue)
{
newCharacter.SetOriginalTeamAndChangeTeam(inheritedTeam.Value, processImmediately: true);
}
if (characterSpawnInfo.TotalMaxCount > 0)
{
if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount)
@@ -2074,7 +2249,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.
@@ -2119,9 +2298,13 @@ namespace Barotrauma
if (spawnItems.Count > 0)
{
var randomSpawn = spawnItems.GetRandomUnsynced();
for (int i = 0; i < randomSpawn.Count; i++)
int count = randomSpawn.GetCount(Rand.RandSync.Unsynced);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < randomSpawn.Probability)
{
ProcessItemSpawnInfo(randomSpawn);
for (int i = 0; i < count; i++)
{
ProcessItemSpawnInfo(randomSpawn);
}
}
}
}
@@ -2129,9 +2312,13 @@ namespace Barotrauma
{
foreach (ItemSpawnInfo itemSpawnInfo in spawnItems)
{
for (int i = 0; i < itemSpawnInfo.Count; i++)
int count = itemSpawnInfo.GetCount(Rand.RandSync.Unsynced);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < itemSpawnInfo.Probability)
{
ProcessItemSpawnInfo(itemSpawnInfo);
for (int i = 0; i < count; i++)
{
ProcessItemSpawnInfo(itemSpawnInfo);
}
}
}
}
@@ -2167,9 +2354,16 @@ namespace Barotrauma
intervalTimers ??= new Dictionary<Entity, float>();
intervalTimers[entity] = Interval;
}
}
private static Character CharacterFromTarget(ISerializableEntity target)
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;
if (targetCharacter == null)
@@ -2182,6 +2376,46 @@ namespace Barotrauma
return targetCharacter;
}
private void RemoveCharacter(Character character)
{
if (containerForItemsOnCharacterRemoval != Identifier.Empty)
{
ItemPrefab containerPrefab =
ItemPrefab.Prefabs.Find(me => me.Tags.Contains(containerForItemsOnCharacterRemoval)) ??
MapEntityPrefab.FindByIdentifier(containerForItemsOnCharacterRemoval) as ItemPrefab;
if (containerPrefab == null)
{
DebugConsole.ThrowError($"Could not spawn a container for a removed character's items. No item found with the identifier or tag \"{containerForItemsOnCharacterRemoval}\"");
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(containerPrefab, character.WorldPosition, onSpawned: OnItemContainerSpawned);
}
void OnItemContainerSpawned(Item item)
{
if (character.Inventory == null) { return; }
item.UpdateTransform();
item.AddTag("name:" + character.Name);
if (character.Info?.Job is { } job) { item.AddTag($"job:{job.Name}"); }
if (item.GetComponent<ItemContainer>() is not ItemContainer itemContainer) { return; }
List<Item> inventoryItems = new List<Item>(character.Inventory.AllItemsMod);
foreach (Item inventoryItem in inventoryItems)
{
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: true))
{
//if the item couldn't be put inside the despawn container, just drop it
inventoryItem.Drop(dropper: character, createNetworkEvent: true);
}
}
}
}
Entity.Spawner?.AddEntityToRemoveQueue(character);
}
void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo, Entity entity, PhysicsBody sourceBody, Vector2 position, Entity targetEntity)
{
Item parentItem = entity as Item;
@@ -2223,7 +2457,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;
@@ -2232,7 +2466,7 @@ namespace Barotrauma
worldPos += user.Submarine.Position;
}
}
else
else if (!entity.Removed)
{
worldPos = entity.WorldPosition;
}
@@ -2252,7 +2486,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)
@@ -2298,13 +2535,20 @@ 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)
{
ignoredBodies = user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList();
}
projectile.Shoot(user, spawnPos, spawnPos, rotation,
ignoredBodies: ignoredBodies, createNetworkEvent: true);
float damageMultiplier = 1f;
if (entity is Character character && sourceEntity is Limb { attack: Attack attack })
{
attack.ResetDamageMultiplier();
attack.DamageMultiplier *= 1.0f + character.GetStatValue(StatTypes.NaturalRangedAttackMultiplier);
damageMultiplier = attack.DamageMultiplier;
}
projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: ignoredBodies, createNetworkEvent: true, damageMultiplier: damageMultiplier);
projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine;
}
else if (newItem.body != null)
@@ -2339,7 +2583,7 @@ namespace Barotrauma
return;
}
}
if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
if (inventory != null && (inventory.CanProbablyBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
{
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item =>
{
@@ -2406,7 +2650,7 @@ namespace Barotrauma
foreach (Item item in thisInventory.AllItems)
{
Inventory containedInventory = item.GetComponent<ItemContainer>()?.Inventory;
if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
if (containedInventory != null && (containedInventory.CanProbablyBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
{
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) =>
{
@@ -2439,7 +2683,7 @@ namespace Barotrauma
{
if (animationsToTrigger == null) { return; }
// Could probably use a similar pattern in other places above too, but refactoring statuseffects is very volatile.
if ((CharacterFromTarget(target) ?? entity as Character) is Character targetCharacter)
if ((GetCharacterFromTarget(target) ?? entity as Character) is Character targetCharacter)
{
foreach (AnimLoadInfo animLoadInfo in animationsToTrigger)
{
@@ -2646,6 +2890,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);
@@ -2660,18 +2913,21 @@ namespace Barotrauma
if (limb == null) { return; }
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
{
if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
if (result.Afflictions != null &&
/* "affliction" is the affliction directly defined in the status effect (e.g. "5 internal damage (per second / per frame / however the effect is defined to run)"),
* "result" is how much we actually applied of that affliction right now (taking into account the elapsed time, resistances and such) */
result.Afflictions.FirstOrDefault(a => a.Prefab == limbAffliction.Prefab) is Affliction resultAffliction &&
(!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
{
if (type == ActionType.OnUse || type == ActionType.OnSuccess)
{
limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
limb.character.TryAdjustHealerSkill(user, affliction: affliction);
limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction);
}
else if (type == ActionType.OnFailure)
{
limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
limb.character.TryAdjustHealerSkill(user, affliction: affliction);
limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction);
}
}
}
@@ -2688,14 +2944,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) || tags.Contains(tag);
return statusEffectTags.Contains(tag);
}
}
}