Build 0.20.0.0
This commit is contained in:
@@ -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)
|
||||
@@ -804,10 +805,6 @@ namespace Barotrauma
|
||||
Reverse = true;
|
||||
run = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Reverse = false;
|
||||
}
|
||||
SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
|
||||
}
|
||||
else
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1854,7 +1837,33 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
|
||||
if (AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
|
||||
{
|
||||
bool advance = !canAttack && Character.InWater || 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.InWater)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
FaceTarget(SelectedAiTarget.Entity);
|
||||
}
|
||||
}
|
||||
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
|
||||
{
|
||||
if (pathSteering != null)
|
||||
{
|
||||
@@ -1865,20 +1874,30 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable;
|
||||
if (AttackLimb?.attack is Attack { Ranged: true} attack)
|
||||
{
|
||||
Limb limb = GetLimbToRotate(attack);
|
||||
if (limb != null)
|
||||
{
|
||||
Vector2 toTarget = damageTarget.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;
|
||||
}
|
||||
}
|
||||
if (canAttack)
|
||||
{
|
||||
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb))
|
||||
if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
|
||||
{
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
}
|
||||
@@ -2114,13 +2133,14 @@ 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; }
|
||||
ActiveAttack = AttackLimb.attack;
|
||||
if (wallTarget != null)
|
||||
{
|
||||
// If the selected target is not the wall target, make the wall target the selected target.
|
||||
@@ -2131,83 +2151,94 @@ namespace Barotrauma
|
||||
State = AIState.Attack;
|
||||
}
|
||||
}
|
||||
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
|
||||
if (damageTarget != null)
|
||||
if (damageTarget == null) { return false; }
|
||||
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 (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(
|
||||
attackingLimb,
|
||||
AttackLimb,
|
||||
damageTarget,
|
||||
targetLimb,
|
||||
SimPosition));
|
||||
#else
|
||||
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
|
||||
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
|
||||
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
|
||||
{
|
||||
if (ActiveAttack.CoolDownTimer > 0)
|
||||
{
|
||||
if (attackingLimb.attack.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 +2330,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 +2349,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 +3469,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 +3477,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 +3515,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;
|
||||
|
||||
+4
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -186,8 +186,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.SelectedItem != Item)
|
||||
{
|
||||
if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) ||
|
||||
Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true))
|
||||
if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) ||
|
||||
Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true))
|
||||
{
|
||||
character.SelectedItem = Item;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
+14
-11
@@ -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)
|
||||
@@ -1389,7 +1392,7 @@ namespace Barotrauma
|
||||
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");
|
||||
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
|
||||
@@ -1406,7 +1409,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (target.Oxygen < -10.0f)
|
||||
{
|
||||
if (powerfulCPR)
|
||||
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);
|
||||
@@ -1453,7 +1456,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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
@@ -1337,7 +1337,7 @@ namespace Barotrauma
|
||||
bool limbsValid = true;
|
||||
foreach (Limb limb in limbs)
|
||||
{
|
||||
if (limb.body == null || !limb.body.Enabled) { continue; }
|
||||
if (limb?.body == null || !limb.body.Enabled) { continue; }
|
||||
if (!CheckValidity(limb.body))
|
||||
{
|
||||
limbsValid = false;
|
||||
@@ -1959,7 +1959,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Limb l in Limbs)
|
||||
{
|
||||
l.Remove();
|
||||
l?.Remove();
|
||||
}
|
||||
limbs = null;
|
||||
}
|
||||
@@ -1968,7 +1968,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (PhysicsBody b in collider)
|
||||
{
|
||||
b.Remove();
|
||||
b?.Remove();
|
||||
}
|
||||
collider = null;
|
||||
}
|
||||
@@ -1977,7 +1977,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var joint in LimbJoints)
|
||||
{
|
||||
var j = joint.Joint;
|
||||
var j = joint?.Joint;
|
||||
if (GameMain.World.JointList.Contains(j))
|
||||
{
|
||||
GameMain.World.Remove(j);
|
||||
|
||||
@@ -189,6 +189,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>
|
||||
@@ -529,6 +538,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;
|
||||
@@ -591,6 +606,12 @@ namespace Barotrauma
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -129,6 +129,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
|
||||
@@ -611,7 +612,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 })
|
||||
@@ -999,7 +1002,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool InWater => AnimController?.InWater ?? false;
|
||||
public bool InWater => AnimController is AnimController { InWater: true };
|
||||
|
||||
public bool GodMode = false;
|
||||
|
||||
@@ -1053,6 +1056,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public HashSet<Identifier> MarkedAsLooted = new();
|
||||
|
||||
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
|
||||
|
||||
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
|
||||
@@ -1574,14 +1579,23 @@ 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; }
|
||||
@@ -1617,6 +1631,16 @@ namespace Barotrauma
|
||||
|
||||
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
|
||||
|
||||
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
|
||||
{
|
||||
float skillOverride = GetStatValue(statType);
|
||||
if (skillOverride > skillLevel)
|
||||
{
|
||||
skillLevel = skillOverride;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return skillLevel;
|
||||
}
|
||||
|
||||
@@ -2058,30 +2082,42 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Item item in HeldItems)
|
||||
{
|
||||
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
|
||||
tryUseItem(item, deltaTime);
|
||||
}
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
{
|
||||
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
|
||||
{
|
||||
item.SecondaryUse(deltaTime, this);
|
||||
tryUseItem(item, deltaTime);
|
||||
}
|
||||
if (IsKeyDown(InputType.Use) && !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 (IsKeyDown(InputType.Shoot) && item.IsShootable)
|
||||
{
|
||||
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
|
||||
{
|
||||
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
|
||||
{
|
||||
item.Use(deltaTime, this);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2721,6 +2757,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
bool selectInputSameAsDeselect = false;
|
||||
#if CLIENT
|
||||
selectInputSameAsDeselect = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect];
|
||||
#endif
|
||||
|
||||
if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars
|
||||
{
|
||||
DeselectCharacter();
|
||||
@@ -2760,14 +2801,16 @@ namespace Barotrauma
|
||||
{
|
||||
FocusedCharacter.onCustomInteract(FocusedCharacter, this);
|
||||
}
|
||||
else if (IsKeyHit(InputType.Deselect) && SelectedItem != null)
|
||||
else if (IsKeyHit(InputType.Deselect) && SelectedItem != null &&
|
||||
(focusedItem == null || focusedItem == SelectedItem || !selectInputSameAsDeselect))
|
||||
{
|
||||
SelectedItem = null;
|
||||
#if CLIENT
|
||||
CharacterHealth.OpenHealthWindow = null;
|
||||
#endif
|
||||
}
|
||||
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null)
|
||||
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
|
||||
(focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
|
||||
{
|
||||
SelectedSecondaryItem = null;
|
||||
#if CLIENT
|
||||
@@ -2782,6 +2825,10 @@ namespace Barotrauma
|
||||
{
|
||||
#if CLIENT
|
||||
if (CharacterInventory.DraggingItemToWorld) { return; }
|
||||
if (selectInputSameAsDeselect)
|
||||
{
|
||||
keys[(int)InputType.Deselect].Reset();
|
||||
}
|
||||
#endif
|
||||
bool canInteract = focusedItem.TryInteract(this);
|
||||
#if CLIENT
|
||||
@@ -3787,7 +3834,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; }
|
||||
@@ -3805,7 +3852,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 && probability < 1)
|
||||
{
|
||||
probability *= joint.Params.SeveranceProbabilityModifier;
|
||||
}
|
||||
@@ -4778,6 +4825,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))
|
||||
@@ -4786,7 +4859,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
|
||||
{
|
||||
if (talentOption.TalentIdentifiers.None(t => HasTalent(t)))
|
||||
if (talentOption.TalentIdentifiers.None(HasTalent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -4831,6 +4904,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>
|
||||
@@ -5043,6 +5129,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)
|
||||
|
||||
@@ -543,7 +543,7 @@ namespace Barotrauma
|
||||
|
||||
private void GetName(Rand.RandSync randSync, out string name)
|
||||
{
|
||||
var nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name");
|
||||
ContentXElement nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name");
|
||||
ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty;
|
||||
XElement namesXml = null;
|
||||
if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined
|
||||
@@ -554,8 +554,8 @@ namespace Barotrauma
|
||||
else //the legacy firstnames.txt/lastnames.txt shit is defined
|
||||
{
|
||||
namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]"));
|
||||
var firstNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
|
||||
var lastNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
|
||||
string firstNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
|
||||
string lastNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
|
||||
if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath))
|
||||
{
|
||||
var firstNames = File.ReadAllLines(firstNamesPath);
|
||||
@@ -735,9 +735,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 +811,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 +840,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 +851,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 +1165,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 +1189,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; }
|
||||
@@ -1314,7 +1353,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 +1401,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 +1764,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)
|
||||
@@ -1756,7 +1816,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 +1839,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 +1853,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)
|
||||
{
|
||||
|
||||
+4
@@ -384,6 +384,8 @@ namespace Barotrauma
|
||||
|
||||
private readonly ConstructorInfo constructor;
|
||||
|
||||
public readonly bool ResetBetweenRounds;
|
||||
|
||||
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
|
||||
{
|
||||
get
|
||||
@@ -465,6 +467,8 @@ namespace Barotrauma
|
||||
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
|
||||
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
|
||||
|
||||
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
|
||||
@@ -140,9 +140,20 @@ namespace Barotrauma
|
||||
private float vitality;
|
||||
public float Vitality
|
||||
{
|
||||
get
|
||||
{
|
||||
return Character.IsDead ? minVitality : vitality;
|
||||
get
|
||||
{
|
||||
if (Character.IsDead)
|
||||
{
|
||||
return minVitality;
|
||||
}
|
||||
|
||||
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
|
||||
{
|
||||
return Math.Max(vitality, MinVitality + 1);
|
||||
}
|
||||
|
||||
return vitality;
|
||||
|
||||
}
|
||||
private set
|
||||
{
|
||||
@@ -881,6 +892,9 @@ namespace Barotrauma
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
|
||||
decreaseSpeed *= (1f - oxygenlowResistance);
|
||||
increaseSpeed *= (1f + oxygenlowResistance);
|
||||
|
||||
float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier);
|
||||
decreaseSpeed *= holdBreathMultiplier;
|
||||
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
|
||||
}
|
||||
|
||||
@@ -1217,6 +1231,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))));
|
||||
|
||||
@@ -778,6 +778,7 @@ namespace Barotrauma
|
||||
{
|
||||
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
|
||||
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
|
||||
newAffliction = abilityAfflictionCharacter.Affliction;
|
||||
}
|
||||
if (applyAffliction)
|
||||
{
|
||||
@@ -896,6 +897,12 @@ namespace Barotrauma
|
||||
{
|
||||
reEnableTimer = duration;
|
||||
}
|
||||
#if CLIENT
|
||||
if (Hidden && LightSource != null)
|
||||
{
|
||||
LightSource.Enabled = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ReEnable()
|
||||
@@ -1194,7 +1201,25 @@ namespace Barotrauma
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -649,7 +649,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
|
||||
|
||||
+6
-6
@@ -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
|
||||
{
|
||||
@@ -34,6 +31,7 @@ namespace Barotrauma.Abilities
|
||||
Alive = 4,
|
||||
Monster = 5,
|
||||
InFriendlySubmarine = 6,
|
||||
Large = 7,
|
||||
};
|
||||
|
||||
protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings)
|
||||
@@ -41,8 +39,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 + ")");
|
||||
}
|
||||
@@ -83,6 +80,9 @@ namespace Barotrauma.Abilities
|
||||
return !targetCharacter.IsHuman;
|
||||
case TargetType.InFriendlySubmarine:
|
||||
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
|
||||
case TargetType.Large:
|
||||
// mass of mudraptor is ~48
|
||||
return targetCharacter.AnimController is { Mass: > 50.0f };
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
+11
-3
@@ -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)
|
||||
{
|
||||
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -13,6 +12,11 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
|
||||
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
|
||||
|
||||
if (!identifiers.Any() && !tags.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers or tags defined.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
|
||||
+8
@@ -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,19 @@ 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.Faction.Reputation.Value <= 0) { return false; }
|
||||
}
|
||||
|
||||
if (locationIdentifiers.Any())
|
||||
{
|
||||
if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; }
|
||||
|
||||
+30
-18
@@ -1,38 +1,50 @@
|
||||
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 && GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is { } highestFaction)
|
||||
{
|
||||
if (highestFaction.Reputation.Value < 0 || !mission.ReputationRewards.ContainsKey(highestFaction.Reputation.Identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return missionType.Contains(mission.Prefab.Type);
|
||||
}
|
||||
|
||||
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
|
||||
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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.IsUnconscious)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{
|
||||
var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier);
|
||||
if (affliction == null) { return false; }
|
||||
return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength;
|
||||
return affliction.Strength >= affliction.Prefab.ActivationThreshold && minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (tags.None())
|
||||
{
|
||||
return character.GetEquippedItem(null) is Item;
|
||||
return character.GetEquippedItem(null) != null;
|
||||
}
|
||||
|
||||
if (requireAll)
|
||||
|
||||
+43
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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)) { return false; }
|
||||
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance)
|
||||
{
|
||||
amountNeeded--;
|
||||
|
||||
if (amountNeeded <= 0) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
-1
@@ -15,5 +15,4 @@ namespace Barotrauma.Abilities
|
||||
}
|
||||
public Character Character { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ namespace Barotrauma.Abilities
|
||||
if (abilityObject is null)
|
||||
{
|
||||
ApplyEffect();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyEffect(abilityObject);
|
||||
|
||||
+35
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
-28
@@ -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();
|
||||
targets.AddRange(statusEffect.GetNearbyTargets(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()
|
||||
{
|
||||
|
||||
+27
-1
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -6,11 +7,15 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
private readonly bool allowSelf;
|
||||
private readonly float maxDistance = float.MaxValue;
|
||||
private readonly bool inSameRoom;
|
||||
private readonly ImmutableHashSet<Identifier> jobIdentifiers;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +24,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; }
|
||||
|
||||
+63
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -6,12 +6,15 @@ 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);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
@@ -19,7 +22,20 @@ 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 character in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
if (character == Character) { continue; }
|
||||
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
namespace Barotrauma.Abilities;
|
||||
|
||||
internal sealed class CharacterAbilityGiveExperience : CharacterAbility
|
||||
{
|
||||
public override bool AppliesEffectOnIntervalUpdate => true;
|
||||
|
||||
private readonly int amount;
|
||||
|
||||
public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
amount = abilityElement.GetAttributeInt("amount", 0);
|
||||
}
|
||||
|
||||
private void ApplyEffectSpecific(Character targetCharacter)
|
||||
{
|
||||
targetCharacter.Info?.GiveExperience(amount);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
|
||||
{
|
||||
ApplyEffectSpecific(targetCharacter);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyEffectSpecific(Character);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect()
|
||||
{
|
||||
ApplyEffectSpecific(Character);
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#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()
|
||||
{
|
||||
foreach (Item item in Character.Submarine.GetItems(true))
|
||||
{
|
||||
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
|
||||
{
|
||||
item.StatManager.ApplyStat(stat, value, CharacterTalent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-6
@@ -1,11 +1,17 @@
|
||||
using Barotrauma.Extensions;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
|
||||
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 +19,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 +27,7 @@ namespace Barotrauma.Abilities
|
||||
|
||||
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
|
||||
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
|
||||
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 +36,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 +59,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is not IAbilityCharacter { Character: { } character }) { return; }
|
||||
|
||||
character.MarkedAsLooted.Add(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-11
@@ -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
|
||||
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
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);
|
||||
}
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -13,21 +13,23 @@ namespace Barotrauma.Abilities
|
||||
|
||||
protected override void ApplyEffect()
|
||||
{
|
||||
if (!SelectedItemHasTag(Character)) { return; }
|
||||
if (!SelectedItemHasTag(Character, tag)) { return; }
|
||||
|
||||
Character closestCharacter = null;
|
||||
float closestDistance = squaredMaxDistance;
|
||||
|
||||
foreach (Character crewCharacter in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance)
|
||||
if (crewCharacter != Character &&
|
||||
Vector2.DistanceSquared(Character.WorldPosition, crewCharacter.WorldPosition) is float tempDistance && tempDistance < closestDistance &&
|
||||
SelectedItemHasTag(crewCharacter, tag))
|
||||
{
|
||||
closestCharacter = crewCharacter;
|
||||
closestDistance = tempDistance;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestCharacter == null || !SelectedItemHasTag(closestCharacter)) { return; }
|
||||
if (closestCharacter == null) { return; }
|
||||
|
||||
if (closestDistance < squaredMaxDistance)
|
||||
{
|
||||
@@ -35,7 +37,7 @@ namespace Barotrauma.Abilities
|
||||
ApplyEffectSpecific(closestCharacter);
|
||||
}
|
||||
|
||||
bool SelectedItemHasTag(Character character) =>
|
||||
static bool SelectedItemHasTag(Character character, string tag) =>
|
||||
(character.SelectedItem != null && character.SelectedItem.HasTag(tag)) ||
|
||||
(character.SelectedSecondaryItem != null && character.SelectedSecondaryItem.HasTag(tag));
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
internal sealed class CharacterAbilityUnlockApprenticeshipTalentTree : CharacterAbility
|
||||
{
|
||||
public CharacterAbilityUnlockApprenticeshipTalentTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
{
|
||||
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; }
|
||||
if (Character.GiveTalent(identifier))
|
||||
{
|
||||
Character.Info.AdditionalTalentPoints++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
ApplyEffect();
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-4
@@ -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,6 +41,9 @@ namespace Barotrauma.Abilities
|
||||
case "abilities":
|
||||
LoadAbilities(subElement);
|
||||
break;
|
||||
case "fallbackabilities":
|
||||
LoadFallbackAbilities(subElement);
|
||||
break;
|
||||
case "conditions":
|
||||
LoadConditions(subElement);
|
||||
break;
|
||||
@@ -47,10 +53,23 @@ namespace Barotrauma.Abilities
|
||||
|
||||
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 +104,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 +165,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);
|
||||
|
||||
+20
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-27
@@ -1,4 +1,7 @@
|
||||
namespace Barotrauma.Abilities
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
class CharacterAbilityGroupInterval : CharacterAbilityGroup
|
||||
{
|
||||
@@ -9,48 +12,71 @@
|
||||
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 shouldApplyDelayedEffect;
|
||||
bool conditionsDidntMatch;
|
||||
|
||||
if (AllConditionsMatched())
|
||||
{
|
||||
effectDelayTimer += TimeSinceLastUpdate;
|
||||
shouldApplyDelayedEffect = effectDelayTimer >= effectDelay;
|
||||
conditionsDidntMatch = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
effectDelayTimer = 0f;
|
||||
shouldApplyDelayedEffect = false;
|
||||
conditionsDidntMatch = true;
|
||||
}
|
||||
|
||||
bool hasFallbacks = fallbackAbilities.Count > 0;
|
||||
|
||||
List<CharacterAbility> abilitiesToRun =
|
||||
conditionsDidntMatch && hasFallbacks
|
||||
? fallbackAbilities
|
||||
: characterAbilities;
|
||||
|
||||
foreach (var characterAbility in abilitiesToRun)
|
||||
{
|
||||
if (!characterAbility.IsViable()) { continue; }
|
||||
|
||||
characterAbility.UpdateCharacterAbility(
|
||||
shouldApplyDelayedEffect || conditionsDidntMatch,
|
||||
TimeSinceLastUpdate);
|
||||
}
|
||||
|
||||
if (shouldApplyDelayedEffect || (conditionsDidntMatch && hasFallbacks))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework;
|
||||
#endif
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -14,6 +14,10 @@ namespace Barotrauma
|
||||
|
||||
public readonly Sprite Icon;
|
||||
|
||||
#if CLIENT
|
||||
public readonly Option<Color> ColorOverride;
|
||||
#endif
|
||||
|
||||
public static readonly PrefabCollection<TalentPrefab> TalentPrefabs = new PrefabCollection<TalentPrefab>();
|
||||
|
||||
public ContentXElement ConfigElement
|
||||
@@ -28,8 +32,22 @@ namespace Barotrauma
|
||||
|
||||
DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value);
|
||||
|
||||
Description = "";
|
||||
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
|
||||
if (!nameIdentifier.IsEmpty)
|
||||
{
|
||||
DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
|
||||
}
|
||||
|
||||
Description = string.Empty;
|
||||
|
||||
#if CLIENT
|
||||
Color colorOverride = element.GetAttributeColor("coloroverride", Color.TransparentBlack);
|
||||
|
||||
ColorOverride = colorOverride != Color.TransparentBlack
|
||||
? Option<Color>.Some(colorOverride)
|
||||
: Option<Color>.None();
|
||||
#endif
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class TalentTree : Prefab
|
||||
internal sealed class TalentTree : Prefab
|
||||
{
|
||||
public enum TalentTreeStageState
|
||||
{
|
||||
@@ -40,16 +40,17 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
List<TalentSubTree> subTrees = new List<TalentSubTree>();
|
||||
foreach (var subTreeElement in element.GetChildElements("subtree"))
|
||||
{
|
||||
subTrees.Add(new TalentSubTree(subTreeElement));
|
||||
}
|
||||
|
||||
TalentSubTrees = subTrees.ToImmutableArray();
|
||||
AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet();
|
||||
}
|
||||
|
||||
|
||||
public bool TalentIsInTree(Identifier talentIdentifier)
|
||||
{
|
||||
return AllTalentIdentifiers.Contains(talentIdentifier);
|
||||
@@ -57,29 +58,42 @@ namespace Barotrauma
|
||||
|
||||
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier)
|
||||
{
|
||||
return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection<Identifier>)Array.Empty<Identifier>());
|
||||
return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection<Identifier>)Array.Empty<Identifier>());
|
||||
}
|
||||
|
||||
public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection<Identifier> selectedTalents)
|
||||
{
|
||||
IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)),
|
||||
requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier));
|
||||
|
||||
return requiredSubTrees.All(tst => tst.IsCompleted(selectedTalents)) && // check if we meet requirements
|
||||
!blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents)); // check if any other talent trees are blocking this one
|
||||
}
|
||||
|
||||
// i hate this function - markus
|
||||
// me too - joonas
|
||||
public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List<Identifier> selectedTalents)
|
||||
public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection<Identifier> selectedTalents)
|
||||
{
|
||||
if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; }
|
||||
|
||||
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; }
|
||||
|
||||
TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
|
||||
TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
|
||||
if (subTree is null) { return TalentTreeStageState.Invalid; }
|
||||
|
||||
if (subTree == null) { return TalentTreeStageState.Invalid; }
|
||||
if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
|
||||
{
|
||||
return TalentTreeStageState.Locked;
|
||||
}
|
||||
|
||||
TalentOption targetTalentOption = subTree.TalentOptionStages[index];
|
||||
|
||||
if (targetTalentOption.TalentIdentifiers.Any(t => character.HasTalent(t)))
|
||||
if (targetTalentOption.HasEnoughTalents(character.Info))
|
||||
{
|
||||
return TalentTreeStageState.Unlocked;
|
||||
}
|
||||
|
||||
if (targetTalentOption.TalentIdentifiers.Any(t => selectedTalents.Contains(t)))
|
||||
if (targetTalentOption.HasSelectedTalent(selectedTalents))
|
||||
{
|
||||
return TalentTreeStageState.Highlighted;
|
||||
}
|
||||
@@ -91,8 +105,8 @@ namespace Barotrauma
|
||||
if (lastindex >= 0)
|
||||
{
|
||||
TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex];
|
||||
hasTalentInLastTier = lastLatentOption.TalentIdentifiers.Any(HasTalent);
|
||||
isLastTalentPurchased = lastLatentOption.TalentIdentifiers.Any(t => character.HasTalent(t));
|
||||
hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents);
|
||||
isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info);
|
||||
}
|
||||
|
||||
if (!hasTalentInLastTier)
|
||||
@@ -108,38 +122,29 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
return TalentTreeStageState.Locked;
|
||||
|
||||
bool HasTalent(Identifier talentId)
|
||||
{
|
||||
return selectedTalents.Contains(talentId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection<Identifier> selectedTalents)
|
||||
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
|
||||
{
|
||||
if (character?.Info?.Job.Prefab == null) { return false; }
|
||||
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; }
|
||||
|
||||
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
|
||||
|
||||
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
|
||||
|
||||
foreach (var subTree in talentTree.TalentSubTrees)
|
||||
foreach (var subTree in talentTree!.TalentSubTrees)
|
||||
{
|
||||
if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.TalentIdentifiers.Contains(talentIdentifier))) { return true; }
|
||||
|
||||
foreach (var talentOptionStage in subTree.TalentOptionStages)
|
||||
{
|
||||
bool hasTalentInThisTier = talentOptionStage.TalentIdentifiers.Any(t => selectedTalents.Contains(t));
|
||||
bool hasTalentInThisTier = talentOptionStage.HasEnoughTalents(selectedTalents);
|
||||
if (!hasTalentInThisTier)
|
||||
{
|
||||
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,60 +169,119 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return viableTalents;
|
||||
}
|
||||
|
||||
public override void Dispose() { }
|
||||
}
|
||||
|
||||
class TalentSubTree
|
||||
internal enum TalentTreeType
|
||||
{
|
||||
Specialization,
|
||||
Primary
|
||||
}
|
||||
|
||||
internal sealed class TalentSubTree
|
||||
{
|
||||
public Identifier Identifier { get; }
|
||||
|
||||
public LocalizedString DisplayName { get; }
|
||||
|
||||
public bool ForceUnlock;
|
||||
|
||||
public readonly ImmutableArray<TalentOption> TalentOptionStages;
|
||||
|
||||
public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
|
||||
|
||||
public readonly TalentTreeType Type;
|
||||
public readonly ImmutableHashSet<Identifier> RequiredTrees;
|
||||
public readonly ImmutableHashSet<Identifier> BlockedTrees;
|
||||
|
||||
public bool IsCompleted(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
|
||||
public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents));
|
||||
|
||||
public TalentSubTree(ContentXElement subTreeElement)
|
||||
{
|
||||
Identifier = subTreeElement.GetAttributeIdentifier("identifier", "");
|
||||
DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value);
|
||||
string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(nameIdentifier))
|
||||
{
|
||||
nameIdentifier = $"talenttree.{Identifier}";
|
||||
}
|
||||
DisplayName = TextManager.Get($"talenttree.{nameIdentifier}").Fallback(Identifier.Value);
|
||||
Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization);
|
||||
RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet<Identifier>.Empty);
|
||||
BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet<Identifier>.Empty);
|
||||
List<TalentOption> talentOptionStages = new List<TalentOption>();
|
||||
foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions"))
|
||||
{
|
||||
talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier));
|
||||
}
|
||||
|
||||
TalentOptionStages = talentOptionStages.ToImmutableArray();
|
||||
AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TalentOption
|
||||
internal readonly struct TalentOption
|
||||
{
|
||||
private readonly ImmutableHashSet<Identifier> talentIdentifiers;
|
||||
|
||||
public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
|
||||
|
||||
public bool HasTalent(Identifier talentIdentifier)
|
||||
public readonly int MaxChosenTalents;
|
||||
|
||||
/// <summary>
|
||||
/// When specified the talent option will show talent with this identifier
|
||||
/// and clicking on it will expand the talent option to show the talents
|
||||
/// </summary>
|
||||
public readonly Option<Identifier> ShowcaseTalent;
|
||||
|
||||
public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents;
|
||||
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
|
||||
|
||||
// No LINQ
|
||||
public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
|
||||
{
|
||||
return talentIdentifiers.Contains(talentIdentifier);
|
||||
foreach (Identifier talent in selectedTalents)
|
||||
{
|
||||
if (talentIdentifiers.Contains(talent))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int CountMatchingTalents(IReadOnlyCollection<Identifier> talents)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (Identifier talent in talents)
|
||||
{
|
||||
if (talentIdentifiers.Contains(talent))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
|
||||
{
|
||||
MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1);
|
||||
|
||||
Identifier showcaseTalent = talentOptionsElement.GetAttributeIdentifier("showcasetalent", Identifier.Empty);
|
||||
ShowcaseTalent = !showcaseTalent.IsEmpty
|
||||
? Option<Identifier>.Some(showcaseTalent)
|
||||
: Option<Identifier>.None();
|
||||
|
||||
var talentIdentifiers = new HashSet<Identifier>();
|
||||
foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption"))
|
||||
{
|
||||
Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
talentIdentifiers.Add(identifier);
|
||||
}
|
||||
|
||||
this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace Barotrauma
|
||||
{
|
||||
Option<ContentPackageId> ugcId = ContentPackageId.Parse(otherModName.Value);
|
||||
ContentPackage? otherMod =
|
||||
allPackages.FirstOrDefault(p => ugcId == p.UgcId)
|
||||
allPackages.FirstOrDefault(p => ugcId.IsSome() && ugcId == p.UgcId)
|
||||
?? allPackages.FirstOrDefault(p => p.Name == otherModName)
|
||||
?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName))
|
||||
?? throw new MissingContentPackageException(ContentPackage, otherModName.Value);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Xml.Linq;
|
||||
@@ -63,6 +65,8 @@ namespace Barotrauma
|
||||
public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def);
|
||||
public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def);
|
||||
public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
|
||||
[return:NotNullIfNotNull("def")]
|
||||
public ImmutableHashSet<Identifier>? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
|
||||
public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def);
|
||||
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
|
||||
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
|
||||
|
||||
@@ -121,6 +121,10 @@ namespace Barotrauma
|
||||
|
||||
public static bool operator !=(string str, in Identifier? identifier) =>
|
||||
!(identifier == str);
|
||||
|
||||
internal int IndexOf(char c) => Value.IndexOf(c);
|
||||
|
||||
internal Identifier this[Range range] => Value[range].ToIdentifier();
|
||||
}
|
||||
|
||||
public static class IdentifierExtensions
|
||||
|
||||
@@ -1334,7 +1334,7 @@ namespace Barotrauma
|
||||
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
|
||||
if (!string.IsNullOrWhiteSpace(prefabIdentifier) && prefab.Identifier != prefabIdentifier) { continue; }
|
||||
|
||||
int targetLevel = prefab.MaxLevel - upgradeManager.GetRealUpgradeLevel(prefab, category);
|
||||
int targetLevel = prefab.GetMaxLevelForCurrentSub() - upgradeManager.GetRealUpgradeLevel(prefab, category);
|
||||
for (int i = 0; i < targetLevel; i++)
|
||||
{
|
||||
upgradeManager.PurchaseUpgrade(prefab, category, force: true);
|
||||
@@ -1750,6 +1750,17 @@ namespace Barotrauma
|
||||
NewMessage("Set minimum loading time to " + time + " seconds.", Color.White);
|
||||
}));
|
||||
|
||||
|
||||
commands.Add(new Command("resetcharacternetstate", "resetcharacternetstate [character name]: A debug-only command that resets a character's network state, intended for diagnosing character syncing issues.", null,
|
||||
() =>
|
||||
{
|
||||
if (GameMain.NetworkMember == null) { return null; }
|
||||
return new string[][]
|
||||
{
|
||||
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
|
||||
};
|
||||
}));
|
||||
|
||||
commands.Add(new Command("storeinfo", "", (string[] args) =>
|
||||
{
|
||||
if (GameMain.GameSession?.Map?.CurrentLocation is Location location)
|
||||
@@ -1803,6 +1814,7 @@ namespace Barotrauma
|
||||
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
|
||||
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
|
||||
commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true));
|
||||
commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false));
|
||||
commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false));
|
||||
commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null));
|
||||
commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null));
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace Barotrauma
|
||||
OnRepairComplete,
|
||||
OnItemFabricationSkillGain,
|
||||
OnItemFabricatedAmount,
|
||||
OnItemFabricatedIngredients,
|
||||
OnAllyItemFabricatedAmount,
|
||||
OnOpenItemContainer,
|
||||
OnUseRangedWeapon,
|
||||
@@ -51,6 +52,7 @@ namespace Barotrauma
|
||||
OnSelfRagdoll,
|
||||
OnRagdoll,
|
||||
OnRoundEnd,
|
||||
OnLootCharacter,
|
||||
OnAnyMissionCompleted,
|
||||
OnAllMissionsCompleted,
|
||||
OnGiveOrder,
|
||||
@@ -80,6 +82,11 @@ namespace Barotrauma
|
||||
// Skills
|
||||
ElectricalSkillBonus,
|
||||
HelmSkillBonus,
|
||||
HelmSkillOverride,
|
||||
MedicalSkillOverride,
|
||||
WeaponsSkillOverride,
|
||||
ElectricalSkillOverride,
|
||||
MechanicalSkillOverride,
|
||||
MechanicalSkillBonus,
|
||||
MedicalSkillBonus,
|
||||
WeaponsSkillBonus,
|
||||
@@ -105,6 +112,7 @@ namespace Barotrauma
|
||||
RangedSpreadReduction,
|
||||
// Utility
|
||||
RepairSpeed,
|
||||
MechanicalRepairSpeed,
|
||||
DeconstructorSpeedMultiplier,
|
||||
RepairToolStructureRepairMultiplier,
|
||||
RepairToolStructureDamageMultiplier,
|
||||
@@ -115,20 +123,53 @@ namespace Barotrauma
|
||||
GeneticMaterialRefineBonus,
|
||||
GeneticMaterialTaintedProbabilityReductionOnCombine,
|
||||
SkillGainSpeed,
|
||||
ExtraLevelGain,
|
||||
HelmSkillGainSpeed,
|
||||
WeaponsSkillGainSpeed,
|
||||
MedicalSkillGainSpeed,
|
||||
ElectricalSkillGainSpeed,
|
||||
MechanicalSkillGainSpeed,
|
||||
MedicalItemApplyingMultiplier,
|
||||
MedicalItemDurationMultiplier,
|
||||
PoisonMultiplier,
|
||||
// Tinker
|
||||
TinkeringDuration,
|
||||
TinkeringStrength,
|
||||
TinkeringDamage,
|
||||
// Misc
|
||||
ReputationGainMultiplier,
|
||||
ReputationLossMultiplier,
|
||||
MissionMoneyGainMultiplier,
|
||||
ExperienceGainMultiplier,
|
||||
MissionExperienceGainMultiplier,
|
||||
ExtraMissionCount,
|
||||
ExtraSpecialSalesCount,
|
||||
ApplyTreatmentsOnSelfFraction,
|
||||
StoreSellMultiplier,
|
||||
StoreBuyMultiplierAffiliated,
|
||||
StoreBuyMultiplier,
|
||||
MaxAttachableCount,
|
||||
ExplosionRadiusMultiplier,
|
||||
ExplosionDamageMultiplier,
|
||||
FabricateMedicineSpeedMultiplier,
|
||||
BallastFloraDamageMultiplier,
|
||||
HoldBreathMultiplier,
|
||||
Apprenticeship,
|
||||
CPRBoost
|
||||
}
|
||||
|
||||
internal enum ItemTalentStats
|
||||
{
|
||||
None,
|
||||
DetoriationSpeed,
|
||||
BatteryCapacity,
|
||||
EngineSpeed,
|
||||
EngineMaxSpeed,
|
||||
PumpSpeed,
|
||||
PumpMaxFlow,
|
||||
ReactorMaxOutput,
|
||||
ReactorFuelEfficiency,
|
||||
DeconstructorSpeed,
|
||||
FabricationSpeed
|
||||
}
|
||||
|
||||
[Flags]
|
||||
@@ -145,8 +186,8 @@ namespace Barotrauma
|
||||
GainSkillPastMaximum = 0x80,
|
||||
RetainExperienceForNewCharacter = 0x100,
|
||||
AllowSecondOrderedTarget = 0x200,
|
||||
PowerfulCPR = 0x400,
|
||||
AlwaysStayConscious = 0x800,
|
||||
AlwaysStayConscious = 0x400,
|
||||
CanNotDieToAfflictions = 0x800,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -21,7 +22,14 @@ namespace Barotrauma
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast<CharacterTeamType>();
|
||||
if (!enums.Contains(TeamTag))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}.");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Character> affectedNpcs = null;
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ namespace Barotrauma
|
||||
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
|
||||
if (LootingIsStealing)
|
||||
{
|
||||
foreach (Item item in newCharacter.Inventory.AllItems)
|
||||
foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
|
||||
{
|
||||
item.SpawnedInCurrentOutpost = true;
|
||||
item.AllowStealing = false;
|
||||
@@ -257,21 +257,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (!SpawnPointTag.IsEmpty)
|
||||
{
|
||||
List<Item> potentialItems = SpawnLocation switch
|
||||
{
|
||||
SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub),
|
||||
SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null),
|
||||
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost),
|
||||
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck),
|
||||
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin),
|
||||
SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
List<Item> potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine));
|
||||
var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced();
|
||||
if (item != null) { return item; }
|
||||
|
||||
var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced();
|
||||
var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced();
|
||||
if (target != null) { return target; }
|
||||
}
|
||||
|
||||
@@ -281,19 +271,25 @@ namespace Barotrauma
|
||||
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag);
|
||||
}
|
||||
|
||||
private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
|
||||
{
|
||||
return spawnLocation switch
|
||||
{
|
||||
SpawnLocationType.MainSub => submarine == Submarine.MainSub,
|
||||
SpawnLocationType.MainPath => submarine == null,
|
||||
SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } },
|
||||
SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } },
|
||||
SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } },
|
||||
SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
|
||||
{
|
||||
List<WayPoint> potentialSpawnPoints = spawnLocation switch
|
||||
{
|
||||
SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null),
|
||||
SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null),
|
||||
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
|
||||
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck),
|
||||
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin),
|
||||
SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
|
||||
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
|
||||
|
||||
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
|
||||
|
||||
if (moduleFlags != null && moduleFlags.Any())
|
||||
|
||||
@@ -273,13 +273,13 @@ namespace Barotrauma
|
||||
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
|
||||
ResetTime = element.GetAttributeFloat("resettime", 0);
|
||||
|
||||
DefaultCommonness = 1.0f;
|
||||
DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "commonness":
|
||||
DefaultCommonness = subElement.GetAttributeFloat("commonness", 0.0f);
|
||||
DefaultCommonness = subElement.GetAttributeFloat("commonness", DefaultCommonness);
|
||||
foreach (XElement overrideElement in subElement.Elements())
|
||||
{
|
||||
if (overrideElement.NameAsIdentifier() == "override")
|
||||
|
||||
@@ -163,6 +163,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!subs.Contains(item.Submarine)) { continue; }
|
||||
if (item.GetRootInventoryOwner() is Character) { continue; }
|
||||
if (item.NonInteractable) { continue; }
|
||||
containers.AddRange(item.GetComponents<ItemContainer>());
|
||||
}
|
||||
containers.Shuffle(Rand.RandSync.ServerAndClient);
|
||||
|
||||
@@ -158,6 +158,16 @@ namespace Barotrauma
|
||||
this.campaign = campaign;
|
||||
}
|
||||
|
||||
public static bool HasUnlockedStoreItem(ItemPrefab prefab)
|
||||
{
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
if (character.HasStoreAccessForItem(prefab)) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<T> GetItems<T>(Identifier identifier, Dictionary<Identifier, List<T>> items, bool create = false)
|
||||
{
|
||||
if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -14,6 +15,13 @@ namespace Barotrauma
|
||||
Prefab = prefab;
|
||||
Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
|
||||
}
|
||||
|
||||
public bool IsAffiliated()
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is not { } highestFaction) { return false; }
|
||||
|
||||
return highestFaction.Reputation.Value < 0 || Prefab.Identifier == highestFaction.Prefab.Identifier;
|
||||
}
|
||||
}
|
||||
|
||||
internal class FactionPrefab : Prefab
|
||||
|
||||
@@ -70,6 +70,15 @@ namespace Barotrauma
|
||||
}
|
||||
reputationChange *= reputationGainMultiplier;
|
||||
}
|
||||
else if (reputationChange < 0f)
|
||||
{
|
||||
float reputationLossMultiplier = 1f;
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
reputationLossMultiplier += character.GetStatValue(StatTypes.ReputationLossMultiplier);
|
||||
}
|
||||
reputationChange *= reputationLossMultiplier;
|
||||
}
|
||||
Value += reputationChange;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,15 @@ namespace Barotrauma
|
||||
public virtual bool PurchasedLostShuttles { get; set; }
|
||||
public virtual bool PurchasedItemRepairs { get; set; }
|
||||
|
||||
private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions)
|
||||
{
|
||||
if (GameMain.NetworkMember == null) { return true; }
|
||||
//allow managing if no-one with permissions is alive
|
||||
return
|
||||
GameMain.NetworkMember.ConnectedClients.Count == 1 ||
|
||||
GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions)));
|
||||
}
|
||||
|
||||
protected CampaignMode(GameModePreset preset, CampaignSettings settings)
|
||||
: base(preset)
|
||||
{
|
||||
@@ -156,7 +165,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!(e.ChangedData.BalanceChanged is Some<int> { Value: var changed })) { return; }
|
||||
|
||||
if (changed != 0) { return; }
|
||||
if (changed == 0) { return; }
|
||||
|
||||
bool isGain = changed > 0;
|
||||
Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red;
|
||||
@@ -211,7 +220,7 @@ namespace Barotrauma
|
||||
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
|
||||
}
|
||||
|
||||
public List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
|
||||
public static List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
|
||||
{
|
||||
//leave subs behind if they're not docked to the leaving sub and not at the same exit
|
||||
return Submarine.Loaded.FindAll(sub =>
|
||||
@@ -266,7 +275,7 @@ namespace Barotrauma
|
||||
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
|
||||
}
|
||||
|
||||
public int GetHullRepairCost()
|
||||
public static int GetHullRepairCost()
|
||||
{
|
||||
float totalDamage = 0;
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
@@ -283,7 +292,7 @@ namespace Barotrauma
|
||||
return (int)Math.Min(totalDamage * HullRepairCostPerDamage, MaxHullRepairCost);
|
||||
}
|
||||
|
||||
public int GetItemRepairCost()
|
||||
public static int GetItemRepairCost()
|
||||
{
|
||||
float totalRepairDuration = 0.0f;
|
||||
foreach (Item item in Item.ItemList)
|
||||
@@ -551,7 +560,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Which submarine is at a position where it can leave the level and enter another one (if any).
|
||||
/// </summary>
|
||||
private Submarine GetLeavingSub()
|
||||
private static Submarine GetLeavingSub()
|
||||
{
|
||||
if (Level.IsLoadedOutpost)
|
||||
{
|
||||
@@ -1025,7 +1034,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected void LeaveUnconnectedSubs(Submarine leavingSub)
|
||||
protected static void LeaveUnconnectedSubs(Submarine leavingSub)
|
||||
{
|
||||
if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -103,12 +104,7 @@ namespace Barotrauma
|
||||
|
||||
private static int GetAddedMissionCount()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
|
||||
}
|
||||
return count;
|
||||
return GameSession.GetSessionCrewCharacters(CharacterType.Both).Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -756,7 +756,7 @@ namespace Barotrauma
|
||||
/// </remarks>
|
||||
public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
|
||||
{
|
||||
if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet<Character>.Empty; }
|
||||
if (GameMain.GameSession.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
|
||||
|
||||
IEnumerable<Character> players;
|
||||
IEnumerable<Character> bots;
|
||||
@@ -766,8 +766,8 @@ namespace Barotrauma
|
||||
players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
|
||||
bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
|
||||
#elif CLIENT
|
||||
players = crewManager.GetCharacters().Where(c => c.IsPlayer);
|
||||
bots = crewManager.GetCharacters().Where(c => c.IsBot);
|
||||
players = crewManager.GetCharacters().Where(static c => c.IsPlayer);
|
||||
bots = crewManager.GetCharacters().Where(static c => c.IsBot);
|
||||
#endif
|
||||
if (type.HasFlag(CharacterType.Bot))
|
||||
{
|
||||
|
||||
@@ -177,12 +177,13 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
int price = prefab.Price.GetBuyprice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
|
||||
int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
|
||||
int currentLevel = GetUpgradeLevel(prefab, category);
|
||||
|
||||
if (currentLevel + 1 > prefab.MaxLevel)
|
||||
int maxLevel = prefab.GetMaxLevelForCurrentSub();
|
||||
if (currentLevel + 1 > maxLevel)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {prefab.MaxLevel}). The transaction has been cancelled.");
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,7 +207,7 @@ namespace Barotrauma
|
||||
price = 0;
|
||||
}
|
||||
|
||||
if (Campaign.TryPurchase(client, price))
|
||||
if (force || Campaign.TryPurchase(client, price))
|
||||
{
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
@@ -472,7 +473,7 @@ namespace Barotrauma
|
||||
{
|
||||
int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level);
|
||||
DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})");
|
||||
SetUpgradeLevel(prefab, category, Math.Clamp(GetRealUpgradeLevel(prefab, category) + level, 0, prefab.MaxLevel));
|
||||
SetUpgradeLevel(prefab, category, GetRealUpgradeLevel(prefab, category) + level);
|
||||
}
|
||||
|
||||
PendingUpgrades.Clear();
|
||||
@@ -652,16 +653,13 @@ namespace Barotrauma
|
||||
|
||||
/// <summary>
|
||||
/// Gets the progress that is shown on the store interface.
|
||||
/// Includes values stored in the metadata and <see cref="PendingUpgrades"/>
|
||||
/// Includes values stored in the metadata and <see cref="PendingUpgrades"/>, and takes submarine tier and class restrictions into account
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
|
||||
{
|
||||
if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); }
|
||||
|
||||
return GetRealUpgradeLevel(prefab, category) + GetPendingLevel();
|
||||
return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), prefab.GetMaxLevelForCurrentSub());
|
||||
|
||||
int GetPendingLevel()
|
||||
{
|
||||
@@ -671,11 +669,8 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level of the upgrade that is stored in the metadata.
|
||||
/// Gets the level of the upgrade that is stored in the metadata. May be higher than the apparent level on the current sub if the player has switched to a lower-tier sub
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
public int GetRealUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
|
||||
{
|
||||
return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
|
||||
@@ -684,9 +679,6 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Stores the target upgrade level in the campaign metadata.
|
||||
/// </summary>
|
||||
/// <param name="prefab"></param>
|
||||
/// <param name="category"></param>
|
||||
/// <param name="level"></param>
|
||||
private void SetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, int level)
|
||||
{
|
||||
Metadata.SetValue(FormatIdentifier(prefab, category), level);
|
||||
|
||||
@@ -1164,11 +1164,14 @@ namespace Barotrauma.Items.Components
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient &&
|
||||
!(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(ClientPermissions.ManageMap) ?? false))
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (GameMain.GameSession?.Campaign != null && !CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap))
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (dockingCooldown > 0.0f) { return; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -63,6 +64,9 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
public float RaycastRange { get; set; }
|
||||
|
||||
[Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)]
|
||||
public float Duration
|
||||
{
|
||||
@@ -70,6 +74,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.25f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)]
|
||||
public float Reload
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable]
|
||||
public bool OutdoorsOnly
|
||||
{
|
||||
@@ -77,6 +88,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes)]
|
||||
public bool IgnoreUser
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private readonly List<Node> nodes = new List<Node>();
|
||||
public IEnumerable<Node> Nodes
|
||||
{
|
||||
@@ -91,6 +109,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private readonly Attack attack;
|
||||
|
||||
private Character user;
|
||||
|
||||
private float reloadTimer;
|
||||
|
||||
public ElectricalDischarger(Item item, ContentXElement element) :
|
||||
base(item, element)
|
||||
{
|
||||
@@ -125,6 +147,7 @@ namespace Barotrauma.Items.Components
|
||||
charging = true;
|
||||
timer = Duration;
|
||||
IsActive = true;
|
||||
user = character;
|
||||
#if SERVER
|
||||
if (GameMain.Server != null) { item.CreateServerEvent(this); }
|
||||
#endif
|
||||
@@ -144,6 +167,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (timer <= 0.0f)
|
||||
{
|
||||
if (reloadTimer > 0.0f)
|
||||
{
|
||||
reloadTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
@@ -196,6 +224,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void Discharge()
|
||||
{
|
||||
reloadTimer = Reload;
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f);
|
||||
FindNodes(item.WorldPosition, Range);
|
||||
if (attack != null)
|
||||
@@ -203,7 +232,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach ((Character character, Node node) in charactersInRange)
|
||||
{
|
||||
if (character == null || character.Removed) { continue; }
|
||||
character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor));
|
||||
character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor));
|
||||
}
|
||||
}
|
||||
DischargeProjSpecific();
|
||||
@@ -214,6 +243,18 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public void FindNodes(Vector2 worldPosition, float range)
|
||||
{
|
||||
if (RaycastRange > 0.0f)
|
||||
{
|
||||
float angle = 0.0f;
|
||||
float dir = 1;
|
||||
if (item.body != null)
|
||||
{
|
||||
angle += item.body.Rotation;
|
||||
dir = item.body.Dir;
|
||||
}
|
||||
worldPosition += new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * RaycastRange * dir;
|
||||
}
|
||||
|
||||
//see which submarines are within range so we can skip structures that are in far-away subs
|
||||
List<Submarine> submarinesInRange = new List<Submarine>();
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
@@ -222,7 +263,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
submarinesInRange.Add(sub);
|
||||
}
|
||||
else
|
||||
else if (sub != null)
|
||||
{
|
||||
Rectangle subBorders = new Rectangle(
|
||||
sub.Borders.X - (int)range, sub.Borders.Y + (int)range,
|
||||
@@ -263,26 +304,41 @@ namespace Barotrauma.Items.Components
|
||||
entitiesInRange.Add(structure);
|
||||
}
|
||||
|
||||
|
||||
nodes.Clear();
|
||||
if (RaycastRange > 0.0f)
|
||||
{
|
||||
nodes.Add(new Node(item.WorldPosition, -1));
|
||||
int parentNodeIndex = 0;
|
||||
AddNodesBetweenPoints(item.WorldPosition, worldPosition, 0.5f, ref parentNodeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
nodes.Add(new Node(worldPosition, -1));
|
||||
}
|
||||
|
||||
float totalRange = RaycastRange + range;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (!character.Enabled) continue;
|
||||
if (OutdoorsOnly && character.Submarine != null) continue;
|
||||
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue;
|
||||
if (!character.Enabled) { continue; }
|
||||
if (IgnoreUser && character == user) { continue; }
|
||||
if (OutdoorsOnly && character.Submarine != null) { continue; }
|
||||
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; }
|
||||
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls)
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls ||
|
||||
(RaycastRange > 0.0f && MathUtils.LineToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition) < range * range * RangeMultiplierInWalls))
|
||||
{
|
||||
entitiesInRange.Add(character);
|
||||
charactersInRange.Add((character, nodes[0]));
|
||||
}
|
||||
}
|
||||
|
||||
nodes.Clear();
|
||||
nodes.Add(new Node(worldPosition, -1));
|
||||
FindNodes(entitiesInRange, worldPosition, 0, range);
|
||||
FindNodes(entitiesInRange, worldPosition, nodes.Count - 1, range);
|
||||
|
||||
//construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge)
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (nodes[i].ParentIndex < 0) continue;
|
||||
if (nodes[i].ParentIndex < 0) { continue; }
|
||||
Node parentNode = nodes[nodes[i].ParentIndex];
|
||||
float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f);
|
||||
float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition);
|
||||
@@ -292,7 +348,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void FindNodes(List<Entity> entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange)
|
||||
{
|
||||
if (currentRange <= 0.0f || nodes.Count >= MaxNodes) return;
|
||||
if (currentRange <= 0.0f || nodes.Count >= MaxNodes) { return; }
|
||||
|
||||
//find the closest structure
|
||||
int closestIndex = -1;
|
||||
@@ -434,20 +490,21 @@ namespace Barotrauma.Items.Components
|
||||
for (int j = 0; j < entitiesInRange.Count; j++)
|
||||
{
|
||||
var otherEntity = entitiesInRange[j];
|
||||
if (!(otherEntity is Character character)) continue;
|
||||
if (OutdoorsOnly && character.Submarine != null) continue;
|
||||
if (otherEntity is not Character character) { continue; }
|
||||
if (IgnoreUser && character == user) { continue; }
|
||||
if (OutdoorsOnly && character.Submarine != null) { continue; }
|
||||
|
||||
if (targetStructure.IsHorizontal)
|
||||
{
|
||||
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue;
|
||||
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue;
|
||||
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue;
|
||||
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) { continue; }
|
||||
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) { continue; }
|
||||
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue;
|
||||
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue;
|
||||
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue;
|
||||
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; }
|
||||
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) { continue; }
|
||||
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) { continue; }
|
||||
}
|
||||
float closestNodeDistSqr = float.MaxValue;
|
||||
int closestNodeIndex = -1;
|
||||
@@ -473,7 +530,10 @@ namespace Barotrauma.Items.Components
|
||||
AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex);
|
||||
nodes.Add(new Node(targetPos, parentNodeIndex));
|
||||
entitiesInRange.RemoveAt(closestIndex);
|
||||
charactersInRange.Add((character, nodes[parentNodeIndex]));
|
||||
if (!charactersInRange.Any(c => c.character == character))
|
||||
{
|
||||
charactersInRange.Add((character, nodes[parentNodeIndex]));
|
||||
}
|
||||
FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange);
|
||||
}
|
||||
}
|
||||
@@ -483,7 +543,7 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 diff = targetPos - currPos;
|
||||
float dist = diff.Length();
|
||||
Vector2 normal = new Vector2(-diff.Y, diff.X) / dist;
|
||||
for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.5f))
|
||||
for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.0f))
|
||||
{
|
||||
//0 at the edges, 1 at the center
|
||||
float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f;
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
const float MaxAttachDistance = 150.0f;
|
||||
private const float MaxAttachDistance = ItemPrefab.DefaultInteractDistance * 0.95f;
|
||||
|
||||
//the position(s) in the item that the Character grabs
|
||||
protected Vector2[] handlePos;
|
||||
@@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components
|
||||
set { attachedByDefault = value; }
|
||||
}
|
||||
|
||||
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
|
||||
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
|
||||
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
|
||||
public Vector2 HoldPos
|
||||
{
|
||||
@@ -143,7 +143,11 @@ namespace Barotrauma.Items.Components
|
||||
set { aimPos = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
|
||||
#else
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
#endif
|
||||
public float HoldAngle
|
||||
{
|
||||
get { return MathHelper.ToDegrees(holdAngle); }
|
||||
@@ -151,23 +155,50 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private Vector2 swingAmount;
|
||||
#if DEBUG
|
||||
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
|
||||
#else
|
||||
[Serialize("0.0,0.0", IsPropertySaveable.No)]
|
||||
#endif
|
||||
public Vector2 SwingAmount
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(swingAmount); }
|
||||
set { swingAmount = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")]
|
||||
#else
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
#endif
|
||||
|
||||
public float SwingSpeed { get; set; }
|
||||
|
||||
#if DEBUG
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")]
|
||||
#else
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
#endif
|
||||
public bool SwingWhenHolding { get; set; }
|
||||
|
||||
#if DEBUG
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")]
|
||||
#else
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
#endif
|
||||
public bool SwingWhenAiming { get; set; }
|
||||
|
||||
#if DEBUG
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")]
|
||||
#else
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
#endif
|
||||
public bool SwingWhenUsing { get; set; }
|
||||
|
||||
#if DEBUG
|
||||
[Editable, Serialize(false, IsPropertySaveable.No)]
|
||||
#else
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
#endif
|
||||
public bool DisableHeadRotation { get; set; }
|
||||
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")]
|
||||
@@ -731,10 +762,24 @@ namespace Barotrauma.Items.Components
|
||||
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
|
||||
|
||||
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
|
||||
|
||||
Vector2 attachPos = userPos + mouseDiff;
|
||||
|
||||
if (user.Submarine == null && Level.Loaded != null)
|
||||
if (user.Submarine != null)
|
||||
{
|
||||
if (Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(user.Position),
|
||||
ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
|
||||
{
|
||||
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction;
|
||||
|
||||
//round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
|
||||
return
|
||||
new Vector2(
|
||||
mouseDiff.X > 0 ? (float)Math.Floor(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X,
|
||||
mouseDiff.Y > 0 ? (float)Math.Floor(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y);
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded != null)
|
||||
{
|
||||
bool edgeFound = false;
|
||||
foreach (var cell in Level.Loaded.GetCells(attachPos))
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components
|
||||
reloadTimer = reload;
|
||||
reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed);
|
||||
reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier);
|
||||
character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer;
|
||||
character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f;
|
||||
|
||||
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
|
||||
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
|
||||
@@ -421,7 +421,7 @@ namespace Barotrauma.Items.Components
|
||||
if (targetItem.Removed) { return; }
|
||||
var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
|
||||
#if CLIENT
|
||||
if (attackResult.Damage > 0.0f)
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(targetItem,
|
||||
targetItem.WorldPosition,
|
||||
|
||||
@@ -514,7 +514,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime)
|
||||
if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime && item.CurrentHull != null)
|
||||
{
|
||||
Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f);
|
||||
if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
|
||||
@@ -636,11 +636,14 @@ namespace Barotrauma.Items.Components
|
||||
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
|
||||
levelResource.DeattachTimer += addedDetachTime;
|
||||
#if CLIENT
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
targetItem.WorldPosition,
|
||||
levelResource.DeattachTimer / levelResource.DeattachDuration,
|
||||
GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching");
|
||||
if (targetItem.Prefab.ShowHealthBar)
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
targetItem.WorldPosition,
|
||||
levelResource.DeattachTimer / levelResource.DeattachDuration,
|
||||
GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching");
|
||||
}
|
||||
#endif
|
||||
FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false);
|
||||
return true;
|
||||
|
||||
@@ -111,6 +111,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool drawable = true;
|
||||
|
||||
[Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)]
|
||||
public PropertyConditional.Comparison IsActiveConditionalComparison
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public List<PropertyConditional> IsActiveConditionals;
|
||||
|
||||
public bool Drawable
|
||||
@@ -241,6 +248,18 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public int ManuallySelectedSound { get; private set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects or conditionals to the speed of the item
|
||||
/// </summary>
|
||||
public float Speed
|
||||
{
|
||||
get
|
||||
{
|
||||
return item.Speed;
|
||||
}
|
||||
}
|
||||
|
||||
public ItemComponent(Item item, ContentXElement element)
|
||||
{
|
||||
this.item = item;
|
||||
@@ -814,7 +833,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f, float applyOnUserFraction = 0.0f)
|
||||
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f)
|
||||
{
|
||||
if (statusEffectLists == null) { return; }
|
||||
|
||||
@@ -828,11 +847,6 @@ namespace Barotrauma.Items.Components
|
||||
if (user != null) { effect.SetUser(user); }
|
||||
effect.AfflictionMultiplier = afflictionMultiplier;
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
|
||||
if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.AfflictionMultiplier = applyOnUserFraction;
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition);
|
||||
}
|
||||
effect.AfflictionMultiplier = 1.0f;
|
||||
reducesCondition |= effect.ReducesItemCondition();
|
||||
}
|
||||
|
||||
@@ -104,12 +104,14 @@ namespace Barotrauma.Items.Components
|
||||
// doesn't quite work properly, remaining time changes if tinkering stops
|
||||
float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease);
|
||||
|
||||
float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
|
||||
|
||||
if (DeconstructItemsSimultaneously)
|
||||
{
|
||||
float deconstructTime = 0.0f;
|
||||
foreach (Item targetItem in inputContainer.Inventory.AllItems)
|
||||
{
|
||||
deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier);
|
||||
deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier);
|
||||
}
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
@@ -139,7 +141,7 @@ namespace Barotrauma.Items.Components
|
||||
if (targetItem == null) { return; }
|
||||
|
||||
var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList();
|
||||
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
|
||||
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
if (progressTimer > deconstructTime)
|
||||
@@ -218,7 +220,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (percentageHealth < deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; }
|
||||
|
||||
if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab))
|
||||
if (MapEntityPrefab.FindByIdentifier(deconstructProduct.ItemIdentifier) is not ItemPrefab itemPrefab)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!");
|
||||
return;
|
||||
@@ -284,9 +286,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
spawnedItem.SpawnedInCurrentOutpost = item.SpawnedInCurrentOutpost;
|
||||
spawnedItem.StolenDuringRound = targetItem.StolenDuringRound;
|
||||
spawnedItem.AllowStealing = targetItem.AllowStealing;
|
||||
spawnedItem.OriginalOutpost = targetItem.OriginalOutpost;
|
||||
spawnedItem.SpawnedInCurrentOutpost = targetItem.SpawnedInCurrentOutpost;
|
||||
for (int i = 0; i < outputContainer.Capacity; i++)
|
||||
{
|
||||
var containedItem = outputContainer.Inventory.GetItemAt(i);
|
||||
|
||||
@@ -30,11 +30,8 @@ namespace Barotrauma.Items.Components
|
||||
Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
|
||||
public float MaxForce
|
||||
{
|
||||
get { return maxForce; }
|
||||
set
|
||||
{
|
||||
maxForce = Math.Max(0.0f, value);
|
||||
}
|
||||
get => maxForce;
|
||||
set => maxForce = Math.Max(0.0f, value);
|
||||
}
|
||||
|
||||
[Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes,
|
||||
@@ -94,7 +91,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
@@ -129,12 +126,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
|
||||
}
|
||||
currForce *= maxForce * forceMultiplier;
|
||||
if (item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering)
|
||||
currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
|
||||
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
|
||||
{
|
||||
currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease;
|
||||
}
|
||||
|
||||
currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce);
|
||||
|
||||
//less effective when in a bad condition
|
||||
currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
|
||||
if (item.Submarine.FlippedX) { currForce *= -1; }
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fabricationRecipes = new Dictionary<uint, FabricationRecipe>();
|
||||
@@ -104,6 +104,18 @@ namespace Barotrauma.Items.Components
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool recipeInvalid = false;
|
||||
foreach (var requiredItem in recipe.RequiredItems)
|
||||
{
|
||||
if (requiredItem.ItemPrefabs.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".");
|
||||
recipeInvalid = true;
|
||||
}
|
||||
}
|
||||
if (recipeInvalid) { continue; }
|
||||
|
||||
fabricationRecipes.Add(recipe.RecipeHash, recipe);
|
||||
if (recipe.FabricationLimitMax >= 0)
|
||||
{
|
||||
@@ -356,9 +368,10 @@ namespace Barotrauma.Items.Components
|
||||
bool ingredientsStolen = false;
|
||||
bool ingredientsAllowStealing = true;
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
fabricatedItem.RequiredItems.ForEach(requiredItem =>
|
||||
List<Item> foundAvailableItems = new List<Item>();
|
||||
foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
|
||||
{
|
||||
for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++)
|
||||
{
|
||||
@@ -367,10 +380,7 @@ namespace Barotrauma.Items.Components
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
|
||||
var availableItems = availableIngredients[requiredPrefab.Identifier];
|
||||
var availableItem = availableItems.FirstOrDefault(potentialPrefab =>
|
||||
{
|
||||
return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage);
|
||||
});
|
||||
var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage));
|
||||
|
||||
if (availableItem == null) { continue; }
|
||||
|
||||
@@ -401,13 +411,21 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
foundAvailableItems.Add(availableItem);
|
||||
availableItems.Remove(availableItem);
|
||||
Entity.Spawner.AddItemToRemoveQueue(availableItem);
|
||||
inputContainer.Inventory.RemoveItem(availableItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems);
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
|
||||
|
||||
foreach (Item availableItem in fabricationIngredients.Items)
|
||||
{
|
||||
Entity.Spawner.AddItemToRemoveQueue(availableItem);
|
||||
inputContainer.Inventory.RemoveItem(availableItem);
|
||||
}
|
||||
|
||||
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
|
||||
|
||||
@@ -535,12 +553,13 @@ namespace Barotrauma.Items.Components
|
||||
return currPowerConsumption;
|
||||
}
|
||||
|
||||
private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
|
||||
private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
|
||||
{
|
||||
if (user == null) { return 0; }
|
||||
if (user?.Info == null) { return 0; }
|
||||
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
|
||||
int quality = 0;
|
||||
float floatQuality = 0.0f;
|
||||
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality);
|
||||
foreach (var tag in fabricatedItem.TargetItem.Tags)
|
||||
{
|
||||
floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag);
|
||||
@@ -637,9 +656,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
//fabricating takes 100 times longer if degree of success is close to 0
|
||||
//characters with a higher skill than required can fabricate up to 100% faster
|
||||
return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f);
|
||||
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
|
||||
if (user is not null && fabricableItem.TargetItem is { } it && it.Tags.Contains("medical"))
|
||||
{
|
||||
time *= 1f + user.GetStatValue(StatTypes.FabricateMedicineSpeedMultiplier);
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
|
||||
public float FabricationDegreeOfSuccess(Character character, ImmutableArray<Skill> skills)
|
||||
{
|
||||
if (skills.Length == 0) { return 1.0f; }
|
||||
@@ -713,7 +737,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
availableIngredients[itemIdentifier] = new List<Item>(itemList.Count);
|
||||
}
|
||||
availableIngredients[itemIdentifier].Add(item);
|
||||
//order by condition (prefer using worst-condition items)
|
||||
int index = 0;
|
||||
while (index < availableIngredients[itemIdentifier].Count &&
|
||||
availableIngredients[itemIdentifier][index].Condition < item.Condition)
|
||||
{
|
||||
index++;
|
||||
}
|
||||
availableIngredients[itemIdentifier].Insert(index, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,5 +858,15 @@ namespace Barotrauma.Items.Components
|
||||
public float Value { get; set; }
|
||||
public ItemPrefab ItemPrefab { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class AbilityFabricationItemIngredients : AbilityObject
|
||||
{
|
||||
public List<Item> Items { get; set; }
|
||||
|
||||
public AbilityFabricationItemIngredients(List<Item> items)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ namespace Barotrauma.Items.Components
|
||||
[Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)]
|
||||
public float MaxFlow
|
||||
{
|
||||
get { return maxFlow; }
|
||||
set { maxFlow = value; }
|
||||
get => maxFlow;
|
||||
set => maxFlow = value;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
@@ -92,13 +92,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
pumpSpeedLockTimer -= deltaTime;
|
||||
isActiveLockTimer -= deltaTime;
|
||||
|
||||
if (!IsActive) { return; }
|
||||
if (!IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currFlow = 0.0f;
|
||||
|
||||
@@ -122,7 +125,10 @@ namespace Barotrauma.Items.Components
|
||||
FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f;
|
||||
}
|
||||
|
||||
if (!HasPower) { return; }
|
||||
if (!HasPower)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
@@ -132,13 +138,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
|
||||
|
||||
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
|
||||
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
|
||||
|
||||
if (item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering)
|
||||
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
|
||||
{
|
||||
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
|
||||
}
|
||||
|
||||
currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow);
|
||||
|
||||
//less effective when in a bad condition
|
||||
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
|
||||
@@ -83,19 +83,24 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (lastUser == value) { return; }
|
||||
lastUser = value;
|
||||
degreeOfSuccess = lastUser == null ? 0.0f : Math.Min(DegreeOfSuccess(lastUser), 1.0f);
|
||||
LastUserWasPlayer = lastUser.IsPlayer;
|
||||
if (lastUser == null)
|
||||
{
|
||||
degreeOfSuccess = 0.0f;
|
||||
LastUserWasPlayer = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
degreeOfSuccess = Math.Min(DegreeOfSuccess(lastUser), 1.0f);
|
||||
LastUserWasPlayer = lastUser.IsPlayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)]
|
||||
public float MaxPowerOutput
|
||||
{
|
||||
get { return maxPowerOutput; }
|
||||
set
|
||||
{
|
||||
maxPowerOutput = Math.Max(0.0f, value);
|
||||
}
|
||||
get => maxPowerOutput;
|
||||
set => maxPowerOutput = Math.Max(0.0f, value);
|
||||
}
|
||||
|
||||
[Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")]
|
||||
@@ -144,11 +149,11 @@ namespace Barotrauma.Items.Components
|
||||
turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)]
|
||||
public float FuelConsumptionRate
|
||||
{
|
||||
get { return fuelConsumptionRate; }
|
||||
get => fuelConsumptionRate;
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
@@ -248,6 +253,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#endif
|
||||
|
||||
float maxPowerOut = GetMaxOutput();
|
||||
|
||||
if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1)
|
||||
{
|
||||
TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
|
||||
@@ -281,9 +288,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
//use a smoothed "correct output" instead of the actual correct output based on the load
|
||||
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
|
||||
if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f))
|
||||
if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f))
|
||||
{
|
||||
CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime;
|
||||
CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime;
|
||||
}
|
||||
|
||||
//calculate tolerances of the meters based on the skills of the user
|
||||
@@ -342,7 +349,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (!isConnectedToFriendlyOutpost)
|
||||
{
|
||||
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
|
||||
item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime;
|
||||
}
|
||||
}
|
||||
fuelLeft += item.ConditionPercentage;
|
||||
@@ -351,10 +358,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (fissionRate > 0.0f)
|
||||
{
|
||||
if (item.AiTarget != null && MaxPowerOutput > 0)
|
||||
if (item.AiTarget != null && maxPowerOut > 0)
|
||||
{
|
||||
var aiTarget = item.AiTarget;
|
||||
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
|
||||
float range = Math.Abs(currPowerConsumption) / maxPowerOut;
|
||||
aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
@@ -425,15 +432,17 @@ namespace Barotrauma.Items.Components
|
||||
tolerance = 3f;
|
||||
}
|
||||
|
||||
float maxPowerOut = GetMaxOutput();
|
||||
|
||||
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
|
||||
float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1);
|
||||
float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor);
|
||||
float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1);
|
||||
float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor);
|
||||
|
||||
minUpdatePowerOut = minOutput;
|
||||
maxUpdatePowerOut = maxOutput;
|
||||
|
||||
float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut;
|
||||
|
||||
float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut;
|
||||
|
||||
return new PowerRange(minOutput, maxOutput, reactorMax);
|
||||
}
|
||||
|
||||
@@ -456,11 +465,13 @@ namespace Barotrauma.Items.Components
|
||||
float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut);
|
||||
float newLoad = loadLeft;
|
||||
|
||||
float maxOutput = GetMaxOutput();
|
||||
|
||||
//Adjust behaviour for multi reactor setup
|
||||
if (MaxPowerOutput != minMaxPower.ReactorMaxOutput)
|
||||
if (maxOutput != minMaxPower.ReactorMaxOutput)
|
||||
{
|
||||
float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft;
|
||||
float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100);
|
||||
float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft;
|
||||
float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100);
|
||||
newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft);
|
||||
}
|
||||
|
||||
@@ -501,7 +512,7 @@ namespace Barotrauma.Items.Components
|
||||
//calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max
|
||||
float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate);
|
||||
float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f);
|
||||
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput;
|
||||
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput();
|
||||
|
||||
//maximum output not enough, we need more fuel
|
||||
return theoreticalMaxOutput < Load * minimumOutputRatio;
|
||||
@@ -685,7 +696,7 @@ namespace Barotrauma.Items.Components
|
||||
aiUpdateTimer = AIUpdateInterval;
|
||||
// load more fuel if the current maximum output is only 50% of the current load
|
||||
// or if the fuel rod is (almost) deplenished
|
||||
float minCondition = fuelConsumptionRate * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2);
|
||||
float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2);
|
||||
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
|
||||
{
|
||||
bool outOfFuel = false;
|
||||
@@ -863,5 +874,8 @@ namespace Barotrauma.Items.Components
|
||||
if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; }
|
||||
}
|
||||
}
|
||||
|
||||
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
|
||||
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,13 +153,6 @@ namespace Barotrauma.Items.Components
|
||||
bool changed = currentMode != value;
|
||||
|
||||
currentMode = value;
|
||||
if (value == Mode.Passive)
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SectorDegrees = 360.0f;
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
if (changed) { prevPassivePingRadius = float.MaxValue; }
|
||||
UpdateGUIElements();
|
||||
@@ -204,15 +197,13 @@ namespace Barotrauma.Items.Components
|
||||
if (currentPingIndex != -1)
|
||||
{
|
||||
var activePing = activePings[currentPingIndex];
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom);
|
||||
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range);
|
||||
}
|
||||
if (activePing.State > 1.0f)
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom);
|
||||
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range);
|
||||
item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f;
|
||||
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
|
||||
}
|
||||
aiPingCheckPending = true;
|
||||
currentPingIndex = -1;
|
||||
}
|
||||
@@ -228,15 +219,16 @@ namespace Barotrauma.Items.Components
|
||||
activePings[currentPingIndex].Direction = pingDirection;
|
||||
activePings[currentPingIndex].State = 0.0f;
|
||||
activePings[currentPingIndex].PrevPingRadius = 0.0f;
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f;
|
||||
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
|
||||
}
|
||||
item.Use(deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SectorDegrees = 360.0f;
|
||||
}
|
||||
aiPingCheckPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components
|
||||
[Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")]
|
||||
public float Capacity
|
||||
{
|
||||
get { return capacity; }
|
||||
get => capacity;
|
||||
set { capacity = Math.Max(value, 1.0f); }
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public float ChargePercentage => MathUtils.Percentage(Charge, Capacity);
|
||||
public float ChargePercentage => MathUtils.Percentage(Charge, GetCapacity());
|
||||
|
||||
[Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")]
|
||||
public float MaxRechargeSpeed
|
||||
@@ -125,10 +125,19 @@ namespace Barotrauma.Items.Components
|
||||
set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); }
|
||||
}
|
||||
|
||||
private bool flipIndicator;
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge be flipped to fill from the other side.")]
|
||||
public bool FlipIndicator
|
||||
{
|
||||
get { return flipIndicator; }
|
||||
set { flipIndicator = value; }
|
||||
}
|
||||
|
||||
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
|
||||
|
||||
public const float aiRechargeTargetRatio = 0.5f;
|
||||
private bool isRunning;
|
||||
|
||||
public bool HasBeenTuned { get; private set; }
|
||||
|
||||
public PowerContainer(Item item, ContentXElement element)
|
||||
@@ -146,7 +155,7 @@ namespace Barotrauma.Items.Components
|
||||
return picker != null;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (item.Connections == null)
|
||||
{
|
||||
@@ -283,7 +292,7 @@ namespace Barotrauma.Items.Components
|
||||
else
|
||||
{
|
||||
//Decrease charge based on how much power is leaving the device
|
||||
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity);
|
||||
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, GetCapacity());
|
||||
prevCharge = Charge;
|
||||
}
|
||||
}
|
||||
@@ -370,5 +379,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,6 +736,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (target.IsSensor) { return false; }
|
||||
if (hits.Contains(target.Body)) { return false; }
|
||||
if (target.Body.UserData is Submarine)
|
||||
{
|
||||
@@ -881,7 +882,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
|
||||
#if CLIENT
|
||||
if (attackResult.Damage > 0.0f)
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(targetItem,
|
||||
targetItem.WorldPosition,
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Abilities;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -420,7 +421,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.ConditionPercentage > MinDeteriorationCondition)
|
||||
{
|
||||
item.Condition -= DeteriorationSpeed * deltaTime;
|
||||
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
|
||||
item.Condition -= deteriorationSpeed * deltaTime;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -467,8 +469,14 @@ namespace Barotrauma.Items.Components
|
||||
wasGoodCondition = true;
|
||||
}
|
||||
|
||||
float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed);
|
||||
if (requiredSkills.Any(static skill => skill.Identifier == "mechanical"))
|
||||
{
|
||||
talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed);
|
||||
}
|
||||
|
||||
float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor);
|
||||
fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
|
||||
fixDuration /= 1 + talentMultiplier + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
|
||||
fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed);
|
||||
|
||||
item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer);
|
||||
@@ -500,7 +508,7 @@ namespace Barotrauma.Items.Components
|
||||
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f));
|
||||
}
|
||||
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
|
||||
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete);
|
||||
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item));
|
||||
}
|
||||
if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; }
|
||||
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
|
||||
@@ -687,4 +695,14 @@ namespace Barotrauma.Items.Components
|
||||
//where set_active/set_state signals can disable the component
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AbilityRepairable : AbilityObject, IAbilityItem
|
||||
{
|
||||
public Item Item { get; set; }
|
||||
|
||||
public AbilityRepairable(Item item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +270,9 @@ namespace Barotrauma.Items.Components
|
||||
public bool AutoEquipWhenFull { get; private set; }
|
||||
public bool DisplayContainedStatus { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
|
||||
public bool AllowUseWhenWorn { get; set; }
|
||||
|
||||
public readonly int Variants;
|
||||
|
||||
private int variant;
|
||||
|
||||
@@ -226,6 +226,14 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var item in slots[i].Items)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
bool duplicateFound = false;
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
|
||||
@@ -424,6 +424,39 @@ namespace Barotrauma
|
||||
|
||||
public Color? HighlightColor;
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects or conditionals to check whether the item is contained inside something
|
||||
/// </summary>
|
||||
public bool IsContained
|
||||
{
|
||||
get
|
||||
{
|
||||
return parentInventory != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects or conditionals to the speed of the item
|
||||
/// </summary>
|
||||
public float Speed
|
||||
{
|
||||
get
|
||||
{
|
||||
if (body != null && body.PhysEnabled)
|
||||
{
|
||||
return body.LinearVelocity.Length();
|
||||
}
|
||||
else if (ParentInventory?.Owner is Character character)
|
||||
{
|
||||
return character.AnimController.MainLimb.LinearVelocity.Length();
|
||||
}
|
||||
else if (container != null)
|
||||
{
|
||||
return container.Speed;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
|
||||
@@ -821,6 +854,16 @@ namespace Barotrauma
|
||||
|
||||
public bool IsSecondaryItem { get; }
|
||||
|
||||
private ItemStatManager statManager;
|
||||
public ItemStatManager StatManager
|
||||
{
|
||||
get
|
||||
{
|
||||
statManager ??= new ItemStatManager(this);
|
||||
return statManager;
|
||||
}
|
||||
}
|
||||
|
||||
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
|
||||
: this(new Rectangle(
|
||||
(int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
|
||||
@@ -1837,16 +1880,32 @@ namespace Barotrauma
|
||||
|
||||
if (ic.IsActiveConditionals != null)
|
||||
{
|
||||
bool shouldBeActive = true;
|
||||
foreach (var conditional in ic.IsActiveConditionals)
|
||||
if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And)
|
||||
{
|
||||
if (!ConditionalMatches(conditional))
|
||||
bool shouldBeActive = true;
|
||||
foreach (var conditional in ic.IsActiveConditionals)
|
||||
{
|
||||
shouldBeActive = false;
|
||||
break;
|
||||
if (!ConditionalMatches(conditional))
|
||||
{
|
||||
shouldBeActive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ic.IsActive = shouldBeActive;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool shouldBeActive = false;
|
||||
foreach (var conditional in ic.IsActiveConditionals)
|
||||
{
|
||||
if (ConditionalMatches(conditional))
|
||||
{
|
||||
shouldBeActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ic.IsActive = shouldBeActive;
|
||||
}
|
||||
ic.IsActive = shouldBeActive;
|
||||
}
|
||||
#if CLIENT
|
||||
if (ic.HasSounds)
|
||||
@@ -2072,7 +2131,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//no need to apply buoyancy if the item is still and not light enough to float
|
||||
if (moving || body.Density < 10.0f)
|
||||
if (moving || body.Density <= 10.0f)
|
||||
{
|
||||
Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity;
|
||||
body.ApplyForce(buoyancy);
|
||||
@@ -2699,8 +2758,6 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f;
|
||||
|
||||
bool remove = false;
|
||||
foreach (ItemComponent ic in components)
|
||||
{
|
||||
@@ -2713,19 +2770,7 @@ namespace Barotrauma
|
||||
ic.PlaySound(actionType, user);
|
||||
#endif
|
||||
ic.WasUsed = true;
|
||||
ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction);
|
||||
|
||||
if (applyOnSelfFraction > 0.0f)
|
||||
{
|
||||
//hacky af
|
||||
ic.statusEffectLists.TryGetValue(actionType, out var effectList);
|
||||
if (effectList != null)
|
||||
{
|
||||
effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction);
|
||||
ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user);
|
||||
effectList.ForEach(e => e.AfflictionMultiplier = 1.0f);
|
||||
}
|
||||
}
|
||||
ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user);
|
||||
|
||||
if (GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
@@ -2866,15 +2911,20 @@ namespace Barotrauma
|
||||
//to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients
|
||||
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
|
||||
SerializableProperty property = extraData.SerializableProperty;
|
||||
ISerializableEntity entity = extraData.Entity;
|
||||
if (property != null)
|
||||
{
|
||||
var propertyOwner = allProperties.Find(p => p.property == property);
|
||||
if (allProperties.Count > 1)
|
||||
{
|
||||
msg.WriteByte((byte)allProperties.FindIndex(p => p.property == property));
|
||||
int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity);
|
||||
if (propertyIndex < -1)
|
||||
{
|
||||
throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\"");
|
||||
}
|
||||
msg.WriteVariableUInt32((uint)propertyIndex);
|
||||
}
|
||||
|
||||
object value = property.GetValue(propertyOwner.obj);
|
||||
object value = property.GetValue(entity);
|
||||
if (value is string stringVal)
|
||||
{
|
||||
msg.WriteString(stringVal);
|
||||
@@ -2979,7 +3029,7 @@ namespace Barotrauma
|
||||
int propertyIndex = 0;
|
||||
if (allProperties.Count > 1)
|
||||
{
|
||||
propertyIndex = msg.ReadByte();
|
||||
propertyIndex = (int)msg.ReadVariableUInt32();
|
||||
}
|
||||
|
||||
bool allowEditing = true;
|
||||
@@ -3119,14 +3169,14 @@ namespace Barotrauma
|
||||
}
|
||||
logPropertyChangeCoroutine = CoroutineManager.Invoke(() =>
|
||||
{
|
||||
GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
|
||||
GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
|
||||
}, delay: 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GameMain.NetworkMember is { IsServer: true })
|
||||
if (GameMain.NetworkMember is { IsServer: true } && parentObject is ISerializableEntity entity)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property));
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property, entity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3230,7 +3280,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!(property.GetValue(item)?.Equals(prevValue) ?? true))
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property));
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property, item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3349,8 +3399,24 @@ namespace Barotrauma
|
||||
item.PurchasedNewSwap = false;
|
||||
}
|
||||
|
||||
item.condition = element.GetAttributeFloat("condition", item.condition);
|
||||
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
|
||||
if (element.GetAttribute("conditionpercentage") != null)
|
||||
{
|
||||
item.condition = element.GetAttributeFloat("conditionpercentage", 100.0f) / 100.0f * item.MaxCondition;
|
||||
}
|
||||
else
|
||||
{
|
||||
//backwards compatibility
|
||||
item.condition = element.GetAttributeFloat("condition", item.condition);
|
||||
//if the item was in full condition considering the unmodified health
|
||||
//(not taking possible HealthMultipliers added by mods into account),
|
||||
//make sure it stays in full condition
|
||||
bool wasFullCondition = item.condition >= item.Prefab.Health;
|
||||
if (wasFullCondition)
|
||||
{
|
||||
item.condition = item.MaxCondition;
|
||||
}
|
||||
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
|
||||
}
|
||||
item.lastSentCondition = item.condition;
|
||||
item.RecalculateConditionValues();
|
||||
item.SetActiveSprite();
|
||||
@@ -3370,6 +3436,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (ItemComponent component in item.components)
|
||||
{
|
||||
if (component.Parent != null) { component.IsActive = component.Parent.IsActive; }
|
||||
component.OnItemLoaded();
|
||||
}
|
||||
|
||||
@@ -3401,11 +3468,6 @@ namespace Barotrauma
|
||||
element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier))));
|
||||
}
|
||||
|
||||
if (condition < MaxCondition)
|
||||
{
|
||||
element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
if (!MathUtils.NearlyEqual(healthMultiplier, 1.0f))
|
||||
{
|
||||
element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture)));
|
||||
@@ -3442,6 +3504,16 @@ namespace Barotrauma
|
||||
upgrade.Save(element);
|
||||
}
|
||||
|
||||
if (condition < MaxCondition)
|
||||
{
|
||||
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var conditionAttribute = element.GetAttribute("condition");
|
||||
if (conditionAttribute != null) { conditionAttribute.Remove(); }
|
||||
}
|
||||
|
||||
parentElement.Add(element);
|
||||
|
||||
return element;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -18,9 +19,10 @@ namespace Barotrauma
|
||||
AssignCampaignInteraction = 6,
|
||||
ApplyStatusEffect = 7,
|
||||
Upgrade = 8,
|
||||
ItemStat = 9,
|
||||
|
||||
MinValue = 0,
|
||||
MaxValue = 6
|
||||
MaxValue = 9
|
||||
}
|
||||
|
||||
public interface IEventData : NetEntityEvent.IData
|
||||
@@ -56,10 +58,24 @@ namespace Barotrauma
|
||||
{
|
||||
public EventType EventType => EventType.ChangeProperty;
|
||||
public readonly SerializableProperty SerializableProperty;
|
||||
public readonly ISerializableEntity Entity;
|
||||
|
||||
public ChangePropertyEventData(SerializableProperty serializableProperty)
|
||||
public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity)
|
||||
{
|
||||
SerializableProperty = serializableProperty;
|
||||
Entity = entity;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct SetItemStatEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.ItemStat;
|
||||
|
||||
public readonly Dictionary<ItemStatManager.TalentStatIdentifier, float> Stats;
|
||||
|
||||
public SetItemStatEventData(Dictionary<ItemStatManager.TalentStatIdentifier, float> stats)
|
||||
{
|
||||
Stats = stats;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace Barotrauma
|
||||
CopyCondition = element.GetAttributeBool("copycondition", false);
|
||||
Commonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor",
|
||||
element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]);
|
||||
RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]);
|
||||
element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty<string>()) ?? Array.Empty<string>());
|
||||
RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty<string>());
|
||||
ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty);
|
||||
InfoText = element.GetAttributeString("infotext", string.Empty);
|
||||
InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty);
|
||||
@@ -102,12 +102,13 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly Identifier ItemPrefabIdentifier;
|
||||
|
||||
public ItemPrefab ItemPrefab => ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab
|
||||
: MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab ?? throw new Exception($"No ItemPrefab with identifier or name \"{ItemPrefabIdentifier}\"");
|
||||
public ItemPrefab ItemPrefab =>
|
||||
ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab
|
||||
: MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab;
|
||||
|
||||
public override UInt32 UintIdentifier { get; }
|
||||
|
||||
public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab.ToEnumerable();
|
||||
public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab == null ? Enumerable.Empty<ItemPrefab>() : ItemPrefab.ToEnumerable();
|
||||
|
||||
public override ItemPrefab FirstMatchingPrefab => ItemPrefab;
|
||||
|
||||
@@ -122,6 +123,11 @@ namespace Barotrauma
|
||||
using MD5 md5 = MD5.Create();
|
||||
UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} ({ItemPrefabIdentifier})";
|
||||
}
|
||||
}
|
||||
|
||||
public class RequiredItemByTag : RequiredItem
|
||||
@@ -146,6 +152,11 @@ namespace Barotrauma
|
||||
using MD5 md5 = MD5.Create();
|
||||
UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} ({Tag})";
|
||||
}
|
||||
}
|
||||
|
||||
public readonly Identifier TargetItemPrefabIdentifier;
|
||||
@@ -390,6 +401,8 @@ namespace Barotrauma
|
||||
{
|
||||
public static readonly PrefabCollection<ItemPrefab> Prefabs = new PrefabCollection<ItemPrefab>();
|
||||
|
||||
public const float DefaultInteractDistance = 120.0f;
|
||||
|
||||
//default size
|
||||
public Vector2 Size { get; private set; }
|
||||
|
||||
@@ -410,7 +423,6 @@ namespace Barotrauma
|
||||
public ImmutableArray<Rectangle> Triggers { get; private set; }
|
||||
|
||||
private ImmutableDictionary<Identifier, float> treatmentSuitability;
|
||||
private readonly List<XElement> fabricationRecipeElements = new List<XElement>();
|
||||
|
||||
/// <summary>
|
||||
/// Is this prefab overriding a prefab in another content package
|
||||
@@ -590,7 +602,7 @@ namespace Barotrauma
|
||||
public override ImmutableHashSet<string> Aliases => aliases;
|
||||
|
||||
//how close the Character has to be to the item to pick it up
|
||||
[Serialize(120.0f, IsPropertySaveable.No)]
|
||||
[Serialize(DefaultInteractDistance, IsPropertySaveable.No)]
|
||||
public float InteractDistance { get; private set; }
|
||||
|
||||
// this can be used to allow items which are behind other items tp
|
||||
@@ -752,6 +764,9 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool DontTransferBetweenSubs { get; private set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool ShowHealthBar { get; private set; }
|
||||
|
||||
protected override Identifier DetermineIdentifier(XElement element)
|
||||
{
|
||||
Identifier identifier = base.DetermineIdentifier(element);
|
||||
@@ -1143,7 +1158,7 @@ namespace Barotrauma
|
||||
public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
|
||||
{
|
||||
priceInfo = GetPriceInfo(store);
|
||||
return priceInfo != null && priceInfo.CanBeBought && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
|
||||
return priceInfo is { CanBeBought: true } && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
|
||||
}
|
||||
|
||||
public bool CanBeBoughtFrom(Location location)
|
||||
@@ -1240,13 +1255,12 @@ namespace Barotrauma
|
||||
throw new ArgumentException("Both name and identifier cannot be null.");
|
||||
}
|
||||
|
||||
ItemPrefab prefab;
|
||||
if (identifier.IsEmpty)
|
||||
{
|
||||
//legacy support
|
||||
identifier = GenerateLegacyIdentifier(name);
|
||||
}
|
||||
Prefabs.TryGet(identifier, out prefab);
|
||||
Prefabs.TryGet(identifier, out ItemPrefab prefab);
|
||||
|
||||
//not found, see if we can find a prefab with a matching alias
|
||||
if (prefab == null && !string.IsNullOrEmpty(name))
|
||||
@@ -1294,8 +1308,8 @@ namespace Barotrauma
|
||||
return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags));
|
||||
}
|
||||
|
||||
private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition;
|
||||
private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) =>
|
||||
private static bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition;
|
||||
private static bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) =>
|
||||
pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item));
|
||||
|
||||
public static bool IsContainerPreferred(IEnumerable<Identifier> preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id));
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal sealed class ItemStatManager
|
||||
{
|
||||
private Item item;
|
||||
|
||||
public ItemStatManager(Item item)
|
||||
{
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct
|
||||
{
|
||||
public override int GetHashCode() => HashCode.Combine(TalentIdentifier, CharacterID, Stat);
|
||||
}
|
||||
|
||||
private readonly Dictionary<TalentStatIdentifier, float> talentStats = new();
|
||||
|
||||
public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent)
|
||||
{
|
||||
if (talent.Character?.ID is not { } characterId ||
|
||||
talent.Prefab?.Identifier is not { } talentIdentifier)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId);
|
||||
talentStats[identifier] = value;
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.NetworkMember is { IsServer: true } server)
|
||||
{
|
||||
server.CreateEntityEvent(item, new Item.SetItemStatEventData(talentStats));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Used for getting the value value from network packet
|
||||
public void ApplyStat(TalentStatIdentifier identifier, float value)
|
||||
{
|
||||
talentStats[identifier] = value;
|
||||
}
|
||||
|
||||
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
|
||||
{
|
||||
float total = originalValue;
|
||||
foreach (var (key, value) in talentStats)
|
||||
{
|
||||
if (key.Stat == stat)
|
||||
{
|
||||
total *= value;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -21,6 +22,8 @@ namespace Barotrauma
|
||||
|
||||
public bool MatchOnEmpty { get; set; }
|
||||
|
||||
public bool RequireEmpty { get; set; }
|
||||
|
||||
public bool IgnoreInEditor { get; set; }
|
||||
|
||||
private ImmutableHashSet<Identifier> excludedIdentifiers;
|
||||
@@ -133,40 +136,52 @@ namespace Barotrauma
|
||||
if (parentItem == null) { return false; }
|
||||
return CheckContained(parentItem);
|
||||
case RelationType.Container:
|
||||
if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; }
|
||||
return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && (!ExcludeFullCondition || !parentItem.Container.IsFullCondition) && MatchesItem(parentItem.Container);
|
||||
if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty || RequireEmpty; }
|
||||
return CheckItem(parentItem.Container, this);
|
||||
case RelationType.Equipped:
|
||||
if (character == null) { return false; }
|
||||
if (MatchOnEmpty && !character.HeldItems.Any()) { return true; }
|
||||
foreach (Item equippedItem in character.HeldItems)
|
||||
var heldItems = character.HeldItems;
|
||||
if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; }
|
||||
foreach (Item equippedItem in heldItems)
|
||||
{
|
||||
if (equippedItem == null) { continue; }
|
||||
if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && (!ExcludeFullCondition || !equippedItem.IsFullCondition) && MatchesItem(equippedItem)) { return true; }
|
||||
if (CheckItem(equippedItem, this))
|
||||
{
|
||||
if (RequireEmpty && equippedItem.Condition > 0) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RelationType.Picked:
|
||||
if (character == null || character.Inventory == null) { return false; }
|
||||
foreach (Item pickedItem in character.Inventory.AllItems)
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; }
|
||||
var allItems = character.Inventory.AllItems;
|
||||
if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; }
|
||||
foreach (Item pickedItem in allItems)
|
||||
{
|
||||
if (MatchesItem(pickedItem)) { return true; }
|
||||
if (pickedItem == null) { continue; }
|
||||
if (CheckItem(pickedItem, this))
|
||||
{
|
||||
if (RequireEmpty && pickedItem.Condition > 0) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CheckItem(Item i, RelatedItem ri) => (!ri.ExcludeBroken || ri.RequireEmpty || i.Condition > 0.0f) && (!ri.ExcludeFullCondition || !i.IsFullCondition) && ri.MatchesItem(i);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CheckContained(Item parentItem)
|
||||
{
|
||||
if (parentItem.OwnInventory == null) { return false; }
|
||||
|
||||
if (MatchOnEmpty && parentItem.OwnInventory.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isEmpty = parentItem.OwnInventory.IsEmpty();
|
||||
if (RequireEmpty && !isEmpty) { return false; }
|
||||
if (MatchOnEmpty && isEmpty) { return true; }
|
||||
foreach (Item contained in parentItem.ContainedItems)
|
||||
{
|
||||
if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; }
|
||||
@@ -184,6 +199,7 @@ namespace Barotrauma
|
||||
new XAttribute("optional", IsOptional),
|
||||
new XAttribute("ignoreineditor", IgnoreInEditor),
|
||||
new XAttribute("excludebroken", ExcludeBroken),
|
||||
new XAttribute("requireempty", RequireEmpty),
|
||||
new XAttribute("excludefullcondition", ExcludeFullCondition),
|
||||
new XAttribute("targetslot", TargetSlot),
|
||||
new XAttribute("allowvariants", AllowVariants));
|
||||
@@ -249,6 +265,7 @@ namespace Barotrauma
|
||||
RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers)
|
||||
{
|
||||
ExcludeBroken = element.GetAttributeBool("excludebroken", true),
|
||||
RequireEmpty = element.GetAttributeBool("requireempty", false),
|
||||
ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false),
|
||||
AllowVariants = element.GetAttributeBool("allowvariants", true)
|
||||
};
|
||||
|
||||
@@ -1026,7 +1026,7 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
branch.DamageVisualizationTimer = 1.0f;
|
||||
}
|
||||
|
||||
if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; }
|
||||
if (branch.IsRootGrowth && root is { Health: > 0.0f }) { return; }
|
||||
|
||||
if (type != AttackType.Other && type != AttackType.CutFromRoot)
|
||||
{
|
||||
@@ -1035,7 +1035,7 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
{
|
||||
// damage is handled server side
|
||||
if (GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
@@ -1059,6 +1059,11 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
|
||||
if (type == AttackType.Fire)
|
||||
{
|
||||
if (attacker is not null)
|
||||
{
|
||||
damage *= 1f + attacker.GetStatValue(StatTypes.BallastFloraDamageMultiplier);
|
||||
}
|
||||
|
||||
if (IsInWater(branch))
|
||||
{
|
||||
damage *= 1f - SubmergedWaterResistance;
|
||||
@@ -1066,7 +1071,7 @@ namespace Barotrauma.MapCreatures.Behavior
|
||||
|
||||
if (defenseCooldown <= 0)
|
||||
{
|
||||
if (!(StateMachine.State is DefendWithPumpState))
|
||||
if (StateMachine.State is not DefendWithPumpState)
|
||||
{
|
||||
StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker));
|
||||
defenseCooldown = 180f;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user