Release v0.15.12.0

This commit is contained in:
Joonas Rikkonen
2021-10-27 18:50:57 +03:00
parent bf95e82d80
commit 234fb6bc06
450 changed files with 26042 additions and 10457 deletions
@@ -9,6 +9,7 @@ using System.Xml.Linq;
using Barotrauma.Items.Components;
using FarseerPhysics.Dynamics;
using Barotrauma.Extensions;
using Barotrauma.Abilities;
#if SERVER
using System.Text;
#endif
@@ -128,6 +129,8 @@ namespace Barotrauma
}
}
public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
@@ -250,6 +253,8 @@ namespace Barotrauma
private readonly List<Attacker> lastAttackers = new List<Attacker>();
public IEnumerable<Attacker> LastAttackers => lastAttackers;
public Character LastAttacker => lastAttackers.LastOrDefault()?.Character;
public Character LastOrderedCharacter { get; private set; }
public Character SecondLastOrderedCharacter { get; private set; }
public Entity LastDamageSource;
@@ -261,6 +266,7 @@ namespace Barotrauma
public readonly CharacterParams Params;
public string SpeciesName => Params.SpeciesName;
public string Group => Params.Group;
public bool IsHumanoid => Params.Humanoid;
public bool IsHusk => Params.Husk;
@@ -306,7 +312,14 @@ namespace Barotrauma
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Can be used by status effects to check the character's gender
/// </summary>
public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male;
/// <summary>
/// Can be used by status effects to check the character's gender
/// </summary>
public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female;
private float attackCoolDown;
@@ -461,10 +474,7 @@ namespace Barotrauma
}
}
public bool AllowInput
{
get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; }
}
public bool AllowInput => !Removed && !IsIncapacitated && Stun <= 0.0f;
public bool CanMove
{
@@ -475,11 +485,10 @@ namespace Barotrauma
return true;
}
}
public bool CanInteract => AllowInput && Params.CanInteract && !LockHands;
public bool CanInteract
{
get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; }
}
// Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction.
public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null;
public Vector2 CursorPosition
{
@@ -581,6 +590,14 @@ namespace Barotrauma
}
}
/// <summary>
/// Can be used by status effects to check whether the characters is in a high-pressure environment
/// </summary>
public bool InPressure
{
get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; }
}
public const float KnockbackCooldown = 5.0f;
public float KnockbackCooldownTimer;
@@ -594,6 +611,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
if (IsDead) { return true; }
return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
}
}
@@ -628,7 +646,7 @@ namespace Barotrauma
public float Stun
{
get { return IsRagdolled ? 1.0f : CharacterHealth.Stun; }
get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; }
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -640,22 +658,14 @@ namespace Barotrauma
public bool DisableHealthWindow;
public float Vitality
{
get { return CharacterHealth.Vitality; }
}
public float Health
{
get { return CharacterHealth.Vitality; }
}
// These properties needs to be exposed for status effects
public float Vitality => CharacterHealth.Vitality;
public float Health => Vitality;
public float HealthPercentage => CharacterHealth.HealthPercentage;
public float MaxVitality
{
get { return CharacterHealth.MaxVitality; }
}
public float MaxVitality => CharacterHealth.MaxVitality;
public float MaxHealth => MaxVitality;
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float Bloodloss
{
@@ -773,8 +783,6 @@ namespace Barotrauma
}
}
public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe;
public bool EnableDespawn { get; set; } = true;
public CauseOfDeath CauseOfDeath
@@ -1134,7 +1142,7 @@ namespace Barotrauma
{
nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction);
}
if (ragdollParams == null)
if (ragdollParams == null && prefab.VariantOf == null)
{
string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name) as RagdollParams;
@@ -1176,6 +1184,7 @@ namespace Barotrauma
{
LoadHeadAttachments();
}
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
}
partial void InitProjSpecific(XElement mainElement);
@@ -1371,13 +1380,18 @@ namespace Barotrauma
}
}
}
private List<Item> wearableItems = new List<Item>();
public float GetSkillLevel(string skillIdentifier)
{
if (Info?.Job == null) { return 0.0f; }
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
// apply multipliers first so that multipliers only affect base skill value
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
skillLevel *= affliction.GetSkillMultiplier();
}
if (skillIdentifier != null)
{
for (int i = 0; i < Inventory.Capacity; i++)
@@ -1392,10 +1406,8 @@ namespace Barotrauma
}
}
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
skillLevel *= affliction.GetSkillMultiplier();
}
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
return skillLevel;
}
@@ -1432,9 +1444,9 @@ namespace Barotrauma
// - dragging someone
// - crouching
// - moving backwards
public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) &&
public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged || HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) &&
(!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) &&
!AnimController.IsMovingBackwards;
!AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk);
public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
{
@@ -1595,7 +1607,7 @@ namespace Barotrauma
return Math.Clamp(reduction, 0, 1f);
}
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f)
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f)
{
if (limb != null)
{
@@ -1628,7 +1640,7 @@ namespace Barotrauma
float max;
if (AnimController is HumanoidAnimController)
{
max = AnimController.InWater ? 0.5f : 0.7f;
max = AnimController.InWater ? 0.5f : 0.8f;
}
else
{
@@ -1664,13 +1676,13 @@ namespace Barotrauma
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
}
if (AnimController is HumanoidAnimController)
if (AnimController is HumanoidAnimController humanAnimController)
{
((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch);
humanAnimController.Crouching = humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || IsKeyDown(InputType.Crouch);
}
if (!aiControlled &&
AnimController.onGround &&
AnimController.OnGround &&
!AnimController.InWater &&
AnimController.Anim != AnimController.Animation.UsingConstruction &&
AnimController.Anim != AnimController.Animation.CPR &&
@@ -1734,8 +1746,12 @@ namespace Barotrauma
}
else if (IsKeyDown(InputType.Attack))
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this)
{
if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
{
currentAttackTarget = default(AttackTargetData);
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
else if (IsPlayer)
@@ -2044,7 +2060,7 @@ namespace Barotrauma
return false;
}
public Item GetEquippedItem(string tagOrIdentifier, InvSlotType? slotType = null)
public Item GetEquippedItem(string tagOrIdentifier = null, InvSlotType? slotType = null)
{
if (Inventory == null) { return null; }
for (int i = 0; i < Inventory.Capacity; i++)
@@ -2059,7 +2075,7 @@ namespace Barotrauma
}
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
if (tagOrIdentifier == null || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
}
return null;
}
@@ -2076,11 +2092,10 @@ namespace Barotrauma
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
}
if (inventory.Owner is Item)
if (inventory.Owner is Item item)
{
var owner = (Item)inventory.Owner;
if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
ItemContainer container = owner.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
ItemContainer container = item.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
}
return true;
@@ -2219,6 +2234,12 @@ namespace Barotrauma
}
}
if (SelectedConstruction?.GetComponent<RemoteController>()?.TargetItem == item ||
HeldItems.Any(it => it.GetComponent<RemoteController>()?.TargetItem == item))
{
return true;
}
if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; }
Pickable pickableComponent = item.GetComponent<Pickable>();
@@ -2365,9 +2386,9 @@ namespace Barotrauma
{
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this))
{
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld))
{
FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null;
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
@@ -2397,7 +2418,7 @@ namespace Barotrauma
var head = AnimController.GetLimb(LimbType.Head);
bool headInWater = head == null ?
AnimController.InWater :
head.inWater;
head.InWater;
//climb ladders automatically when pressing up/down inside their trigger area
Ladder currentLadder = SelectedConstruction?.GetComponent<Ladder>();
if ((SelectedConstruction == null || currentLadder != null) &&
@@ -2445,7 +2466,7 @@ namespace Barotrauma
{
DeselectCharacter();
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && CanInteract)
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat))
{
SelectCharacter(FocusedCharacter);
}
@@ -2624,6 +2645,11 @@ namespace Barotrauma
UpdateAttackers(deltaTime);
foreach (var characterTalent in characterTalents)
{
characterTalent.UpdateTalent(deltaTime);
}
if (IsDead) { return; }
if (GameMain.NetworkMember != null)
@@ -2688,6 +2714,11 @@ namespace Barotrauma
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (aiTarget != null)
{
aiTarget.InDetectable = false;
}
UpdateControlled(deltaTime, cam);
//Health effects
@@ -2711,14 +2742,18 @@ namespace Barotrauma
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 5.0f * 5.0f;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f;
bool wasRagdolled = false;
bool selfRagdolled = false;
if (IsForceRagdolled)
{
IsRagdolled = IsForceRagdolled;
}
else if (this != Controlled)
{
IsRagdolled = IsKeyDown(InputType.Ragdoll);
wasRagdolled = IsRagdolled;
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll);
}
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll))
@@ -2730,20 +2765,30 @@ namespace Barotrauma
}
else
{
bool wasRagdolled = IsRagdolled;
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; }
wasRagdolled = IsRagdolled;
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; }
}
}
if (!wasRagdolled && IsRagdolled)
{
if (selfRagdolled)
{
CheckTalents(AbilityEffectType.OnSelfRagdoll);
}
// currently does not work when you are stunned, like it should
CheckTalents(AbilityEffectType.OnRagdoll);
}
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
//ragdoll button
if (IsRagdolled || !CanMove)
{
if (AnimController is HumanoidAnimController)
if (AnimController is HumanoidAnimController humanAnimController)
{
((HumanoidAnimController)AnimController).Crouching = false;
humanAnimController.Crouching = false;
}
AnimController.ResetPullJoints();
SelectedConstruction = null;
@@ -2821,7 +2866,7 @@ namespace Barotrauma
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
reduction *= 0.5f;
}
enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction);
enemy.Damage = Math.Max(0.0f, enemy.Damage - reduction);
}
}
}
@@ -3061,30 +3106,47 @@ namespace Barotrauma
//set the character order only if the character is close enough to hear the message
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (order.OrderGiver != orderGiver)
if (order != null && order.OrderGiver != orderGiver)
{
order.OrderGiver = orderGiver;
}
// If there's another character operating the same device, make them dismiss themself
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
switch (order?.Category)
{
foreach (var character in CharacterList)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
case OrderCategory.Operate when order?.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
}
if (orderToReplace.HasValue)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
// Prevent adding duplicate orders
@@ -3092,6 +3154,19 @@ namespace Barotrauma
OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority);
AddCurrentOrder(newOrderInfo);
if (orderGiver != null)
{
var abilityOrderedCharacter = new AbilityCharacter(this);
orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter);
if (orderGiver.LastOrderedCharacter != this)
{
orderGiver.SecondLastOrderedCharacter = orderGiver.LastOrderedCharacter;
orderGiver.LastOrderedCharacter = this;
}
}
if (AIController is HumanAIController humanAI)
{
humanAI.SetOrder(order, orderOption, priority, orderGiver, speak);
@@ -3337,9 +3412,40 @@ namespace Barotrauma
float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime;
AbilityAttackData attackData = new AbilityAttackData(attack, this);
if (attacker != null)
{
attackData.Attacker = attacker;
attacker.CheckTalents(AbilityEffectType.OnAttack, attackData);
CheckTalents(AbilityEffectType.OnAttacked, attackData);
attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.AttackMultiplier);
if (attacker.TeamID == TeamID)
{
attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.TeamAttackMultiplier);
}
}
IEnumerable<Affliction> attackAfflictions;
if (attackData.Afflictions != null)
{
attackAfflictions = attackData.Afflictions.Union(attack.Afflictions.Keys);
}
else
{
attackAfflictions = attack.Afflictions.Keys;
}
var attackResult = targetLimb == null ?
AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) :
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier, penetration: penetration);
AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) :
DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration);
if (attacker != null)
{
var abilityAttackResult = new AbilityAttackResult(attackResult);
attacker.CheckTalents(AbilityEffectType.OnAttackResult, abilityAttackResult);
CheckTalents(AbilityEffectType.OnAttackedResult, abilityAttackResult);
}
if (limbHit == null) { return new AttackResult(); }
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
@@ -3425,9 +3531,9 @@ namespace Barotrauma
}
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null)
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f)
{
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker);
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier);
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
@@ -3436,11 +3542,6 @@ namespace Barotrauma
if (Removed) { return new AttackResult(); }
if (attacker != null && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID) { return new AttackResult(); }
}
float closestDistance = 0.0f;
foreach (Limb limb in AnimController.Limbs)
{
@@ -3457,6 +3558,13 @@ namespace Barotrauma
public void RecordKill(Character target)
{
var abilityCharacter = new AbilityCharacter(target);
foreach (Character attackerCrewmember in GetFriendlyCrew(this))
{
attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, abilityCharacter);
}
CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacter);
if (!IsOnPlayerTeam) { return; }
if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; }
GameMain.Config.KilledCreatures.Add(target.SpeciesName);
@@ -3496,7 +3604,11 @@ namespace Barotrauma
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID) { return new AttackResult(); }
if (attacker.TeamID == TeamID)
{
afflictions = afflictions.Where(a => !a.Prefab.IsBuff);
if (!afflictions.Any()) { return new AttackResult(); }
}
}
#if CLIENT
@@ -3524,7 +3636,7 @@ namespace Barotrauma
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
float prevVitality = CharacterHealth.Vitality;
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker);
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
if (attacker != this)
{
@@ -3551,6 +3663,7 @@ namespace Barotrauma
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
}
return attackResult;
}
@@ -3567,16 +3680,14 @@ namespace Barotrauma
{
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
attacker.Info?.IncreaseSkillLevel("weapons",
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f),
attacker.Position + Vector2.UnitY * 100.0f);
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f));
}
}
else if (healthChange > 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("medical");
attacker.Info?.IncreaseSkillLevel("medical",
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f),
attacker.Position + Vector2.UnitY * 100.0f);
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f));
}
}
@@ -3590,6 +3701,7 @@ namespace Barotrauma
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
if (Screen.Selected != GameMain.GameScreen) { return; }
if (newStun > 0 && Params.Health.StunImmunity) { return; }
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
if (Math.Sign(newStun) != Math.Sign(Stun))
{
@@ -3611,10 +3723,7 @@ namespace Barotrauma
if (statusEffect.type != actionType) { continue; }
if (statusEffect.type == ActionType.OnDamaged)
{
if (statusEffect.AllowedAfflictions != null && (LastDamage.Afflictions == null || LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier))))
{
continue;
}
if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
{
if (LastAttacker == null || !LastAttacker.IsPlayer)
@@ -3677,7 +3786,7 @@ namespace Barotrauma
}
}
private void Implode(bool isNetworkMessage = false)
public void Implode(bool isNetworkMessage = false)
{
if (CharacterHealth.Unkillable || GodMode || IsDead) { return; }
@@ -3720,6 +3829,7 @@ namespace Barotrauma
foreach (var joint in AnimController.LimbJoints)
{
if (joint.LimbA.type == LimbType.Head || joint.LimbB.type == LimbType.Head) { continue; }
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.LimitEnabled = false;
@@ -3775,6 +3885,9 @@ namespace Barotrauma
causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource);
OnDeath?.Invoke(this, CauseOfDeath);
var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller);
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
@@ -3782,13 +3895,20 @@ namespace Barotrauma
KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
if (info != null) { info.CauseOfDeath = CauseOfDeath; }
if (info != null)
{
info.CauseOfDeath = CauseOfDeath;
info.MissionsCompletedSinceDeath = 0;
}
AnimController.movement = Vector2.Zero;
AnimController.TargetMovement = Vector2.Zero;
foreach (Item heldItem in HeldItems.ToList())
if (!LockHands)
{
heldItem.Drop(this);
foreach (Item heldItem in HeldItems.ToList())
{
heldItem.Drop(this);
}
}
SelectedConstruction = null;
@@ -3805,12 +3925,17 @@ namespace Barotrauma
if (GameMain.GameSession != null)
{
if (GameMain.GameSession.Campaign != null && TeamID == CharacterTeamType.Team1 && !IsAssistant)
{
GameMain.GameSession.Campaign.CrewHasDied = true;
}
GameMain.GameSession.KillCharacter(this);
}
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
public void Revive()
public void Revive(bool removeAllAfflictions = true)
{
if (Removed)
{
@@ -3821,7 +3946,14 @@ namespace Barotrauma
aiTarget?.Remove();
aiTarget = new AITarget(this);
CharacterHealth.RemoveAllAfflictions();
if (removeAllAfflictions)
{
CharacterHealth.RemoveAllAfflictions();
}
else
{
CharacterHealth.RemoveNegativeAfflictions();
}
SetAllDamage(0.0f, 0.0f, 0.0f);
Oxygen = 100.0f;
Bloodloss = 0.0f;
@@ -3842,7 +3974,10 @@ namespace Barotrauma
foreach (Limb limb in AnimController.Limbs)
{
#if CLIENT
if (limb.LightSource != null) limb.LightSource.Color = limb.InitialLightSourceColor;
if (limb.LightSource != null)
{
limb.LightSource.Color = limb.InitialLightSourceColor;
}
#endif
limb.body.Enabled = true;
limb.IsSevered = false;
@@ -4164,12 +4299,8 @@ namespace Barotrauma
}
if (Submarine == null && target.Submarine != null)
{
if (AIController == null || !(AIController.SteeringManager is IndoorsSteeringManager))
{
// outside and targeting inside
// doesn't work with inside steering
targetPos += target.Submarine.SimPosition;
}
// outside and targeting inside
targetPos += target.Submarine.SimPosition;
}
else if (Submarine != null && target.Submarine == null)
{
@@ -4203,8 +4334,277 @@ namespace Barotrauma
public bool IsProtectedFromPressure()
{
return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
}
// Talent logic begins here. Should be encapsulated to its own controller soon
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
public void LoadTalents()
{
List<string> toBeRemoved = null;
foreach (string talent in info.UnlockedTalents)
{
if (!GiveTalent(talent, addingFirstTime: false))
{
DebugConsole.AddWarning(Name + " had talent that did not exist! Removing talent from CharacterInfo.");
toBeRemoved ??= new List<string>();
toBeRemoved.Add(talent);
}
}
if (toBeRemoved != null)
{
foreach (string removeTalent in toBeRemoved)
{
Info.UnlockedTalents.Remove(removeTalent);
}
}
}
public bool GiveTalent(string talentIdentifier, bool addingFirstTime = true)
{
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talentIdentifier, StringComparison.OrdinalIgnoreCase));
if (talentPrefab == null)
{
DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists.");
return false;
}
return GiveTalent(talentPrefab, addingFirstTime);
}
public bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime = true)
{
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UIntIdentifier == talentIdentifier);
if (talentPrefab == null)
{
DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists.");
return false;
}
return GiveTalent(talentPrefab, addingFirstTime);
}
public bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime = true)
{
if (info == null) { return false; }
info.UnlockedTalents.Add(talentPrefab.Identifier);
if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; }
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents });
#endif
CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this);
characterTalent.ActivateTalent(addingFirstTime);
characterTalents.Add(characterTalent);
characterTalent.AddedThisRound = addingFirstTime;
if (addingFirstTime)
{
OnTalentGiven(talentPrefab.Identifier);
}
return true;
}
public bool HasTalent(string identifier)
{
return info.UnlockedTalents.Contains(identifier);
}
public static IEnumerable<Character> GetFriendlyCrew(Character character)
{
return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead);
}
public bool HasTalents()
{
return characterTalents.Any();
}
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
}
public void CheckTalents(AbilityEffectType abilityEffectType)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, null);
}
}
public bool HasRecipeForItem(string recipeIdentifier)
{
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
/// <summary>
/// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains.
/// </summary>
public void GiveMoney(int amount)
{
if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; }
if (amount <= 0) { return; }
int prevAmount = campaign.Money;
campaign.Money += amount;
OnMoneyChanged(prevAmount, campaign.Money);
}
public void SetMoney(int amount)
{
if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; }
if (amount == campaign.Money) { return; }
int prevAmount = campaign.Money;
campaign.Money = amount;
OnMoneyChanged(prevAmount, campaign.Money);
}
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(string talentIdentifier);
/// <summary>
/// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now.
/// If necessary, the approach of using a dictionary could be replaced by an encapsulated class that contains the stats as attributes.
/// </summary>
private readonly Dictionary<StatTypes, float> statValues = new Dictionary<StatTypes, float>();
/// <summary>
/// A dictionary with temporary values, updated when the character equips/unequips wearables. Used to reduce unnecessary inventory checking.
/// </summary>
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
public float GetStatValue(StatTypes statType)
{
if (!IsHuman) { return 0f; }
float statValue = 0f;
if (statValues.TryGetValue(statType, out float value))
{
statValue += value;
}
if (CharacterHealth != null)
{
statValue += CharacterHealth.GetStatValue(statType);
}
if (Info != null)
{
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
statValue += Info.GetSavedStatValue(statType);
}
if (wearableStatValues.TryGetValue(statType, out float wearableValue))
{
statValue += wearableValue;
}
return statValue;
}
public void OnWearablesChanged()
{
wearableStatValues.Clear();
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
&& Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
{
foreach (var statValuePair in wearable.WearableStatValues)
{
if (wearableStatValues.ContainsKey(statValuePair.Key))
{
wearableStatValues[statValuePair.Key] += statValuePair.Value;
}
else
{
wearableStatValues.Add(statValuePair.Key, statValuePair.Value);
}
}
}
}
}
public void ChangeStat(StatTypes statType, float value)
{
if (statValues.ContainsKey(statType))
{
statValues[statType] += value;
}
else
{
statValues.Add(statType, value);
}
}
public static StatTypes GetSkillStatType(string skillIdentifier)
{
// Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way
switch (skillIdentifier)
{
case "electrical":
return StatTypes.ElectricalSkillBonus;
case "helm":
return StatTypes.HelmSkillBonus;
case "mechanical":
return StatTypes.MechanicalSkillBonus;
case "medical":
return StatTypes.MedicalSkillBonus;
case "weapons":
return StatTypes.WeaponsSkillBonus;
default:
return StatTypes.None;
}
}
private readonly List<AbilityFlags> abilityFlags = new List<AbilityFlags>();
public void AddAbilityFlag(AbilityFlags abilityFlag)
{
abilityFlags.Add(abilityFlag);
}
public void RemoveAbilityFlag(AbilityFlags abilityFlag)
{
abilityFlags.Remove(abilityFlag);
}
public bool HasAbilityFlag(AbilityFlags abilityFlag)
{
return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
}
private readonly Dictionary<string, float> abilityResistances = new Dictionary<string, float>();
public float GetAbilityResistance(AfflictionPrefab affliction)
{
return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f;
}
public void ChangeAbilityResistance(string resistanceId, float value)
{
if (abilityResistances.ContainsKey(resistanceId))
{
abilityResistances[resistanceId] *= value;
}
else
{
abilityResistances.Add(resistanceId, value);
}
}
/// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary>
public bool IsFriendly(Character other) => IsFriendly(this, other);
/// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary>
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
}
class ActiveTeamChange