Merge remote-tracking branch 'origin/dev'

# Conflicts:
#	Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs
#	Barotrauma/BarotraumaClient/LinuxClient.csproj
#	Barotrauma/BarotraumaClient/MacClient.csproj
#	Barotrauma/BarotraumaClient/WindowsClient.csproj
#	Barotrauma/BarotraumaServer/LinuxServer.csproj
#	Barotrauma/BarotraumaServer/MacServer.csproj
#	Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaServer/WindowsServer.csproj
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs
#	Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs
This commit is contained in:
Regalis11
2022-12-14 17:04:04 +02:00
417 changed files with 12748 additions and 5874 deletions
@@ -7,18 +7,21 @@
</CampaignSettingDefinitions>
<CampaignSettings
presetname="Easy"
TutorialEnabled="true"
RadiationEnabled="false"
StartingBalanceAmount="High"
StartItemSet="easy"
Difficulty="Easy"/>
<CampaignSettings
presetname="Normal"
TutorialEnabled="true"
RadiationEnabled="false"
StartingBalanceAmount="Medium"
StartItemSet="normal"
Difficulty="Medium"/>
<CampaignSettings
presetname="Hard"
TutorialEnabled="false"
RadiationEnabled="true"
StartingBalanceAmount="Low"
StartItemSet="hard"
@@ -8,7 +8,7 @@
<Preset
name="Moderator"
description="Allowed to manage round settings, kick players and access server logs."
permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney">
permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents">
<Command name="clientlist"/>
<Command name="readycheck"/>
<Command name="autorestart"/>
@@ -102,6 +102,8 @@ namespace Barotrauma
private bool inDetectable;
public double InDetectableSetTime;
/// <summary>
/// Should be reset to false each frame and kept indetectable by e.g. a status effect.
/// </summary>
@@ -115,7 +117,8 @@ namespace Barotrauma
{
inDetectable = value;
if (inDetectable)
{
{
InDetectableSetTime = Timing.TotalTime;
NeedsUpdate = true;
}
}
@@ -257,9 +260,14 @@ namespace Barotrauma
SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public bool HasSector()
{
return sectorRad < MathHelper.TwoPi;
}
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) { return true; }
if (!HasSector()) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f;
}
@@ -442,6 +442,7 @@ namespace Barotrauma
base.Update(deltaTime);
UpdateTriggers(deltaTime);
Character.ClearInputs();
Reverse = false;
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
@@ -689,7 +690,7 @@ namespace Barotrauma
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
//TODO: a way for pets to differentiate hostile and friendly humans?
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget))
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter))
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
@@ -804,10 +805,6 @@ namespace Barotrauma
Reverse = true;
run = true;
}
else
{
Reverse = false;
}
SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
}
else
@@ -918,7 +915,7 @@ namespace Barotrauma
else
{
// Wander around outside or swimming
steeringManager.SteeringWander();
steeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
@@ -1086,7 +1083,7 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
else
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
SelectedAiTarget = owner.AiTarget;
}
@@ -1490,40 +1487,26 @@ namespace Barotrauma
canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle);
if (canAttack && AttackLimb.attack.AvoidFriendlyFire)
{
float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2);
bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance;
if (SwarmBehavior != null)
canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity));
bool IsBlocked(Vector2 targetPosition)
{
canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c));
}
else
{
canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c));
}
if (canAttack)
{
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range));
bool IsBlocked(Vector2 targetPosition)
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
{
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
Character hitTarget = null;
if (body.UserData is Character c)
{
Character hitTarget = null;
if (body.UserData is Character c)
{
hitTarget = c;
}
else if (body.UserData is Limb limb)
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
hitTarget = c;
}
else if (body.UserData is Limb limb)
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
return false;
}
return false;
}
}
}
@@ -1853,8 +1836,41 @@ namespace Barotrauma
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
}
}
if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else if (advance)
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else
{
if (Character.CurrentHull == null && !canAttack)
{
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
}
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
@@ -1865,20 +1881,22 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (AttackLimb.attack.Ranged)
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
}
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
IDamageable damageTarget = targetEntity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack)
{
AimRangedAttack(attack, targetEntity);
}
if (canAttack)
{
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb))
if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
{
IgnoreTarget(SelectedAiTarget);
}
@@ -1889,6 +1907,22 @@ namespace Barotrauma
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
{
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
Character.SetInput(InputType.Aim, false, true);
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition;
float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
limb.body.SuppressSmoothRotationCalls = false;
float angle = MathUtils.VectorToAngle(toTarget);
limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
limb.body.SuppressSmoothRotationCalls = true;
}
}
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, Entity target)
{
if (attackingLimb == null) { return false; }
@@ -2114,13 +2148,13 @@ namespace Barotrauma
}
// 10 dmg, 100 health -> 0.1
private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null)
private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null)
{
if (SelectedAiTarget?.Entity == null) { return false; }
if (attackingLimb?.attack == null) { return false; }
ActiveAttack = attackingLimb.attack;
if (AttackLimb?.attack == null) { return false; }
if (damageTarget == null) { return false; }
if (wallTarget != null)
{
// If the selected target is not the wall target, make the wall target the selected target.
@@ -2129,85 +2163,97 @@ namespace Barotrauma
{
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority);
State = AIState.Attack;
return true;
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
if (damageTarget != null)
ActiveAttack = AttackLimb.attack;
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{
if (Character.Params.CanInteract && Character.Inventory != null)
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
// Use equipped items (weapons)
Item item = GetEquippedItem(attackingLimb);
if (item != null)
Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition;
float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
if (angle > ActiveAttack.RequiredAngleToShoot)
{
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
return true;
}
}
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
}
if (Character.Params.CanInteract && Character.Inventory != null)
{
// Use equipped items (weapons)
Item item = GetEquippedItem(AttackLimb);
if (item != null)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
attackingLimb,
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
}
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (attackingLimb.attack.CoolDownTimer > 0)
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
}
}
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
}
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (ActiveAttack.CoolDownTimer > 0)
{
SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!ActiveAttack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character))
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
else
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!attackingLimb.attack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character))
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
else
{
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
}
return true;
}
return false;
return true;
}
private float aimTimer;
@@ -2299,7 +2345,6 @@ namespace Barotrauma
{
if (attackVector == null)
{
// TODO: test adding some random variance here?
attackVector = attackWorldPos - WorldPosition;
}
Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
@@ -2319,6 +2364,16 @@ namespace Barotrauma
return true;
}
private Limb GetLimbToRotate(Attack attack)
{
Limb limb = AttackLimb;
if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length)
{
limb = Character.AnimController.Limbs[attack.RotationLimbIndex];
}
return limb;
}
#endregion
#region Eat
@@ -3429,7 +3484,7 @@ namespace Barotrauma
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
=> ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting);
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false)
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
{
@@ -3437,6 +3492,11 @@ namespace Barotrauma
{
if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams))
{
if (state == AIState.Attack)
{
// Only applies to new temp target params. Shouldn't affect any existing definitions (handled below).
targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
}
tempParams.Add(tag, targetParams);
}
}
@@ -3470,7 +3530,7 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority);
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;
@@ -3554,7 +3614,10 @@ namespace Barotrauma
{
// We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible.
// When the monsters attack the player sub, they wall hack so that they can be more aggressive.
checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
// Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack)
checkVisibility =
Character.IsPet && (Character.Submarine != null || target.Entity.Submarine != null) ||
target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
}
if (dist > 0)
{
@@ -3755,6 +3818,7 @@ namespace Barotrauma
{
SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority);
State = AIState.Attack;
return false;
}
}
}
@@ -3767,7 +3831,7 @@ namespace Barotrauma
}
else
{
SteeringManager.SteeringWander();
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: Character.CurrentHull == null);
if (Character.CurrentHull == null)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
@@ -3826,9 +3890,8 @@ namespace Barotrauma
return targetLimb;
}
private Character GetOwner(Item item)
private static Character GetOwner(Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
{
@@ -395,6 +395,10 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
if (reportProblemsTimer > 0)
{
reportProblemsTimer -= deltaTime;
}
if (reactTimer > 0.0f)
{
reactTimer -= deltaTime;
@@ -407,7 +411,6 @@ namespace Barotrauma
else
{
Character.UpdateTeam();
if (Character.CurrentHull != null)
{
if (Character.IsOnPlayerTeam)
@@ -425,19 +428,15 @@ namespace Barotrauma
}
}
}
if (Character.SpeechImpediment < 100.0f)
if (reportProblemsTimer <= 0.0f)
{
reportProblemsTimer -= deltaTime;
if (reportProblemsTimer <= 0.0f)
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
reportProblemsTimer = reportProblemsInterval;
ReportProblems();
}
UpdateSpeaking();
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
UnequipUnnecessaryItems();
reactTimer = GetReactionTime();
}
@@ -912,7 +911,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
bool speak = true;
bool speak = Character.SpeechImpediment < 100;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1063,17 +1062,15 @@ namespace Barotrauma
private void UpdateSpeaking()
{
if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
@@ -1860,6 +1857,7 @@ namespace Barotrauma
bool targetAdded = false;
DoForEachCrewMember(caller, humanAI =>
{
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
if (objective != null)
{
@@ -1909,8 +1907,8 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreWater = character.IsProtectedFromPressure();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
@@ -77,11 +77,11 @@ namespace Barotrauma
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
if (GameMain.GameSession.RoundDuration < 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
@@ -748,6 +748,9 @@ namespace Barotrauma
}
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
@@ -764,7 +767,7 @@ namespace Barotrauma
}
return true;
bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
}
private float findHullTimer;
@@ -873,7 +876,20 @@ namespace Barotrauma
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
onAbandon: () => Abandon = true);
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
});
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
{
@@ -25,6 +25,8 @@ namespace Barotrauma
private readonly Item item;
public Item ItemToContain { get; private set; }
public int? TargetSlot;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
@@ -116,7 +118,11 @@ namespace Barotrauma
Abandon = true;
return;
}
ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
ItemToContain = item ?? character.Inventory.FindItem(it =>
CheckItem(it) &&
//ignore items already in the container, unless we're trying to place to a specific slot, and the item's not in it
(it.Container != container.Item || (TargetSlot.HasValue && it.Container.OwnInventory.FindIndex(it) != TargetSlot)),
recursive: true);
if (ItemToContain != null)
{
if (!character.CanInteractWith(ItemToContain, checkLinked: false))
@@ -126,7 +132,19 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain)))
static bool CanBePut(Inventory inventory, int? targetSlot, Item itemToContain)
{
if (targetSlot.HasValue)
{
return inventory.CanBePutInSlot(itemToContain, targetSlot.Value);
}
else
{
return inventory.CanBePut(itemToContain);
}
}
if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain)))
{
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
}
@@ -136,7 +154,20 @@ namespace Barotrauma
}
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
{
if (targetSlot.HasValue)
{
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null);
}
else
{
return inventory.TryPutItem(itemToContain, user: null);
}
}
if (TryPutItem(container.Inventory, TargetSlot, ItemToContain))
{
if (MoveWholeStack && slots != null)
{
@@ -144,7 +175,7 @@ namespace Barotrauma
{
foreach (Item item in originalInventory.GetItemsAt(slot).ToList())
{
container.Inventory.TryPutItem(item, null);
TryPutItem(container.Inventory, TargetSlot, item);
}
}
}
@@ -211,6 +242,15 @@ namespace Barotrauma
}
}
public bool IsInTargetSlot(Item item)
{
if (container?.Inventory is ItemInventory inventory && TargetSlot is not null)
{
return inventory.IsInSlot(item, (int)TargetSlot);
}
return false;
}
public override void Reset()
{
base.Reset();
@@ -23,9 +23,8 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; }
int totalEnemies = Targets.Count();
if (totalEnemies == 0) { return 0; }
if (Targets.None()) { return 0; }
if (!character.IsOnPlayerTeam) { return 100; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
@@ -103,13 +103,21 @@ namespace Barotrauma
character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f);
}
}
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
var container = targetItem.GetComponent<ItemContainer>();
var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN,
RemoveExistingWhenNecessary = true
};
if (container.HasSubContainers)
{
objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
}
// Only remove the oxygen source being replaced
objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i);
return objective;
},
onAbandon: () =>
{
@@ -262,7 +262,7 @@ namespace Barotrauma
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit));
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (Mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
@@ -413,28 +413,10 @@ namespace Barotrauma
}
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
bool remove = false;
foreach (ItemComponent ic in item.Components)
{
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character);
if (ic.DeleteOnUse)
{
remove = true;
}
}
if (remove)
{
Entity.Spawner?.AddItemToRemoveQueue(item);
}
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
}
protected override bool CheckObjectiveSpecific()
@@ -502,10 +484,12 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static Barotrauma.CharacterParams;
namespace Barotrauma
{
@@ -44,7 +45,7 @@ namespace Barotrauma
public float PlayTimer { get; set; }
private float? unstunY { get; set; }
public EnemyAIController AiController { get; private set; } = null;
public EnemyAIController AIController { get; private set; } = null;
public Character Owner { get; set; }
@@ -134,8 +135,8 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
break;
}
}
@@ -160,8 +161,8 @@ namespace Barotrauma
public PetBehavior(XElement element, EnemyAIController aiController)
{
AiController = aiController;
AiController.Character.CanBeDragged = true;
AIController = aiController;
AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f);
MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f);
@@ -218,7 +219,7 @@ namespace Barotrauma
bool success = OnEat(item.GetTags());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier);
}
return success;
}
@@ -229,7 +230,7 @@ namespace Barotrauma
bool success = OnEat("dead".ToIdentifier());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName);
}
return success;
}
@@ -252,7 +253,7 @@ namespace Barotrauma
Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif
return true;
}
@@ -265,20 +266,20 @@ namespace Barotrauma
if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; }
PlayTimer = 5.0f;
AiController.Character.IsRagdolled = true;
AIController.Character.IsRagdolled = true;
Happiness += 10.0f;
AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AiController.Character.SimPosition.Y;
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AIController.Character.SimPosition.Y;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
#endif
}
public string GetTagName()
{
if (AiController.Character.Inventory != null)
if (AIController.Character.Inventory != null)
{
foreach (Item item in AiController.Character.Inventory.AllItems)
foreach (Item item in AIController.Character.Inventory.AllItems)
{
var tag = item.GetComponent<NameTag>();
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
@@ -293,7 +294,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
var character = AiController.Character;
var character = AIController.Character;
if (character?.Removed ?? true || character.IsDead) { return; }
if (unstunY.HasValue)
@@ -332,16 +333,27 @@ namespace Barotrauma
Food food = foods[i];
if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
{
if (food.TargetParams == null &&
AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams))
if (food.TargetParams == null)
{
targetParams.IgnoreContained = food.IgnoreContained;
food.TargetParams = targetParams;
if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
{
food.TargetParams = target;
}
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
{
food.TargetParams = targetParams;
}
if (food.TargetParams != null)
{
food.TargetParams.State = AIState.Eat;
food.TargetParams.Priority = food.Priority;
food.TargetParams.IgnoreContained = food.IgnoreContained;
}
}
}
else if (food.TargetParams != null)
{
AiController.AIParams.RemoveTarget(food.TargetParams);
AIController.AIParams.RemoveTarget(food.TargetParams);
food.TargetParams = null;
}
}
@@ -423,7 +435,7 @@ namespace Barotrauma
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false);
var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
{
@@ -116,10 +116,10 @@ namespace Barotrauma
}
// accept only the highest priority order
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
if (CurrentOrder == null || OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
{
#if DEBUG
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
ShipCommandManager.ShipCommandLog($"{this} is no longer the top priority of {OrderedCharacter}, considering the issue unattended.");
#endif
return false;
}
@@ -356,7 +356,7 @@ namespace Barotrauma
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order));
}
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint")))
{
var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>());
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order));
@@ -43,9 +43,9 @@ namespace Barotrauma
steering += DoSteeringSeek(targetSimPos, weight);
}
public void SteeringWander(float weight = 1)
public void SteeringWander(float weight = 1, bool avoidWanderingOutsideLevel = false)
{
steering += DoSteeringWander(weight);
steering += DoSteeringWander(weight, avoidWanderingOutsideLevel);
}
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
@@ -119,7 +119,7 @@ namespace Barotrauma
//return newSteering;
}
protected virtual Vector2 DoSteeringWander(float weight)
protected virtual Vector2 DoSteeringWander(float weight, bool avoidWanderingOutsideLevel)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Vector2.UnitY : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
@@ -127,19 +127,35 @@ namespace Barotrauma
Vector2 displacement = new Vector2(
(float)Math.Cos(wanderAngle),
(float)Math.Sin(wanderAngle));
displacement = displacement * CircleRadius;
displacement *= CircleRadius;
float angleChange = 1.5f;
wanderAngle += Rand.Range(0.0f, 1.0f) * angleChange - angleChange * 0.5f;
Vector2 newSteering = circleCenter + displacement;
if (avoidWanderingOutsideLevel && Level.Loaded != null)
{
float margin = 5000.0f;
if (host.WorldPosition.X < -margin)
{
// Too far left
newSteering.X += (-margin - host.WorldPosition.X) * weight / margin;
}
else if (host.WorldPosition.X > Level.Loaded.Size.X - margin)
{
// Too far right
newSteering.X -= (host.WorldPosition.X - (Level.Loaded.Size.X - margin)) * weight / margin;
}
}
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > weight)
{
newSteering = Vector2.Normalize(newSteering) * weight;
}
return newSteering;
}
@@ -11,8 +11,8 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null, bool spawnInitialItems = true)
: base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll, spawnInitialItems)
{
InitProjSpecific();
}
@@ -87,7 +87,7 @@ namespace Barotrauma
}
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false };
// TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f;
@@ -610,15 +610,18 @@ namespace Barotrauma
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
}
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
if (!head.Disabled)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
}
}
if (!onGround)
@@ -1368,7 +1371,7 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition;
targetMovement = new Vector2(diff.X, 0.0f);
TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left;
@@ -1383,15 +1386,25 @@ namespace Barotrauma
float prevVitality = target.Vitality;
bool wasCritical = prevVitality < 0.0f;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
{
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR);
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical");
if (GameMain.NetworkMember is not { IsClient: true })
{
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
}
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
if (cprAnimTimer > 15.0f && targetHead != null && head != null)
{
@@ -1402,23 +1415,15 @@ namespace Barotrauma
torso.PullJointEnabled = true;
//Serverside code
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
if (GameMain.NetworkMember is not { IsClient: true })
{
if (target.Oxygen < -10.0f)
{
if (powerfulCPR)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
else
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
}
@@ -1453,7 +1458,7 @@ namespace Barotrauma
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
if (powerfulCPR) { reviveChance *= 2.0f; }
reviveChance *= 1f + cprBoost;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
@@ -1833,8 +1838,6 @@ namespace Barotrauma
{
heldItem.FlipX(relativeToSub: false);
}
// TODO: was this added by a mistake?
//heldItem.FlipX(relativeToSub: false);
}
foreach (Limb limb in Limbs)
@@ -873,7 +873,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered) { continue; }
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
limb.Dir = Dir;
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint();
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -189,6 +190,15 @@ namespace Barotrauma
[Serialize(20f, IsPropertySaveable.Yes)]
public float RequiredAngle { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
public float RequiredAngleToShoot { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable]
public float AimRotationTorque { get; set; }
[Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable]
public int RotationLimbIndex { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
@@ -196,7 +206,7 @@ namespace Barotrauma
public float Stun { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; private set; }
public bool OnlyHumans { get; set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
public string ApplyForceOnLimbs
@@ -312,7 +322,7 @@ namespace Barotrauma
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions.Keys)
{
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability));
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction));
}
return multipliedAfflictions;
}
@@ -521,7 +531,7 @@ namespace Barotrauma
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetEntity, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
@@ -529,6 +539,12 @@ namespace Barotrauma
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
}
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
}
return attackResult;
@@ -554,7 +570,15 @@ namespace Barotrauma
DamageParticles(deltaTime, worldPosition);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration);
float penetration = Penetration;
float? penetrationValue = SourceItem?.GetComponent<RangedWeapon>()?.Penetration;
if (penetrationValue.HasValue)
{
penetration += penetrationValue.Value;
}
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
@@ -584,13 +608,19 @@ namespace Barotrauma
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
}
return attackResult;
@@ -25,6 +25,8 @@ namespace Barotrauma
FriendlyNPC = 3
}
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{
public readonly static List<Character> CharacterList = new List<Character>();
@@ -129,6 +131,7 @@ namespace Barotrauma
public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active);
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
public bool IsEscorted { get; set; }
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
public readonly Dictionary<Identifier, SerializableProperty> Properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties
@@ -346,7 +349,7 @@ namespace Barotrauma
private readonly Dictionary<ItemPrefab, double> itemSelectedDurations = new Dictionary<ItemPrefab, double>();
private double itemSelectedTime;
public float InvisibleTimer;
public float InvisibleTimer { get; set; }
public readonly CharacterPrefab Prefab;
@@ -526,7 +529,16 @@ namespace Barotrauma
private Color speechBubbleColor;
private float speechBubbleTimer;
public bool ResetInteract;
/// <summary>
/// Prevents the character from interacting with items or characters
/// </summary>
public bool DisableInteract { get; set; }
/// <summary>
/// Prevents the character from highlighting items or characters with the cursor,
/// meaning it can't interact with anything but the things it has currently selected/equipped
/// </summary>
public bool DisableFocusingOnEntities { get; set; }
//text displayed when the character is highlighted if custom interact is set
public LocalizedString CustomInteractHUDText { get; private set; }
@@ -611,7 +623,9 @@ namespace Barotrauma
CharacterHealth.SetHealthBarVisibility(value == null);
#endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet })
{
#if SERVER
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings })
@@ -793,6 +807,7 @@ namespace Barotrauma
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 EmpVulnerability => Params.Health.EmpVulnerability;
public float Bloodloss
{
@@ -840,6 +855,12 @@ namespace Barotrauma
set;
}
public bool IgnoreMeleeWeapons
{
get;
set;
}
/// <summary>
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
/// </summary>
@@ -890,6 +911,10 @@ namespace Barotrauma
{
itemSelectedTime = Timing.TotalTime;
}
if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null)
{
prevSelectedItem.OnDeselect(this);
}
}
}
/// <summary>
@@ -999,7 +1024,7 @@ namespace Barotrauma
}
}
public bool InWater => AnimController?.InWater ?? false;
public bool InWater => AnimController is AnimController { InWater: true };
public bool GodMode = false;
@@ -1053,6 +1078,8 @@ namespace Barotrauma
}
}
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
@@ -1070,9 +1097,9 @@ namespace Barotrauma
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
/// <param name="hasAi">Is the character controlled by AI.</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null)
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null, bool spawnInitialItems = true)
{
return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, true, ragdoll);
return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent: true, ragdoll, spawnInitialItems);
}
/// <summary>
@@ -1087,16 +1114,16 @@ namespace Barotrauma
/// <param name="hasAi">Is the character controlled by AI.</param>
/// <param name="createNetworkEvent">Should clients receive a network event about the creation of this character?</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true)
public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
speciesName = Path.GetFileNameWithoutExtension(speciesName);
}
return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound);
return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound, spawnInitialItems);
}
public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true)
public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true)
{
var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab == null)
@@ -1113,29 +1140,29 @@ namespace Barotrauma
return null;
}
return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll);
return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, spawnInitialItems);
}
public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null)
public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true)
{
Character newCharacter = null;
if (prefab.Identifier != CharacterPrefab.HumanSpeciesName)
{
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
var ai = new EnemyAIController(aiCharacter, seed);
aiCharacter.SetAI(ai);
newCharacter = aiCharacter;
}
else if (hasAi)
{
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
var ai = new HumanAIController(aiCharacter);
aiCharacter.SetAI(ai);
newCharacter = aiCharacter;
}
else
{
newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
}
#if SERVER
@@ -1152,7 +1179,7 @@ namespace Barotrauma
wallet = new Wallet(Option<Character>.Some(this));
}
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true)
: this(null, id)
{
this.Seed = seed;
@@ -1262,7 +1289,8 @@ namespace Barotrauma
{
Inventory = new CharacterInventory(
inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random),
this);
this,
spawnInitialItems);
}
if (healthElements.Count == 0)
{
@@ -1367,7 +1395,28 @@ namespace Barotrauma
tags.RemoveWhere(t => t.StartsWith("variant"));
tags.Add($"variant{headId.Value}".ToIdentifier());
}
var oldHeadInfo = Info.Head;
Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
if (hairIndex == -1)
{
Info.Head.HairIndex = oldHeadInfo.HairIndex;
}
if (beardIndex == -1)
{
Info.Head.BeardIndex = oldHeadInfo.BeardIndex;
}
if (moustacheIndex == -1)
{
Info.Head.MoustacheIndex = oldHeadInfo.MoustacheIndex;
}
if (faceAttachmentIndex == -1)
{
Info.Head.FaceAttachmentIndex = oldHeadInfo.FaceAttachmentIndex;
}
Info.Head.SkinColor = oldHeadInfo.SkinColor;
Info.Head.HairColor = oldHeadInfo.HairColor;
Info.Head.FacialHairColor = oldHeadInfo.FacialHairColor;
Info.CheckColors();
#if CLIENT
head.RecreateSprites();
#endif
@@ -1574,19 +1623,37 @@ namespace Barotrauma
}
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()]));
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
}
}
}
public float GetSkillLevel(string skillIdentifier) =>
GetSkillLevel(skillIdentifier.ToIdentifier());
private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillOverride },
{ new("medical"), StatTypes.MedicalSkillOverride },
{ new("weapons"), StatTypes.WeaponsSkillOverride },
{ new("electrical"), StatTypes.ElectricalSkillOverride },
{ new("mechanical"), StatTypes.MechanicalSkillOverride }
}.ToImmutableDictionary();
public float GetSkillLevel(Identifier skillIdentifier)
{
if (Info?.Job == null) { return 0.0f; }
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
{
float skillOverride = GetStatValue(statType);
if (skillOverride > skillLevel)
{
skillLevel = skillOverride;
}
}
// apply multipliers first so that multipliers only affect base skill value
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
@@ -1617,6 +1684,7 @@ namespace Barotrauma
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
return skillLevel;
}
@@ -1948,6 +2016,15 @@ namespace Barotrauma
}
}
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim))
{
if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi)
{
enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity);
}
}
if (attackCoolDown > 0.0f)
{
attackCoolDown -= deltaTime;
@@ -1958,7 +2035,7 @@ namespace Barotrauma
{
if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
{
currentAttackTarget = default(AttackTargetData);
currentAttackTarget = default;
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
@@ -2053,35 +2130,50 @@ namespace Barotrauma
}
}
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
if (Inventory != null)
{
foreach (Item item in HeldItems)
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
foreach (Item item in HeldItems)
{
item.SecondaryUse(deltaTime, this);
tryUseItem(item, deltaTime);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
foreach (Item item in Inventory.AllItems)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
{
item.Use(deltaTime, this);
tryUseItem(item, deltaTime);
}
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
}
}
void tryUseItem(Item item, float deltaTime)
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
{
item.SecondaryUse(deltaTime, this);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
}
item.Use(deltaTime, this);
}
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
}
#if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
}
}
@@ -2616,9 +2708,9 @@ namespace Barotrauma
return;
}
if (ResetInteract)
if (DisableInteract)
{
ResetInteract = false;
DisableInteract = false;
return;
}
@@ -2637,27 +2729,33 @@ namespace Barotrauma
#if CLIENT
if (isLocalPlayer)
{
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this))
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities)
{
if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld))
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
{
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
var item = FindItemAtPosition(mouseSimPos, aimAssist);
focusedItem = CanInteract ? item : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
{
FocusedCharacter = null;
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
{
FocusedCharacter = null;
}
findFocusedTimer = 0.05f;
}
else
{
if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; }
if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; }
}
findFocusedTimer = 0.05f;
}
}
else
@@ -2666,6 +2764,7 @@ namespace Barotrauma
focusedItem = null;
}
findFocusedTimer -= deltaTime;
DisableFocusingOnEntities = false;
}
#endif
var head = AnimController.GetLimb(LimbType.Head);
@@ -2886,6 +2985,15 @@ namespace Barotrauma
{
UpdateProjSpecific(deltaTime, cam);
if (InvisibleTimer > 0.0f)
{
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{
InvisibleTimer = Math.Min(InvisibleTimer, 1.0f);
}
InvisibleTimer -= deltaTime;
}
KnockbackCooldownTimer -= deltaTime;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
@@ -2925,6 +3033,7 @@ namespace Barotrauma
}
HideFace = false;
IgnoreMeleeWeapons = false;
UpdateSightRange(deltaTime);
UpdateSoundRange(deltaTime);
@@ -3002,7 +3111,8 @@ namespace Barotrauma
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (aiTarget != null)
//wait 0.1 seconds so status effects that continuously set InDetectable to true can keep the character InDetectable
if (aiTarget != null && Timing.TotalTime > aiTarget.InDetectableSetTime + 0.1f)
{
aiTarget.InDetectable = false;
}
@@ -3788,7 +3898,7 @@ namespace Barotrauma
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -3798,7 +3908,7 @@ namespace Barotrauma
return;
}
#endif
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
@@ -3816,7 +3926,7 @@ namespace Barotrauma
var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
if (referenceLimb != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
if (!IsDead && !ignoreSeveranceProbabilityModifier)
{
probability *= joint.Params.SeveranceProbabilityModifier;
}
@@ -4030,7 +4140,13 @@ 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 > 0 && Params.Health.StunImmunity)
{
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
{
return;
}
}
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
if (Math.Sign(newStun) != Math.Sign(Stun))
{
@@ -4075,7 +4191,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, this, targets);
}
else if (statusEffect.targetLimbs != null)
@@ -4144,11 +4260,14 @@ namespace Barotrauma
if (!isNetworkMessage)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (GameMain.NetworkMember is { IsClient: true }) { return; }
}
CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength));
if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); }
if (GameMain.NetworkMember is not { IsClient: true } || isNetworkMessage)
{
Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true);
}
if (IsDead)
{
BreakJoints();
@@ -4709,6 +4828,8 @@ namespace Barotrauma
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool IsProtectedFromPressure()
{
return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
@@ -4789,6 +4910,32 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
private readonly HashSet<Hull> sameRoomHulls = new();
/// <summary>
/// Check if the character is in the same room
/// Room and hull differ in that a room can consist of multiple linked hulls
/// </summary>
public bool IsInSameRoomAs(Character character)
{
if (character == this) { return true; }
if (character.CurrentHull is null || CurrentHull is null)
{
// Outside doesn't count as a room
return false;
}
if (character.Submarine != Submarine) { return false; }
if (character.CurrentHull == CurrentHull) { return true; }
sameRoomHulls.Clear();
CurrentHull.GetLinkedEntities(sameRoomHulls);
sameRoomHulls.Add(CurrentHull);
return sameRoomHulls.Contains(character.CurrentHull);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -4797,7 +4944,7 @@ namespace Barotrauma
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (talentOption.TalentIdentifiers.None(t => HasTalent(t)))
if (!talentOption.HasMaxTalents(info.UnlockedTalents))
{
return false;
}
@@ -4842,6 +4989,19 @@ namespace Barotrauma
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
public bool HasStoreAccessForItem(ItemPrefab prefab)
{
foreach (CharacterTalent talent in characterTalents)
{
foreach (Identifier unlockedItem in talent.UnlockedStoreItems)
{
if (prefab.Tags.Contains(unlockedItem)) { return true; }
}
}
return false;
}
/// <summary>
/// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains.
/// </summary>
@@ -4900,7 +5060,7 @@ namespace Barotrauma
/// </summary>
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
public float GetStatValue(StatTypes statType)
public float GetStatValue(StatTypes statType, bool includeSaved = true)
{
if (!IsHuman) { return 0f; }
@@ -4913,7 +5073,7 @@ namespace Barotrauma
{
statValue += CharacterHealth.GetStatValue(statType);
}
if (Info != null)
if (Info != null && includeSaved)
{
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
statValue += Info.GetSavedStatValue(statType);
@@ -4998,25 +5158,48 @@ namespace Barotrauma
return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
}
private readonly Dictionary<Identifier, float> abilityResistances = new Dictionary<Identifier, float>();
private readonly Dictionary<TalentResistanceIdentifier, float> abilityResistances = new();
public float GetAbilityResistance(AfflictionPrefab affliction)
{
return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f;
float resistance = 0f;
bool hadResistance = false;
foreach (var (key, value) in abilityResistances)
{
if (key.ResistanceIdentifier == affliction.AfflictionType ||
key.ResistanceIdentifier == affliction.Identifier)
{
resistance += value;
hadResistance = true;
}
}
return hadResistance ? resistance : 1f;
}
public void ChangeAbilityResistance(Identifier resistanceId, float value)
public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
{
if (abilityResistances.ContainsKey(resistanceId))
if (!MathUtils.IsValid(value))
{
abilityResistances[resistanceId] *= value;
#if DEBUG
DebugConsole.ThrowError($"Attempted to set ability resistance to an invalid value ({value})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (abilityResistances.ContainsKey(identifier))
{
abilityResistances[identifier] *= value;
}
else
{
abilityResistances.Add(resistanceId, value);
abilityResistances.Add(identifier, value);
}
}
public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
/// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary>
@@ -5054,6 +5237,16 @@ namespace Barotrauma
}
}
internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter
{
public Character Character { get; set; }
public AbilityCharacterLoot(Character character)
{
Character = character;
}
}
class AbilityCharacterKill : AbilityObject, IAbilityCharacter
{
public AbilityCharacterKill(Character character, Character killer)
@@ -0,0 +1,12 @@
namespace Barotrauma;
partial class CharacterHUD
{
static partial void RecreateHudTextsIfControllingProjSpecific(Character character);
static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items);
public static void RecreateHudTextsIfControlling(Character character) => RecreateHudTextsIfControllingProjSpecific(character);
public static void RecreateHudTextsIfFocused(params Item[] items) => RecreateHudTextsIfFocusedProjSpecific(items);
}
@@ -63,27 +63,34 @@ namespace Barotrauma
public readonly CharacterInfo CharacterInfo;
public readonly HeadPreset Preset;
private int hairIndex;
public int HairIndex { get; set; }
public int HairIndex
private int? hairWithHatIndex;
public void SetHairWithHatIndex()
{
get => hairIndex;
set
if (CharacterInfo.Hairs is null)
{
hairIndex = value;
if (CharacterInfo.Hairs is null)
if (HairIndex == -1)
{
HairWithHatIndex = value;
return;
#if DEBUG
DebugConsole.ThrowError("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#else
DebugConsole.AddWarning("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#endif
}
HairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", hairIndex) ?? -1;
if (HairWithHatIndex < 0 || HairWithHatIndex >= CharacterInfo.Hairs.Count)
hairWithHatIndex = HairIndex;
}
else
{
hairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", HairIndex) ?? -1;
if (hairWithHatIndex < 0 || hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
HairWithHatIndex = hairIndex;
hairWithHatIndex = HairIndex;
}
}
}
public int HairWithHatIndex { get; private set; }
public int BeardIndex;
public int MoustacheIndex;
public int FaceAttachmentIndex;
@@ -99,26 +106,29 @@ namespace Barotrauma
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (hairIndex >= CharacterInfo.Hairs.Count)
if (HairIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})");
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex);
return CharacterInfo.Hairs.ElementAtOrDefault(HairIndex);
}
}
public ContentXElement HairWithHatElement
{
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (HairWithHatIndex >= CharacterInfo.Hairs.Count)
if (hairWithHatIndex == null)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})");
SetHairWithHatIndex();
}
return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex);
if (CharacterInfo.Hairs == null) { return null; }
if (hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairWithHatIndex.Value);
}
}
public ContentXElement BeardElement
{
get
@@ -711,7 +721,7 @@ namespace Barotrauma
private bool IsColorValid(in Color clr)
=> clr.R != 0 || clr.G != 0 || clr.B != 0;
private void CheckColors()
public void CheckColors()
{
if (!IsColorValid(Head.HairColor))
{
@@ -735,9 +745,7 @@ namespace Barotrauma
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<Identifier>(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty<Identifier>()));
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
@@ -813,18 +821,22 @@ namespace Barotrauma
infoElement.GetAttributeIdentifier("npcid", Identifier.Empty));
MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
UnlockedTalents = new HashSet<Identifier>();
foreach (var subElement in infoElement.Elements())
{
bool jobCreated = false;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated)
Identifier elementName = subElement.Name.ToIdentifier();
if (elementName == "job" && !jobCreated)
{
Job = new Job(subElement);
jobCreated = true;
// there used to be a break here, but it had to be removed to make room for statvalues
// using the jobCreated boolean to make sure that only the first job found is created
}
else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase))
else if (elementName == "savedstatvalues")
{
foreach (XElement savedStat in subElement.Elements())
{
@@ -838,8 +850,8 @@ namespace Barotrauma
float value = savedStat.GetAttributeFloat("statvalue", 0f);
if (value == 0f) { continue; }
string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(statIdentifier))
Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
return;
@@ -849,6 +861,20 @@ namespace Barotrauma
ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
}
}
else if (elementName == "talents")
{
Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe
foreach (XElement talentElement in subElement.Elements())
{
if (talentElement.Name.ToIdentifier() != "talent") { continue; }
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
UnlockedTalents.Add(talentIdentifier);
}
}
}
LoadHeadAttachments();
}
@@ -1149,13 +1175,17 @@ namespace Barotrauma
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
float newLevel = Job.GetSkillLevel(skillIdentifier);
if ((int)newLevel > (int)prevLevel)
{
{
float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain);
Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
// assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
@@ -1169,6 +1199,25 @@ namespace Barotrauma
OnSkillChanged(skillIdentifier, prevLevel, newLevel);
}
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillGainSpeed },
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
}.ToImmutableDictionary();
private float GetSkillSpecificGain(float increase, Identifier skillIdentifier)
{
if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType))
{
increase *= 1f + Character.GetStatValue(statType);
}
return increase;
}
public void SetSkillLevel(Identifier skillIdentifier, float level)
{
if (Job == null) { return; }
@@ -1189,15 +1238,11 @@ namespace Barotrauma
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
public void GiveExperience(int amount, bool isMissionExperience = false)
public void GiveExperience(int amount)
{
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
if (isMissionExperience)
{
Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier);
}
experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0;
amount = (int)(amount * experienceGainMultiplier.Value);
@@ -1247,6 +1292,18 @@ namespace Barotrauma
return experienceRequired + ExperienceRequiredPerLevel(level);
}
public int GetExperienceRequiredForLevel(int level)
{
int currentLevel = GetCurrentLevel(out int experienceRequired);
if (currentLevel >= level) { return 0; }
int required = experienceRequired;
for (int i = currentLevel + 1; i <= level; i++)
{
required += ExperienceRequiredPerLevel(i);
}
return required;
}
public int GetCurrentLevel()
{
return GetCurrentLevel(out _);
@@ -1264,7 +1321,7 @@ namespace Barotrauma
return level;
}
private int ExperienceRequiredPerLevel(int level)
private static int ExperienceRequiredPerLevel(int level)
{
return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
}
@@ -1314,7 +1371,6 @@ namespace Barotrauma
new XAttribute("tags", string.Join(",", Head.Preset.TagSet)),
new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("hairindex", Head.HairIndex),
new XAttribute("beardindex", Head.BeardIndex),
@@ -1363,7 +1419,16 @@ namespace Barotrauma
}
}
XElement talentElement = new XElement("Talents");
talentElement.Add(new XAttribute("version", GameMain.Version.ToString()));
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
}
charElement.Add(savedStatElement);
charElement.Add(talentElement);
parentElement?.Add(charElement);
return charElement;
}
@@ -1717,20 +1782,33 @@ namespace Barotrauma
}
}
public void ResetSavedStatValue(string statIdentifier)
public void ResetSavedStatValue(Identifier statIdentifier)
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
bool changed = false;
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (savedStatValue.StatIdentifier != statIdentifier) { continue; }
if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
}
static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
{
if (statIdentifier == identifier) { return true; }
if (identifier.IndexOf('*') is var index and > -1)
{
return statIdentifier.StartsWith(identifier[0..index]);
}
return false;
}
}
public float GetSavedStatValue(StatTypes statType)
@@ -1748,7 +1826,7 @@ namespace Barotrauma
{
if (SavedStatValues.TryGetValue(statType, out var statValues))
{
return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue);
return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue);
}
else
{
@@ -1756,7 +1834,7 @@ namespace Barotrauma
}
}
public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
{
@@ -1779,13 +1857,13 @@ namespace Barotrauma
}
}
public class SavedStatValue
internal sealed class SavedStatValue
{
public string StatIdentifier { get; set; }
public Identifier StatIdentifier { get; set; }
public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; }
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath)
public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath)
{
StatValue = value;
RemoveOnDeath = removeOnDeath;
@@ -1793,7 +1871,7 @@ namespace Barotrauma
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility)
{
@@ -27,6 +27,14 @@ namespace Barotrauma
get { return _strength; }
set
{
if (!MathUtils.IsValid(value))
{
#if DEBUG
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({value})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (_nonClampedStrength < 0 && value > 0)
{
_nonClampedStrength = value;
@@ -53,6 +61,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable]
public bool DivideByLimbCount { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
public bool MultiplyByMaxVitality { get; private set; }
public float DamagePerSecond;
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
@@ -71,6 +82,13 @@ namespace Barotrauma
/// </summary>
public Character Source;
private readonly static LocalizedString[] strengthTexts = new LocalizedString[]
{
TextManager.Get("AfflictionStrengthLow"),
TextManager.Get("AfflictionStrengthMedium"),
TextManager.Get("AfflictionStrengthHigh")
};
public Affliction(AfflictionPrefab prefab, float strength)
{
#if CLIENT
@@ -89,6 +107,16 @@ namespace Barotrauma
}
}
/// <summary>
/// Copy properties here instead of using SerializableProperties (with reflection).
/// </summary>
public void CopyProperties(Affliction source)
{
Probability = source.Probability;
DivideByLimbCount = source.DivideByLimbCount;
MultiplyByMaxVitality = source.MultiplyByMaxVitality;
}
public void Serialize(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
@@ -99,15 +127,26 @@ namespace Barotrauma
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public Affliction CreateMultiplied(float multiplier, float probability)
public Affliction CreateMultiplied(float multiplier, Affliction affliction)
{
var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
instance.Probability = probability;
instance.CopyProperties(affliction);
return instance;
}
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
public LocalizedString GetStrengthText()
{
return GetStrengthText(Strength, Prefab.MaxStrength);
}
public static LocalizedString GetStrengthText(float strength, float maxStrength)
{
return strengthTexts[
MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)];
}
public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength);
public float GetVitalityDecrease(CharacterHealth characterHealth)
@@ -421,15 +460,15 @@ namespace Barotrauma
{
statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
if (characterHealth?.Character?.AnimController?.Limbs != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs);
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets: characterHealth.Character.AnimController.Limbs);
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets));
statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets);
}
}
@@ -10,7 +10,8 @@
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier);
characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime;
if (Source != null)
{
characterHealth.BloodlossAffliction.Source = Source;
@@ -405,45 +405,53 @@ namespace Barotrauma
var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
//the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing)
//that take up the IDs of this appendage
int idOffset = 0;
foreach (var jointElement in root.GetChildElements("joint"))
{
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement))
if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; }
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams)
{
// Ensure that we have a valid id for the new limb
ID = ragdoll.Limbs.Length
};
jointParams.Limb2 = appendageLimbParams.ID;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
//the joint attaches to a limb outside the character's normal limb count = to another part of the appendage
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count)
{
jointParams.Limb1 += idOffset;
}
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams);
if (idOffset == 0)
{
idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID;
}
jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
}
return appendage;
}
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -214,7 +215,6 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.No)]
public Identifier DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Tag { get; private set; }
@@ -276,6 +276,47 @@ namespace Barotrauma
}
}
public class Description
{
public enum TargetType
{
Any,
Self,
OtherCharacter
}
public readonly LocalizedString Text;
public readonly Identifier TextTag;
public readonly float MinStrength, MaxStrength;
public readonly TargetType Target;
public Description(ContentXElement element, AfflictionPrefab affliction)
{
TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty);
if (!TextTag.IsEmpty)
{
Text = TextManager.Get(TextTag);
}
string text = element.GetAttributeString("text", string.Empty);
if (!text.IsNullOrEmpty())
{
Text = Text?.Fallback(text) ?? text;
}
else if (TextTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.");
}
MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
if (MinStrength >= MaxStrength)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.");
}
Target = element.GetAttributeEnum(nameof(Target), TargetType.Any);
}
}
public class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
@@ -313,7 +354,6 @@ namespace Barotrauma
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
private bool disposed = false;
public override void Dispose() { }
public static IEnumerable<AfflictionPrefab> List => Prefabs;
@@ -330,15 +370,22 @@ namespace Barotrauma
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly LocalizedString Name, Description;
public readonly LocalizedString Name;
public readonly Identifier TranslationIdentifier;
public readonly bool IsBuff;
public readonly bool AffectMachines;
public readonly bool HealableInMedicalClinic;
public readonly float HealCostMultiplier;
public readonly int BaseHealCost;
public readonly bool ShowBarInHealthMenu;
public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
private readonly LocalizedString defaultDescription;
public readonly ImmutableList<Description> Descriptions;
public readonly bool HideIconAfterDelay;
//how high the strength has to be for the affliction to take affect
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
@@ -355,6 +402,11 @@ namespace Barotrauma
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
/// </summary>
public ImmutableHashSet<Identifier> IgnoreTreatmentIfAfflictedBy;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// </summary>
@@ -384,6 +436,8 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public readonly bool ResetBetweenRounds;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{
get
@@ -411,13 +465,16 @@ namespace Barotrauma
{
Name = Name.Fallback(fallbackName);
}
Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
{
Description = Description.Fallback(fallbackDescription);
defaultDescription = defaultDescription.Fallback(fallbackDescription);
}
IsBuff = element.GetAttributeBool("isbuff", false);
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
ShowBarInHealthMenu = element.GetAttributeBool("showbarinhealthmenu", true);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
!IsBuff &&
@@ -426,6 +483,8 @@ namespace Barotrauma
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0);
IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty<Identifier>()).ToImmutableHashSet();
Duration = element.GetAttributeFloat(nameof(Duration), 0.0f);
if (element.GetAttribute("nameidentifier") != null)
@@ -443,28 +502,33 @@ namespace Barotrauma
}
}
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f);
HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold",
ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f);
ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f);
CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
IconColors = element.GetAttributeColorArray("iconcolors", null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
IconColors = element.GetAttributeColorArray(nameof(IconColors), null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false);
AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), "");
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -481,15 +545,38 @@ namespace Barotrauma
case "effect":
case "periodiceffect":
break;
case "description":
descriptions.Add(new Description(subElement, this));
break;
default:
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})");
break;
}
}
Descriptions = descriptions.ToImmutableList();
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
public LocalizedString GetDescription(float strength, Description.TargetType targetType)
{
foreach (var description in Descriptions)
{
if (strength < description.MinStrength || strength > description.MaxStrength) { continue; }
switch (targetType)
{
case Description.TargetType.Self:
if (description.Target == Description.TargetType.OtherCharacter) { continue; }
break;
case Description.TargetType.OtherCharacter:
if (description.Target == Description.TargetType.Self) { continue; }
break;
}
return description.Text;
}
return defaultDescription;
}
public static void LoadAllEffects()
{
Prefabs.ForEach(p => p.LoadEffects());
@@ -104,7 +104,7 @@ namespace Barotrauma
public bool DoesBleed
{
get => Character.Params.Health.DoesBleed;
get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine;
private set => Character.Params.Health.DoesBleed = value;
}
@@ -132,7 +132,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); }
get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -140,9 +140,14 @@ namespace Barotrauma
private float vitality;
public float Vitality
{
get
{
return Character.IsDead ? minVitality : vitality;
get
{
if (Character.IsDead)
{
return minVitality;
}
return vitality;
}
private set
{
@@ -539,7 +544,7 @@ namespace Barotrauma
amount -= reduceAmount;
if (treatmentAction != null)
{
if (treatmentAction.Value == ActionType.OnUse)
if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
{
matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
}
@@ -576,6 +581,15 @@ namespace Barotrauma
}
}
private void KillIfOutOfVitality()
{
if (Vitality <= MinVitality &&
!Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
Kill();
}
}
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
@@ -600,7 +614,7 @@ namespace Barotrauma
}
CalculateVitality();
if (Vitality <= MinVitality) { Kill(); }
KillIfOutOfVitality();
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
@@ -679,10 +693,18 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
{
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun")
{
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
{
return;
}
}
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
@@ -716,10 +738,7 @@ namespace Barotrauma
existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
KillIfOutOfVitality();
return;
}
@@ -733,10 +752,7 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
KillIfOutOfVitality();
#if CLIENT
if (OpenHealthWindow != this && limbHealth != null)
{
@@ -831,11 +847,7 @@ namespace Barotrauma
}
#endif
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
KillIfOutOfVitality();
}
}
@@ -868,25 +880,36 @@ namespace Barotrauma
{
if (!Character.NeedsOxygen) { return; }
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
float prevOxygen = OxygenAmount;
if (IsUnconscious)
{
//clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
OxygenAmount = MathHelper.Clamp(OxygenAmount - 1.0f * deltaTime, -100.0f, 100.0f);
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
}
else
{
float decreaseSpeed = -5.0f;
float increaseSpeed = 10.0f;
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance);
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
if (holdBreathMultiplier <= -1.0f)
{
OxygenAmount = -100.0f;
}
else
{
decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
}
}
UpdateOxygenProjSpecific(prevOxygen, deltaTime);
}
partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
@@ -1062,6 +1085,7 @@ namespace Barotrauma
}
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
{
@@ -1217,6 +1241,7 @@ namespace Barotrauma
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
@@ -79,27 +79,35 @@ namespace Barotrauma
public ref readonly ImmutableArray<Identifier> ParsedAfflictionTypes => ref parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName)
public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true)
{
Deserialize(element);
if (element.Attribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
}
foreach (var afflictionType in parsedAfflictionTypes)
if (checkErrors)
{
if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType))
foreach (var afflictionType in parsedAfflictionTypes)
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?");
}
}
foreach (var afflictionIdentifier in parsedAfflictionIdentifiers)
{
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?");
if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?");
}
}
foreach (var afflictionIdentifier in parsedAfflictionIdentifiers)
{
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?");
}
}
if (!parsedAfflictionTypes.Any() && !parsedAfflictionIdentifiers.Any())
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Neither affliction types of identifiers defined.");
}
}
static void createWarningOrError(string msg)
{
#if DEBUG
@@ -757,6 +757,10 @@ namespace Barotrauma
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
{
finalDamageModifier *= character.EmpVulnerability;
}
foreach (DamageModifier damageModifier in tempModifiers)
{
float damageModifierValue = damageModifier.DamageMultiplier;
@@ -766,9 +770,13 @@ namespace Barotrauma
}
finalDamageModifier *= damageModifierValue;
}
if (affliction.MultiplyByMaxVitality)
{
finalDamageModifier *= character.MaxVitality / 100f;
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability);
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction);
}
else
{
@@ -778,6 +786,7 @@ namespace Barotrauma
{
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
newAffliction = abilityAfflictionCharacter.Affliction;
}
if (applyAffliction)
{
@@ -896,6 +905,12 @@ namespace Barotrauma
{
reEnableTimer = duration;
}
#if CLIENT
if (Hidden && LightSource != null)
{
LightSource.Enabled = false;
}
#endif
}
public void ReEnable()
@@ -1189,12 +1204,30 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory)
{
foreach (Item item in inventory.AllItems)
{
if (statusEffect.TargetIdentifiers != null &&
!statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) &&
statusEffect.TargetIdentifiers.None(id => item.HasTag(id)))
{
continue;
}
if (statusEffect.TargetSlot > -1)
{
if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; }
}
targets.Add(item);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
@@ -50,6 +50,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable]
public bool IsMachine { get; set; }
[Serialize(false, IsPropertySaveable.No), Editable]
public bool CanSpeak { get; set; }
@@ -498,6 +501,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float EmpVulnerability { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
@@ -649,7 +655,7 @@ namespace Barotrauma
if (HasTag(tag))
{
target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
return false;
}
else
@@ -1,8 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -33,7 +30,7 @@ namespace Barotrauma.Abilities
NotSelf = 3,
Alive = 4,
Monster = 5,
InFriendlySubmarine = 6,
InFriendlySubmarine = 6
};
protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings)
@@ -41,8 +38,7 @@ namespace Barotrauma.Abilities
List<TargetType> targetTypes = new List<TargetType>();
foreach (string targetTypeString in targetTypeStrings)
{
TargetType targetType = TargetType.Any;
if (!Enum.TryParse(targetTypeString, true, out targetType))
if (!Enum.TryParse(targetTypeString, true, out TargetType targetType))
{
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -8,11 +9,13 @@ namespace Barotrauma.Abilities
{
private readonly List<TargetType> targetTypes;
private List<PropertyConditional> conditionals = new List<PropertyConditional>();
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true));
targetTypes = ParseTargetTypes(
conditionElement.GetAttributeStringArray("targettypes",
conditionElement.GetAttributeStringArray("targettype", Array.Empty<string>())));
foreach (XElement subElement in conditionElement.Elements())
{
@@ -28,13 +31,18 @@ namespace Barotrauma.Abilities
break;
}
}
if (!targetTypes.Any() && !conditionals.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.");
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityCharacter abilityCharacter)
{
if (!(abilityCharacter.Character is Character character)) { return false; }
if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{
@@ -0,0 +1,19 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
{
private readonly Identifier identifier;
public AbilityConditionCharacterNotLooted(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
}
}
}
@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
}
}
}
@@ -1,18 +1,26 @@
using System;
using Barotrauma.Extensions;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItem : AbilityConditionData
{
private readonly string[] identifiers;
private readonly string[] tags;
private readonly ImmutableArray<Identifier> identifiers;
private readonly ImmutableArray<Identifier> tags;
private readonly MapEntityCategory category = MapEntityCategory.None;
public AbilityConditionItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
identifiers = conditionElement.GetAttributeIdentifierArray("identifiers", Array.Empty<Identifier>()).ToImmutableArray();
tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableArray();
category = conditionElement.GetAttributeEnum("category", MapEntityCategory.None);
if (identifiers.None() && tags.None() && category == MapEntityCategory.None)
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.");
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -29,6 +37,11 @@ namespace Barotrauma.Abilities
if (itemPrefab != null)
{
if (category != MapEntityCategory.None)
{
if (!itemPrefab.Category.HasFlag(category)) { return false; }
}
if (identifiers.Any())
{
if (!identifiers.Any(t => itemPrefab.Identifier == t))
@@ -36,7 +49,6 @@ namespace Barotrauma.Abilities
return false;
}
}
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
}
else
@@ -8,6 +8,7 @@ namespace Barotrauma.Abilities
{
private readonly bool? hasOutpost;
private readonly Identifier[] locationIdentifiers;
private readonly bool isPositiveReputation;
public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -16,12 +17,20 @@ namespace Barotrauma.Abilities
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
}
locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>());
isPositiveReputation = conditionElement.GetAttributeBool("ispositivereputation", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityLocation abilityLocation)
{
if (isPositiveReputation)
{
if (abilityLocation.Location?.Reputation is not { } reputation) { return false; }
if (reputation.Value <= 0) { return false; }
}
if (locationIdentifiers.Any())
{
if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; }
@@ -1,38 +1,62 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionMission : AbilityConditionData
{
private readonly MissionType missionType;
private readonly ImmutableHashSet<MissionType> missionType;
private readonly bool isAffiliated;
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
string missionTypeString = conditionElement.GetAttributeString("missiontype", "None");
if (!Enum.TryParse(missionTypeString, out missionType))
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
foreach (string missionTypeString in missionTypeStrings)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type.");
return;
}
if (missionType == MissionType.None)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none.");
return;
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
return;
}
missionTypes.Add(parsedMission);
}
missionType = missionTypes.ToImmutableHashSet();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityMission)?.Mission is Mission mission)
if (abilityObject is IAbilityMission { Mission: { } mission })
{
return mission.Prefab.Type == missionType;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
if (isAffiliated)
{
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
{
if (amount <= 0) { continue; }
Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier);
if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
{
return true;
}
}
return false;
}
return missionType.Contains(mission.Prefab.Type);
}
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
}
}
}
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -0,0 +1,47 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionAllyNearby : AbilityConditionDataless
{
private enum NearbyCharacterTruthy
{
OneCharacterMatches,
NoCharacterMatches
}
private readonly NearbyCharacterTruthy truthyWhen;
private readonly float distance;
public AbilityConditionAllyNearby(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
truthyWhen = conditionElement.GetAttributeEnum("truthywhen", NearbyCharacterTruthy.OneCharacterMatches);
distance = conditionElement.GetAttributeFloat("distance", 10f);
}
protected override bool MatchesConditionSpecific()
{
bool trueCondition = truthyWhen switch
{
NearbyCharacterTruthy.OneCharacterMatches => true,
NearbyCharacterTruthy.NoCharacterMatches => false,
_ => throw new ArgumentOutOfRangeException(nameof(truthyWhen))
};
foreach (Character ally in Character.GetFriendlyCrew(character))
{
if (ally == character) { continue; }
float distanceToCharacter = Vector2.DistanceSquared(ally.WorldPosition, character.WorldPosition);
if (distanceToCharacter < distance * distance)
{
return trueCondition;
}
}
return !trueCondition;
}
}
}
@@ -0,0 +1,22 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCrewMemberUnconscious : AbilityConditionDataless
{
public AbilityConditionCrewMemberUnconscious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (!c.IsDead && c.IsUnconscious)
{
return true;
}
}
return false;
}
}
}
@@ -1,9 +1,5 @@
using System;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using System;
namespace Barotrauma.Abilities
{
@@ -22,7 +18,7 @@ namespace Barotrauma.Abilities
{
if (tags.None())
{
return character.GetEquippedItem(null) is Item;
return character.GetEquippedItem(null) != null;
}
if (requireAll)
@@ -0,0 +1,43 @@
#nullable enable
using System;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionHasLevel : AbilityConditionDataless
{
private readonly Option<int> matchedLevel;
private readonly Option<int> minLevel;
public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0
? Option<int>.Some(match)
: Option<int>.None();
minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0
? Option<int>.Some(min)
: Option<int>.None();
if (matchedLevel.IsNone() && minLevel.IsNone())
{
throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute.");
}
}
protected override bool MatchesConditionSpecific()
{
if (matchedLevel.TryUnwrap(out int match))
{
return character.Info.GetCurrentLevel() == match;
}
if (minLevel.TryUnwrap(out int min))
{
return character.Info.GetCurrentLevel() >= min;
}
return false;
}
}
}
@@ -1,13 +1,11 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
{
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
private readonly float min;
private readonly PermanentStatPlaceholder placeholder;
public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -19,11 +17,14 @@ namespace Barotrauma.Abilities
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
min = conditionElement.GetAttributeFloat("min", 0f);
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
protected override bool MatchesConditionSpecific()
{
return character.Info.GetSavedStatValue(statType, statIdentifier) >= min;
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
}
}
@@ -0,0 +1,19 @@
namespace Barotrauma.Abilities
{
class AbilityConditionHasTalent : AbilityConditionDataless
{
private readonly Identifier talentIdentifier;
public AbilityConditionHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific()
{
bool result = character.HasTalent(talentIdentifier);
return result;
}
}
}
@@ -0,0 +1,34 @@
#nullable enable
using System.Collections.Immutable;
namespace Barotrauma.Abilities;
internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless
{
private readonly ImmutableHashSet<Identifier> tags;
public AbilityConditionHoldingItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
tags = conditionElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
}
protected override bool MatchesConditionSpecific()
{
if (tags.Count is 0)
{
return HasItemInHand(character, null);
}
foreach (Identifier tag in tags)
{
if (HasItemInHand(character, tag)) { return true; }
}
return false;
static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) =>
character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null ||
character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null;
}
}
@@ -0,0 +1,23 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless
{
public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
int ownLevel = character.Info.GetCurrentLevel();
foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (crew == character) { continue; }
if (crew.Info.GetCurrentLevel() < ownLevel) { return false; }
}
return true;
}
}
}
@@ -0,0 +1,39 @@
#nullable enable
using System;
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities;
internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDataless
{
private readonly float distance;
private readonly int count;
private readonly ImmutableHashSet<TargetType> targetTypes;
public AbilityConditionNearbyCharacterCount(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
distance = conditionElement.GetAttributeFloat("distance", 10f);
count = conditionElement.GetAttributeInt("count", 1);
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true)).ToImmutableHashSet();
}
protected override bool MatchesConditionSpecific()
{
int amountNeeded = count;
foreach (Character otherCharacter in Character.CharacterList)
{
if (character.Submarine != otherCharacter.Submarine) { continue; }
if (!IsViableTarget(targetTypes, otherCharacter)) { continue; }
if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance)
{
amountNeeded--;
if (amountNeeded <= 0) { return true; }
}
}
return false;
}
}
@@ -15,5 +15,4 @@ namespace Barotrauma.Abilities
}
public Character Character { get; set; }
}
}
@@ -34,7 +34,7 @@ namespace Barotrauma.Abilities
public bool IsViable()
{
if (!AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (!AllowClientSimulation && GameMain.NetworkMember is { IsClient: true }) { return false; }
if (RequiresAlive && Character.IsDead) { return false; }
return true;
}
@@ -67,7 +67,7 @@ namespace Barotrauma.Abilities
if (abilityObject is null)
{
ApplyEffect();
}
}
else
{
ApplyEffect(abilityObject);
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -0,0 +1,35 @@
#nullable enable
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityApplyStatusEffectToNonHumans : CharacterAbilityApplyStatusEffects
{
private readonly float maxDistance;
public CharacterAbilityApplyStatusEffectToNonHumans(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue);
}
protected override void ApplyEffect()
{
foreach (Character character in Character.CharacterList)
{
if (character.IsHuman) { continue; }
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
}
ApplyEffectSpecific(character);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -17,6 +17,8 @@ namespace Barotrauma.Abilities
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private bool effectBeingApplied;
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
@@ -29,44 +31,57 @@ namespace Barotrauma.Abilities
protected void ApplyEffectSpecific(Character targetCharacter)
{
foreach (var statusEffect in statusEffects)
//prevent an infinite loop if an effect triggers itself
//(e.g. a talent that triggers when an affliction is applied, and applies that same affliction)
if (effectBeingApplied) { return; }
effectBeingApplied = true;
try
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
foreach (var statusEffect in statusEffects)
{
// currently used to spawn items on the targeted character
statusEffect.SetUser(targetCharacter);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets));
if (!nearbyCharactersAppliesToSelf)
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
targets.RemoveAll(c => c == Character);
// currently used to spawn items on the targeted character
statusEffect.SetUser(targetCharacter);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
}
if (!nearbyCharactersAppliesToAllies)
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
targets.Clear();
statusEffect.AddNearbyTargets(targetCharacter.WorldPosition, targets);
if (!nearbyCharactersAppliesToSelf)
{
targets.RemoveAll(c => c == Character);
}
if (!nearbyCharactersAppliesToAllies)
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
if (!nearbyCharactersAppliesToEnemies)
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
else
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
else
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
}
}
finally
{
effectBeingApplied = false;
}
}
protected override void ApplyEffect()
{
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
@@ -6,11 +7,18 @@ namespace Barotrauma.Abilities
{
private readonly bool allowSelf;
private readonly float maxDistance = float.MaxValue;
private readonly bool inSameRoom;
private readonly ImmutableHashSet<Identifier> jobIdentifiers;
public override bool AllowClientSimulation { get; }
public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
allowSelf = abilityElement.GetAttributeBool("allowself", true);
maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue);
inSameRoom = abilityElement.GetAttributeBool("insameroom", false);
jobIdentifiers = abilityElement.GetAttributeIdentifierImmutableHashSet("jobs", ImmutableHashSet<Identifier>.Empty);
AllowClientSimulation = abilityElement.GetAttributeBool("allowclientsimulation", true);
}
@@ -19,6 +27,27 @@ namespace Barotrauma.Abilities
foreach (Character character in Character.GetFriendlyCrew(Character))
{
if (!allowSelf && character == Character) { continue; }
if (!jobIdentifiers.IsEmpty)
{
bool hadJob = false;
foreach (Identifier job in jobIdentifiers)
{
if (character.HasJob(job.Value))
{
hadJob = true;
break;
}
}
if (!hadJob) { continue; }
}
if (inSameRoom && !character.IsInSameRoomAs(Character))
{
continue;
}
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
@@ -0,0 +1,63 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityApplyStatusEffectsToApprenticeship : CharacterAbilityApplyStatusEffects
{
private readonly bool invert;
private readonly ImmutableHashSet<JobPrefab> jobPrefabList = JobPrefab.Prefabs.ToImmutableHashSet();
public CharacterAbilityApplyStatusEffectsToApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
invert = abilityElement.GetAttributeBool("invert", false);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList);
if (apprenticeJob is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
return;
}
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
JobPrefab? characterJob = character.Info?.Job?.Prefab;
if (characterJob is null) { continue; }
switch (characterJob.Identifier == apprenticeJob.Identifier)
{
case true when invert:
continue;
case false when !invert:
continue;
}
ApplyEffectSpecific(character);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
public static JobPrefab? GetApprenticeJob(Character character, IReadOnlyCollection<JobPrefab> jobList)
{
foreach (JobPrefab prefab in jobList)
{
if (character.Info.GetSavedStatValue(StatTypes.Apprenticeship, prefab.Identifier) > 0)
{
return prefab;
}
}
return null;
}
}
}
@@ -1,17 +1,21 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{
private readonly Identifier skillIdentifier;
private readonly bool ignoreAbilitySkillGain;
private readonly bool ignoreAbilitySkillGain,
targetAllies;
public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", "");
ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -19,7 +23,19 @@ namespace Barotrauma.Abilities
if (abilityObject is AbilitySkillGain abilitySkillGain)
{
if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; }
Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true);
Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier;
if (targetAllies)
{
foreach (Character otherCharacter in Character.GetFriendlyCrew(Character))
{
if (otherCharacter == Character) { continue; }
otherCharacter.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
}
}
else
{
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
}
}
else
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveAffliction : CharacterAbility
{
@@ -18,7 +16,7 @@ namespace Barotrauma.Abilities
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.");
}
}
@@ -0,0 +1,53 @@
namespace Barotrauma.Abilities;
internal sealed class CharacterAbilityGiveExperience : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int amount;
private readonly int level;
public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
level = abilityElement.GetAttributeInt("level", 0);
if (amount == 0 && level == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.");
}
if (amount > 0 && level > 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.");
}
}
private void ApplyEffectSpecific(Character targetCharacter)
{
if (amount != 0)
{
targetCharacter.Info?.GiveExperience(amount);
}
if (level > 0)
{
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level));
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
else
{
ApplyEffectSpecific(Character);
}
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveFlag : CharacterAbility
{
@@ -0,0 +1,31 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveItemStat : CharacterAbility
{
private readonly ItemTalentStats stat;
private readonly float value;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
{
ApplyEffect();
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent);
}
}
}
@@ -0,0 +1,41 @@
#nullable enable
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveItemStatToTags: CharacterAbility
{
private readonly ItemTalentStats stat;
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
{
ApplyEffect();
}
}
protected override void ApplyEffect()
{
if (Character?.Submarine is null) { return; }
foreach (Item item in Character.Submarine.GetItems(true))
{
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, value, CharacterTalent);
}
}
}
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveMoney : CharacterAbility
{
@@ -13,6 +11,11 @@ namespace Barotrauma.Abilities
{
amount = abilityElement.GetAttributeInt("amount", 0);
scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.");
}
}
private void ApplyEffectSpecific(Character targetCharacter)
@@ -1,11 +1,15 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
public enum PermanentStatPlaceholder
{
None,
LocationName,
LocationIndex
}
class CharacterAbilityGivePermanentStat : CharacterAbility
{
private readonly string statIdentifier;
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
private readonly float value;
private readonly float maxValue;
@@ -13,6 +17,7 @@ namespace Barotrauma.Abilities
private readonly bool removeOnDeath;
private readonly bool giveOnAddingFirstTime;
private readonly bool setValue;
private readonly PermanentStatPlaceholder placeholder;
//private readonly float maximumValue;
public override bool AllowClientSimulation => true;
@@ -20,7 +25,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.");
}
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
@@ -29,6 +38,7 @@ namespace Barotrauma.Abilities
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -51,14 +61,33 @@ namespace Barotrauma.Abilities
private void ApplyEffectSpecific()
{
Identifier identifier = HandlePlaceholders(placeholder, statIdentifier);
if (targetAllies)
{
Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue));
foreach (Character c in Character.GetFriendlyCrew(Character))
{
c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
else
{
Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
public static Identifier HandlePlaceholders(PermanentStatPlaceholder placeholder, Identifier original)
{
if (GameMain.GameSession?.Campaign?.Map is not { } map) { return original; }
switch (placeholder)
{
case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location:
return original.Replace("[placeholder]", location.Name);
case PermanentStatPlaceholder.LocationIndex:
return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString());
}
return original;
}
}
}
@@ -0,0 +1,39 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveReputation : CharacterAbility
{
private readonly Identifier factionIdentifier;
private readonly float amount;
public CharacterAbilityGiveReputation(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
amount = abilityElement.GetAttributeFloat("amount", 0f);
if (factionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.");
}
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.");
}
}
protected override void ApplyEffect()
{
if (GameMain.GameSession?.Campaign is not { } campaign) { return; }
foreach (Faction faction in campaign.Factions)
{
if (faction.Prefab.Identifier != factionIdentifier) { continue; }
faction.Reputation.AddReputation(amount);
break;
}
}
protected override void ApplyEffect(AbilityObject abilityObject) => ApplyEffect();
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveResistance : CharacterAbility
{
@@ -10,17 +8,23 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty));
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f);
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.");
}
if (MathUtils.NearlyEqual(multiplier, 1))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.");
}
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.ChangeAbilityResistance(resistanceId, multiplier);
TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier);
Character.ChangeAbilityResistance(identifier, multiplier);
}
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveStat : CharacterAbility
{
@@ -1,7 +1,4 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveTalentPoints : CharacterAbility
{
@@ -9,7 +6,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
}
}
public override void InitializeAbility(bool addingFirstTime)
@@ -0,0 +1,29 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveTalentPointsToAllies : CharacterAbility
{
private readonly int amount;
public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
}
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (character.Info is null) { return; }
character.Info.AdditionalTalentPoints += amount;
}
}
}
}
@@ -1,6 +1,4 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -0,0 +1,22 @@
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityMarkAsLooted: CharacterAbility
{
private readonly Identifier identifier;
public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter { Character: { } character }) { return; }
character.MarkedAsLooted.Add(identifier);
}
}
}
@@ -1,30 +1,36 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAffliction : CharacterAbility
{
private readonly string[] afflictionIdentifiers;
private readonly Identifier[] afflictionIdentifiers;
private readonly Identifier replaceWith;
private readonly float addedMultiplier;
public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true);
afflictionIdentifiers = abilityElement.GetAttributeIdentifierArray("afflictionidentifiers", System.Array.Empty<Identifier>());
replaceWith = abilityElement.GetAttributeIdentifier("replacewith", Identifier.Empty);
addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
var abilityAffliction = abilityObject as IAbilityAffliction;
if (abilityAffliction?.Affliction is Affliction affliction)
{
foreach (string afflictionIdentifier in afflictionIdentifiers)
foreach (Identifier afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier == afflictionIdentifier)
if (affliction.Identifier != afflictionIdentifier) { continue; }
affliction.Strength *= 1 + addedMultiplier;
if (!replaceWith.IsEmpty)
{
affliction.Strength *= 1 + addedMultiplier;
}
if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab))
{
abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength);
}
}
}
}
else
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyFlag : CharacterAbility
{
@@ -1,23 +1,25 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyResistance : CharacterAbility
{
private readonly Identifier resistanceId;
private readonly float resistance;
private readonly float multiplier;
bool lastState;
public override bool AllowClientSimulation => true;
// should probably be split to different classes
public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", "");
resistance = abilityElement.GetAttributeFloat("resistance", 1f);
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty));
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f);
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.");
}
if (MathUtils.NearlyEqual(multiplier, 1.0f))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.");
}
}
@@ -25,7 +27,15 @@ namespace Barotrauma.Abilities
{
if (conditionsMatched != lastState)
{
Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance);
TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier);
if (conditionsMatched)
{
Character.ChangeAbilityResistance(identifier, multiplier);
}
else
{
Character.RemoveAbilityResistance(identifier);
}
lastState = conditionsMatched;
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyStat : CharacterAbility
{
@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,5 +1,4 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyValue : CharacterAbility
{
@@ -11,6 +9,10 @@ namespace Barotrauma.Abilities
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f))
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityPutItem : CharacterAbility
{
@@ -0,0 +1,27 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityReduceAffliction : CharacterAbility
{
private readonly Identifier afflictionId;
private readonly float amount;
public CharacterAbilityReduceAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty));
amount = abilityElement.GetAttributeFloat("amount", 0);
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter character) { return; }
character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount);
}
}
}
@@ -0,0 +1,19 @@
#nullable enable
using Barotrauma.Items.Components;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility
{
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; }
int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced);
ingredients.Items.RemoveAt(randomIndex);
}
}
}
@@ -1,16 +1,19 @@
using System.Xml.Linq;

namespace Barotrauma.Abilities
{
class CharacterAbilityResetPermanentStat : CharacterAbility
{
private readonly string statIdentifier;
private readonly Identifier statIdentifier;
public override bool AppliesEffectOnIntervalUpdate => true;
public override bool AllowClientSimulation => true;
public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
@@ -1,29 +0,0 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityRevive : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
private void ApplyEffectSpecific()
{
Character.Revive(removeAllAfflictions: false);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
}
}
}
@@ -0,0 +1,38 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilitySetMetadataInt : CharacterAbility
{
private readonly Identifier identifier;
private readonly int value;
public CharacterAbilitySetMetadataInt(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
value = abilityElement.GetAttributeInt("value", 0);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.");
}
}
public override void InitializeAbility(bool addingFirstTime)
{
ApplyEffect();
}
protected override void ApplyEffect()
{
if (identifier == Identifier.Empty) { return; }
if (GameMain.GameSession?.Campaign?.CampaignMetadata is not { } metadata) { return; }
metadata.SetValue(identifier, value);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,29 +0,0 @@
namespace Barotrauma.Abilities
{
class CharacterAbilityUnlockTree : CharacterAbility
{
public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
var subTree = talentTree.TalentSubTrees.Find(t => t.AllTalentIdentifiers.Contains(CharacterTalent.Prefab.Identifier));
if (subTree == null) { return; }
subTree.ForceUnlock = true;
if (!addingFirstTime) { return; }
foreach (var talentId in subTree.AllTalentIdentifiers)
{
if (talentId == CharacterTalent.Prefab.Identifier) { continue; }
if (Character.GiveTalent(talentId))
{
Character.Info.AdditionalTalentPoints++;
}
}
}
}
}
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityApprenticeship : CharacterAbility
{
@@ -1,20 +1,17 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityAtmosMachine : CharacterAbility
{
private readonly float addedValue;
private readonly float multiplyValue;
private readonly Identifier[] tags;
private readonly int maxMultiplyCount;
public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>());
maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue);
}
@@ -1,11 +1,8 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityBountyHunter : CharacterAbility
{
private float vitalityPercentage;
private readonly float vitalityPercentage;
public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityMultitasker : CharacterAbility
{
@@ -1,12 +1,10 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityPsychoClown : CharacterAbility
{
private StatTypes statType;
private float maxValue;
private string afflictionIdentifier;
private readonly StatTypes statType;
private readonly float maxValue;
private readonly string afflictionIdentifier;
private float lastValue = 0f;
public override bool AllowClientSimulation => true;
@@ -1,8 +1,5 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -0,0 +1,54 @@
#nullable enable
using System;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityUnlockApprenticeshipTalentTree : CharacterAbility
{
public override bool AllowClientSimulation => false;
public CharacterAbilityUnlockApprenticeshipTalentTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
// do not run client-side in multiplayer
if (GameMain.NetworkMember is { IsClient: true }) { return; }
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
return;
}
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
{
if (subTree.Type != TalentTreeType.Specialization) { continue; }
talentsTrees.Add(subTree.AllTalentIdentifiers);
}
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
foreach (Identifier identifier in selectedTalentTree)
{
if (Character.HasTalent(identifier)) { continue; }
Character.GiveTalent(identifier);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -18,12 +18,15 @@ namespace Barotrauma.Abilities
protected readonly int maxTriggerCount;
protected int timesTriggered = 0;
// add support for OR conditions?
// add support for OR conditions?
protected readonly List<AbilityCondition> abilityConditions = new List<AbilityCondition>();
// separate dictionaries for each type of characterability?
protected readonly List<CharacterAbility> characterAbilities = new List<CharacterAbility>();
/// <summary>
/// List of abilities that are triggered by this group.
/// Fallback abilities are triggered if the conditional fails
/// </summary>
protected readonly List<CharacterAbility> characterAbilities = new List<CharacterAbility>(),
fallbackAbilities = new List<CharacterAbility>();
public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup)
{
@@ -38,19 +41,45 @@ namespace Barotrauma.Abilities
case "abilities":
LoadAbilities(subElement);
break;
case "fallbackabilities":
LoadFallbackAbilities(subElement);
break;
case "conditions":
LoadConditions(subElement);
break;
}
}
switch (abilityEffectType)
{
case AbilityEffectType.OnDieToCharacter:
if (characterAbilities.Any(a => a.RequiresAlive))
{
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.");
}
break;
}
}
public void ActivateAbilityGroup(bool addingFirstTime)
{
if (!CheckActivatingCondition()) { return; }
foreach (var characterAbility in characterAbilities)
{
characterAbility.InitializeAbility(addingFirstTime);
}
foreach (var characterAbility in fallbackAbilities)
{
characterAbility.InitializeAbility(addingFirstTime);
}
}
private bool CheckActivatingCondition()
{
if (AbilityEffectType is not AbilityEffectType.None) { return true; }
return !abilityConditions.Any(static abilityCondition => !abilityCondition.MatchesCondition());
}
public void LoadConditions(ContentXElement conditionElements)
@@ -85,6 +114,17 @@ namespace Barotrauma.Abilities
characterAbilities.Add(characterAbility);
}
public void AddFallbackAbility(CharacterAbility characterAbility)
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
return;
}
fallbackAbilities.Add(characterAbility);
}
// XML
private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true)
{
@@ -135,6 +175,14 @@ namespace Barotrauma.Abilities
}
}
private void LoadFallbackAbilities(ContentXElement abilityElements)
{
foreach (var abilityElementGroup in abilityElements.Elements())
{
AddFallbackAbility(ConstructAbility(abilityElementGroup, CharacterTalent));
}
}
private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent)
{
CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this);
@@ -1,29 +1,38 @@
namespace Barotrauma.Abilities
using System.Collections.Generic;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupEffect : CharacterAbilityGroup
{
public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
base(abilityEffectType, characterTalent, abilityElementGroup) { }
public void CheckAbilityGroup(AbilityObject abilityObject)
{
if (!IsActive) { return; }
if (IsApplicable(abilityObject))
if (IsOverTriggerCount) { return; }
List<CharacterAbility> abilities = IsApplicable(abilityObject) ? characterAbilities : fallbackAbilities;
foreach (CharacterAbility characterAbility in abilities)
{
foreach (var characterAbility in characterAbilities)
if (characterAbility.IsViable())
{
if (characterAbility.IsViable())
{
characterAbility.ApplyAbilityEffect(abilityObject);
}
characterAbility.ApplyAbilityEffect(abilityObject);
}
}
if (abilities.Count > 0)
{
timesTriggered++;
}
}
private bool IsOverTriggerCount => timesTriggered >= maxTriggerCount;
private bool IsApplicable(AbilityObject abilityObject)
{
if (timesTriggered >= maxTriggerCount) { return false; }
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition(abilityObject))
@@ -31,7 +40,8 @@
return false;
}
}
return true;
}
}
}
}
@@ -1,4 +1,7 @@
namespace Barotrauma.Abilities
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupInterval : CharacterAbilityGroup
{
@@ -9,48 +12,72 @@
private float effectDelayTimer;
public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
base(abilityEffectType, characterTalent, abilityElementGroup)
{
{
// too many overlapping intervals could cause hitching? maybe randomize a little
interval = abilityElementGroup.GetAttributeFloat("interval", 0f);
effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f);
}
public void UpdateAbilityGroup(float deltaTime)
{
if (!IsActive) { return; }
TimeSinceLastUpdate += deltaTime;
if (TimeSinceLastUpdate >= interval)
{
bool conditionsMatched = IsApplicable();
effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f;
conditionsMatched &= effectDelayTimer >= effectDelay;
foreach (var characterAbility in characterAbilities)
{
if (characterAbility.IsViable())
{
characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate);
}
}
if (conditionsMatched)
{
timesTriggered++;
}
TimeSinceLastUpdate = 0;
TimeSinceLastUpdate += deltaTime;
if (TimeSinceLastUpdate < interval) { return; }
bool conditionsMatched;
if (AllConditionsMatched())
{
effectDelayTimer += TimeSinceLastUpdate;
bool shouldApplyDelayedEffect = effectDelayTimer >= effectDelay;
conditionsMatched = shouldApplyDelayedEffect;
}
else
{
effectDelayTimer = 0f;
conditionsMatched = false;
}
bool hasFallbacks = fallbackAbilities.Count > 0;
List<CharacterAbility> abilitiesToRun =
!conditionsMatched && hasFallbacks
? fallbackAbilities
: characterAbilities;
if (hasFallbacks)
{
conditionsMatched = true;
}
foreach (var characterAbility in abilitiesToRun)
{
if (!characterAbility.IsViable()) { continue; }
characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate);
}
if (conditionsMatched)
{
timesTriggered++;
}
TimeSinceLastUpdate = 0;
}
private bool IsApplicable()
private bool AllConditionsMatched()
{
if (timesTriggered >= maxTriggerCount) { return false; }
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition())
{
return false;
}
if (!abilityCondition.MatchesCondition()) { return false; }
}
return true;
}
}
}
}
@@ -19,6 +19,7 @@ namespace Barotrauma
// works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well
public List<Identifier> UnlockedRecipes { get; } = new List<Identifier>();
public List<Identifier> UnlockedStoreItems { get; } = new List<Identifier>();
public CharacterTalent(TalentPrefab talentPrefab, Character character)
{
@@ -45,7 +46,17 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier);
DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}");
}
break;
case "addedstoreitem":
if (subElement.GetAttributeIdentifier("itemtag", Identifier.Empty) is { IsEmpty: false } storeItemTag)
{
UnlockedStoreItems.Add(storeItemTag);
}
else
{
DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}");
}
break;
}

Some files were not shown because too many files have changed in this diff Show More