Merge branch 'dev' of https://github.com/Regalis11/Barotrauma.git into unstable-tests

This commit is contained in:
Evil Factory
2022-04-08 12:52:28 -03:00
990 changed files with 44338 additions and 38589 deletions
@@ -1,185 +0,0 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class CameraTransition
{
public bool Running
{
get;
private set;
}
public Camera AssignedCamera;
private readonly Alignment? cameraStartPos;
private readonly Alignment? cameraEndPos;
private readonly float? startZoom;
private readonly float? endZoom;
public readonly float WaitDuration;
public readonly float PanDuration;
public readonly bool FadeOut;
public readonly bool LosFadeIn;
private readonly CoroutineHandle updateCoroutine;
private Character prevControlled;
public bool AllowInterrupt = false;
public bool RemoveControlFromCharacter = true;
public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null)
{
WaitDuration = waitDuration;
PanDuration = panDuration;
FadeOut = fadeOut;
LosFadeIn = losFadeIn;
this.cameraStartPos = cameraStartPos;
this.cameraEndPos = cameraEndPos;
this.startZoom = startZoom;
this.endZoom = endZoom;
AssignedCamera = cam;
if (targetEntity == null) { return; }
Running = true;
CoroutineManager.StopCoroutines("CameraTransition");
updateCoroutine = CoroutineManager.StartCoroutine(Update(targetEntity, cam), "CameraTransition");
}
public void Stop()
{
CoroutineManager.StopCoroutines(updateCoroutine);
Running = false;
#if CLIENT
if (FadeOut) { GUI.ScreenOverlayColor = Color.TransparentBlack; }
if (prevControlled != null && !prevControlled.Removed)
{
Character.Controlled = prevControlled;
}
#endif
}
private IEnumerable<CoroutineStatus> Update(ISpatialEntity targetEntity, Camera cam)
{
if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; }
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
{
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Character.Controlled = null;
}
cam.TargetPos = Vector2.Zero;
float startZoom = this.startZoom ?? cam.Zoom;
float endZoom = this.endZoom ?? 0.5f;
Vector2 initialCameraPos = cam.Position;
Vector2? initialTargetPos = targetEntity?.WorldPosition;
float timer = -WaitDuration;
while (timer < PanDuration)
{
float clampedTimer = Math.Max(timer, 0f);
if (Screen.Selected != GameMain.GameScreen)
{
yield return new WaitForSeconds(0.1f);
#if CLIENT
if (FadeOut) { GUI.ScreenOverlayColor = Color.TransparentBlack; }
#endif
Running = false;
yield return CoroutineStatus.Success;
}
//switched control to some other character during the transition -> remove control again
if (Character.Controlled != null)
{
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
{
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Character.Controlled = null;
}
}
if (prevControlled != null && prevControlled.Removed)
{
prevControlled = null;
}
#if CLIENT
if (AllowInterrupt && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape))
{
break;
}
#endif
Vector2 minPos = targetEntity.WorldPosition;
Vector2 maxPos = targetEntity.WorldPosition;
if (targetEntity is Submarine sub)
{
minPos = new Vector2(sub.WorldPosition.X - sub.Borders.Width / 2, sub.WorldPosition.Y - sub.Borders.Height / 2);
maxPos = new Vector2(sub.WorldPosition.X + sub.Borders.Width / 2, sub.WorldPosition.Y + sub.Borders.Height / 2);
}
Vector2 startPos = cameraStartPos.HasValue ?
new Vector2(
MathHelper.Lerp(minPos.X, maxPos.X, (cameraStartPos.Value.ToVector2().X + 1.0f) / 2.0f),
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraStartPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
initialCameraPos;
if (!cameraStartPos.HasValue && initialTargetPos.HasValue)
{
startPos += targetEntity.WorldPosition - initialTargetPos.Value;
}
Vector2 endPos = cameraEndPos.HasValue ?
new Vector2(
MathHelper.Lerp(minPos.X, maxPos.X, (cameraEndPos.Value.ToVector2().X + 1.0f) / 2.0f),
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
prevControlled?.WorldPosition ?? targetEntity.WorldPosition;
Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration);
cam.Translate(cameraPos - cam.Position);
#if CLIENT
cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, clampedTimer / PanDuration);
if (clampedTimer / PanDuration > 0.9f)
{
if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((clampedTimer / PanDuration) - 0.9f) * 10.0f); }
}
if (LosFadeIn && clampedTimer / PanDuration > 0.8f)
{
GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f;
Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity);
GameMain.LightManager.LosEnabled = true;
}
#endif
timer += CoroutineManager.UnscaledDeltaTime;
yield return CoroutineStatus.Running;
}
Running = false;
yield return new WaitForSeconds(0.1f);
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f;
#endif
if (prevControlled != null && !prevControlled.Removed)
{
Character.Controlled = prevControlled;
}
yield return CoroutineStatus.Success;
}
}
}
@@ -89,8 +89,8 @@ namespace Barotrauma
set;
}
public string SonarLabel;
public string SonarIconIdentifier;
public LocalizedString SonarLabel;
public Identifier SonarIconIdentifier;
private bool inDetectable;
@@ -172,13 +172,9 @@ namespace Barotrauma
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
string label = element.GetAttributeString("sonarlabel", "");
SonarLabel = TextManager.Get(label, returnNull: true) ?? label;
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
string typeString = element.GetAttributeString("type", "Any");
if (Enum.TryParse(typeString, out TargetType t))
{
Type = t;
}
SonarLabel = TextManager.Get(label).Fallback(label);
SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", Identifier.Empty);
Type = element.GetAttributeEnum("type", TargetType.Any);
Reset();
}
@@ -56,7 +56,7 @@ namespace Barotrauma
private readonly float updateTargetsInterval = 1;
private readonly float updateMemoriesInverval = 1;
private readonly float attackLimbResetInterval = 2;
private readonly float attackLimbSelectionInterval = 3;
// Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target.
private const float minPriority = 10;
@@ -65,10 +65,10 @@ namespace Barotrauma
private float updateTargetsTimer;
private float updateMemoriesTimer;
private float attackLimbResetTimer;
private float attackLimbSelectionTimer;
private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0;
private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning;
private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0;
public float CombatStrength => AIParams.CombatStrength;
private float Sight => AIParams.Sight;
private float Hearing => AIParams.Hearing;
@@ -77,25 +77,25 @@ namespace Barotrauma
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
private Limb _attackingLimb;
private Limb _previousAttackingLimb;
public Limb AttackingLimb
private Limb _attackLimb;
private Limb _previousAttackLimb;
public Limb AttackLimb
{
get { return _attackingLimb; }
get { return _attackLimb; }
private set
{
attackLimbResetTimer = 0;
if (_attackingLimb != value)
if (_attackLimb != value)
{
_previousAttackingLimb = _attackingLimb;
_previousAttackLimb = _attackLimb;
_previousAttackLimb?.AttachedRope?.Snap();
}
if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0)
else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0)
{
SetAimTimer();
_attackLimb.AttachedRope?.Snap();
}
_attackingLimb = value;
_attackLimb = value;
attackVector = null;
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
Reverse = _attackLimb != null && _attackLimb.attack.Reverse;
}
}
@@ -239,7 +239,7 @@ namespace Barotrauma
{
throw new Exception($"Tried to create an enemy ai controller for human!");
}
if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase))
if (Character.Params.Group == "human")
{
// Pet
Character.TeamID = CharacterTeamType.FriendlyNPC;
@@ -252,7 +252,7 @@ namespace Barotrauma
List<XElement> aiElements = new List<XElement>();
List<float> aiCommonness = new List<float>();
foreach (XElement element in mainElement.Elements())
foreach (var element in mainElement.Elements())
{
if (!element.Name.ToString().Equals("ai", StringComparison.OrdinalIgnoreCase)) { continue; }
aiElements.Add(element);
@@ -270,12 +270,12 @@ namespace Barotrauma
//choose a random ai element
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random);
foreach (XElement subElement in aiElement.Elements())
foreach (var subElement in aiElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "chooserandom":
LoadSubElement(subElement.Elements().GetRandom(random));
LoadSubElement(subElement.Elements().ToArray().GetRandom(random));
break;
default:
LoadSubElement(subElement);
@@ -330,12 +330,13 @@ namespace Barotrauma
return _aiParams;
}
}
private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false);
private CharacterParams.TargetParams GetTargetParams(string targetTag) => GetTargetParams(targetTag.ToIdentifier());
private CharacterParams.TargetParams GetTargetParams(Identifier targetTag) => AIParams.GetTarget(targetTag, false);
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
private string GetTargetingTag(AITarget aiTarget)
private Identifier GetTargetingTag(AITarget aiTarget)
{
if (aiTarget?.Entity == null) { return null; }
string targetingTag = null;
if (aiTarget?.Entity == null) { return Identifier.Empty; }
string targetingTag = string.Empty;
if (aiTarget.Entity is Character targetCharacter)
{
if (targetCharacter.IsDead)
@@ -387,7 +388,7 @@ namespace Barotrauma
break;
}
}
if (targetingTag == null)
if (targetingTag.IsNullOrEmpty())
{
if (targetItem.GetComponent<Sonar>() != null)
{
@@ -407,7 +408,7 @@ namespace Barotrauma
{
targetingTag = "room";
}
return targetingTag;
return targetingTag.ToIdentifier();
}
public override void SelectTarget(AITarget target) => SelectTarget(target, 100);
@@ -424,7 +425,8 @@ namespace Barotrauma
private void ReleaseDragTargets()
{
if (Character.Inventory != null)
AttackLimb?.AttachedRope?.Snap();
if (Character.Params.CanInteract && Character.Inventory != null)
{
Character.HeldItems.ForEach(i => i.GetComponent<Holdable>()?.GetRope()?.Snap());
}
@@ -599,7 +601,7 @@ namespace Barotrauma
UpdatePatrol(deltaTime);
break;
case AIState.Attack:
run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack;
run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack;
UpdateAttack(deltaTime);
break;
case AIState.Eat:
@@ -619,7 +621,7 @@ namespace Barotrauma
return;
}
float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition);
var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition);
if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2))
{
run = true;
@@ -683,7 +685,7 @@ namespace Barotrauma
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
//TODO: a way for pets to differentiate hostile and friendly humans?
if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase))
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget))
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
@@ -874,7 +876,10 @@ namespace Barotrauma
if (followLastTarget)
{
var target = SelectedAiTarget ?? _lastAiTarget;
if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null)
if (target?.Entity != null && !target.Entity.Removed &&
PreviousState == AIState.Attack && Character.CurrentHull == null &&
(_previousAttackLimb?.attack == null ||
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false);
@@ -999,7 +1004,7 @@ namespace Barotrauma
hullWeights.Clear();
float hullMinSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 2);
bool checkWaterLevel = !AIParams.PatrolFlooded || !AIParams.PatrolDry;
foreach (var hull in Hull.hullList)
foreach (var hull in Hull.HullList)
{
if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != Character.Submarine.TeamID) { continue; }
@@ -1125,31 +1130,42 @@ namespace Barotrauma
return;
}
}
attackLimbSelectionTimer -= deltaTime;
if (AttackLimb == null || attackLimbSelectionTimer <= 0)
{
attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f);
if (!IsAttackRunning && !IsCoolDownRunning)
{
AttackLimb = GetAttackLimb(attackWorldPos);
}
}
bool canAttack = true;
bool pursue = false;
if (IsCoolDownRunning)
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
{
var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb;
var currentAttackLimb = AttackLimb ?? _previousAttackLimb;
if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
{
return;
}
switch (currentAttackLimb.attack.AfterAttack)
AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack;
switch (activeBehavior)
{
case AIBehaviorAfterAttack.Pursue:
case AIBehaviorAfterAttack.PursueIfCanAttack:
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
if (activeBehavior == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
pursue = true;
}
else
{
UpdateFallBack(attackWorldPos, deltaTime, true);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
}
}
@@ -1161,13 +1177,13 @@ namespace Barotrauma
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
canAttack = false;
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack)
{
// Fall back if cannot attack.
UpdateFallBack(attackWorldPos, deltaTime, true);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
}
AttackingLimb = null;
AttackLimb = null;
}
else
{
@@ -1176,19 +1192,19 @@ namespace Barotrauma
if (newLimb != null)
{
// Attack with the new limb
AttackingLimb = newLimb;
AttackLimb = newLimb;
}
else
{
// No new limb was found.
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
if (activeBehavior == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
pursue = true;
}
else
{
UpdateFallBack(attackWorldPos, deltaTime, true);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
}
}
@@ -1203,10 +1219,15 @@ namespace Barotrauma
break;
case AIBehaviorAfterAttack.FallBackUntilCanAttack:
case AIBehaviorAfterAttack.FollowThroughUntilCanAttack:
case AIBehaviorAfterAttack.ReverseUntilCanAttack:
if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack)
{
Reverse = true;
}
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
else
@@ -1216,7 +1237,7 @@ namespace Barotrauma
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
else
@@ -1226,12 +1247,12 @@ namespace Barotrauma
if (newLimb != null)
{
// Attack with the new limb
AttackingLimb = newLimb;
AttackLimb = newLimb;
}
else
{
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
@@ -1239,7 +1260,7 @@ namespace Barotrauma
else
{
// Cooldown not yet expired -> steer away from the target
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
@@ -1268,7 +1289,7 @@ namespace Barotrauma
if (newLimb != null)
{
// Attack with the new limb
AttackingLimb = newLimb;
AttackLimb = newLimb;
}
else
{
@@ -1290,7 +1311,12 @@ namespace Barotrauma
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
case AIBehaviorAfterAttack.FallBack:
case AIBehaviorAfterAttack.Reverse:
default:
if (activeBehavior == AIBehaviorAfterAttack.Reverse)
{
Reverse = true;
}
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
return;
}
@@ -1302,12 +1328,13 @@ namespace Barotrauma
if (canAttack)
{
if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable))
if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity))
{
AttackingLimb = GetAttackLimb(attackWorldPos);
AttackLimb = GetAttackLimb(attackWorldPos);
}
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0;
}
if (!AIParams.CanOpenDoors)
{
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
@@ -1346,8 +1373,8 @@ namespace Barotrauma
// Target a specific limb instead of the target center position
if (wallTarget == null && targetCharacter != null)
{
var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType);
var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType);
if (attackTargetLimb == null)
{
State = AIState.Idle;
@@ -1360,7 +1387,7 @@ namespace Barotrauma
}
}
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition;
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
if (wallTarget != null && Character.Submarine == null)
@@ -1388,23 +1415,23 @@ namespace Barotrauma
Vector2 CalculateMargin(Vector2 targetVelocity)
{
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; }
float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
float distanceOffset = diff * AttackingLimb.attack.Duration;
float distanceOffset = diff * AttackLimb.attack.Duration;
// Intentionally omit the unit conversion because we use distanceOffset as a multiplier.
return targetVelocity * distanceOffset * dot;
}
// Check that we can reach the target
distance = toTarget.Length();
canAttack = distance < AttackingLimb.attack.Range;
canAttack = distance < AttackLimb.attack.Range;
// Crouch if the target is down (only humanoids), so that we can reach it.
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2)
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
{
if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range)
if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
{
humanoidAnimController.Crouching = true;
}
@@ -1412,14 +1439,14 @@ namespace Barotrauma
if (canAttack)
{
if (AttackingLimb.attack.Ranged)
if (AttackLimb.attack.Ranged)
{
// Check that is facing the target
float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float angle = VectorExtensions.Angle(forward, toTarget);
canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle);
if (canAttack && AttackingLimb.attack.AvoidFriendlyFire)
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;
@@ -1433,11 +1460,11 @@ namespace Barotrauma
}
if (canAttack)
{
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range));
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range));
bool IsBlocked(Vector2 targetPosition)
{
foreach (var body in Submarine.PickBodies(AttackingLimb.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)
@@ -1459,22 +1486,8 @@ namespace Barotrauma
}
}
}
else if (!IsAttackRunning && !IsCoolDownRunning)
{
// If not, reset the attacking limb, if the cooldown is not running
// Don't use the property, because we don't want cancel reversing, if we are reversing.
if (attackLimbResetTimer > attackLimbResetInterval)
{
_attackingLimb = null;
attackLimbResetTimer = 0;
}
else
{
attackLimbResetTimer += deltaTime;
}
}
}
Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null;
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
if (steeringLimb == null)
{
// If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target.
@@ -1489,9 +1502,9 @@ namespace Barotrauma
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (AttackingLimb != null && AttackingLimb.attack.Retreat)
if (AttackLimb != null && AttackLimb.attack.Retreat)
{
UpdateFallBack(attackWorldPos, deltaTime, false);
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else
{
@@ -1526,7 +1539,7 @@ namespace Barotrauma
}
// When pursuing, we don't want to pursue too close
float max = 300;
float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max;
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
if (!canAttack || distance > margin)
{
// Steer towards the target if in the same room and swimming
@@ -1557,10 +1570,10 @@ namespace Barotrauma
}
else
{
if (AttackingLimb.attack.Ranged)
if (AttackLimb.attack.Ranged)
{
float dir = Character.AnimController.Dir;
if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin)
if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
{
SteeringManager.Reset();
}
@@ -1657,9 +1670,9 @@ namespace Barotrauma
}
break;
case CirclePhase.CloseIn:
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
{
strikeTimer = AttackingLimb.attack.CoolDown;
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
@@ -1702,10 +1715,10 @@ namespace Barotrauma
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
// which makes it continue circling around the point (as supposed)
// But when there is some offset and the offset is too near, this is not what we want.
if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Strike;
strikeTimer = AttackingLimb.attack.CoolDown;
strikeTimer = AttackLimb.attack.CoolDown;
}
else
{
@@ -1740,9 +1753,9 @@ namespace Barotrauma
}
}
}
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
{
strikeTimer = AttackingLimb.attack.CoolDown;
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
canAttack = false;
@@ -1799,7 +1812,7 @@ namespace Barotrauma
}
}
if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100))
if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
@@ -1810,7 +1823,7 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (AttackingLimb.attack.Ranged)
else if (AttackLimb.attack.Ranged)
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
@@ -1823,18 +1836,18 @@ namespace Barotrauma
}
if (canAttack)
{
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb))
{
IgnoreTarget(SelectedAiTarget);
}
}
else if (IsAttackRunning)
{
AttackingLimb.attack.ResetAttackTimer();
AttackLimb.attack.ResetAttackTimer();
}
}
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, IDamageable target)
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, Entity target)
{
if (attackingLimb == null) { return false; }
if (target == null) { return false; }
@@ -1853,10 +1866,11 @@ namespace Barotrauma
// Check that is approximately facing the target
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) { return false; }
float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float angle = VectorExtensions.Angle(forward, toTarget);
if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; }
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
if (angle > attack.RequiredAngle) { return false; }
}
return true;
}
@@ -1866,7 +1880,7 @@ namespace Barotrauma
private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null)
{
var currentContexts = Character.GetAttackContexts();
IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable;
Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity;
if (target == null) { return null; }
Limb selectedLimb = null;
float currentPriority = -1;
@@ -1900,12 +1914,13 @@ namespace Barotrauma
float CalculatePriority(Limb limb, Vector2 attackPos)
{
if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; }
float prio = 1 + limb.attack.Priority;
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
return (1 + limb.attack.Priority) * distanceFactor;
return prio * distanceFactor;
}
}
@@ -1918,7 +1933,7 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
if (Character.Params.CanInteract && attackResult.Damage > 10)
if (attackResult.Damage >= AIParams.DamageThreshold)
{
ReleaseDragTargets();
}
@@ -2004,11 +2019,13 @@ namespace Barotrauma
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine;
if (State == AIState.Attack && !IsAttackRunning && !IsCoolDownRunning)
if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning))
{
// Don't retaliate or escape while performing an attack/under cooldown
retaliate = false;
avoidGunFire = false;
if (IsAttackRunning)
{
avoidGunFire = false;
}
}
if (retaliate)
{
@@ -2021,7 +2038,7 @@ namespace Barotrauma
}
}
}
else if (avoidGunFire)
else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold)
{
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
@@ -2099,15 +2116,11 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new object[]
{
Networking.NetEntityEvent.Type.SetAttackTarget,
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
attackingLimb,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0,
SimPosition.X,
SimPosition.Y
});
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
@@ -2117,7 +2130,7 @@ namespace Barotrauma
{
if (attackingLimb.attack.CoolDownTimer > 0)
{
SetAimTimer();
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))
@@ -2248,19 +2261,19 @@ namespace Barotrauma
// TODO: test adding some random variance here?
attackVector = attackWorldPos - WorldPosition;
}
Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
if (!MathUtils.IsValid(attackDir))
Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
if (!MathUtils.IsValid(dir))
{
attackDir = Vector2.UnitY;
dir = Vector2.UnitY;
}
steeringManager.SteeringManual(deltaTime, attackDir);
if (Character.AnimController.InWater)
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater && !Reverse)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
if (checkBlocking)
{
return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2));
return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2));
}
return true;
}
@@ -2324,7 +2337,7 @@ namespace Barotrauma
if (item.Condition <= 0.0f)
{
if (!wasBroken) { PetBehavior?.OnEat(item); }
Entity.Spawner.AddToRemoveQueue(item);
Entity.Spawner.AddItemToRemoveQueue(item);
}
}
}
@@ -2438,7 +2451,7 @@ namespace Barotrauma
if (targetCharacter == Character) { continue; }
float valueModifier = 1;
string targetingTag = GetTargetingTag(aiTarget);
Identifier targetingTag = GetTargetingTag(aiTarget);
if (targetCharacter != null)
{
// ignore if target is tagged to be explicitly ignored (Feign Death)
@@ -2535,7 +2548,7 @@ namespace Barotrauma
if (s.Submarine == null) { continue; }
if (s.Submarine.Info.IsRuin) { continue; }
bool isCharacterInside = Character.CurrentHull != null;
bool isInnerWall = s.prefab.Tags.Contains("inner");
bool isInnerWall = s.Prefab.Tags.Contains("inner");
if (isInnerWall && !isCharacterInside)
{
// Ignore inner walls when outside (walltargets still work)
@@ -2695,7 +2708,7 @@ namespace Barotrauma
float target = targetParams.Threshold;
if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0)
{
target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin;
target = selectedTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin;
}
if (Character.HealthPercentage > target)
{
@@ -2813,7 +2826,7 @@ namespace Barotrauma
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null)
{
if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep)
if (targetParams.PrioritizeSubCenter || targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep)
{
if (!isAnyTargetClose)
{
@@ -2993,7 +3006,7 @@ namespace Barotrauma
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition;
Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
Vector2 rayEnd = SelectedAiTarget.SimPosition;
@@ -3141,7 +3154,7 @@ namespace Barotrauma
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
if (Character.Submarine == null)
{
if (w.prefab.Tags.Contains("inner"))
if (w.Prefab.Tags.Contains("inner"))
{
if (!Character.AnimController.CanEnterSubmarine) { return false; }
}
@@ -3306,6 +3319,7 @@ namespace Barotrauma
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
{
@@ -3321,10 +3335,13 @@ namespace Barotrauma
inactiveTriggers.Clear();
}
private bool TryResetOriginalState(string tag) =>
TryResetOriginalState(tag.ToIdentifier());
/// <summary>
/// Resets the target's state to the original value defined in the xml.
/// </summary>
private bool TryResetOriginalState(string tag)
private bool TryResetOriginalState(Identifier tag)
{
if (!modifiedParams.ContainsKey(tag)) { return false; }
if (AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
@@ -3344,8 +3361,8 @@ namespace Barotrauma
}
}
private readonly Dictionary<string, CharacterParams.TargetParams> modifiedParams = new Dictionary<string, CharacterParams.TargetParams>();
private readonly Dictionary<string, CharacterParams.TargetParams> tempParams = new Dictionary<string, CharacterParams.TargetParams>();
private readonly Dictionary<Identifier, CharacterParams.TargetParams> modifiedParams = new Dictionary<Identifier, CharacterParams.TargetParams>();
private readonly Dictionary<Identifier, CharacterParams.TargetParams> tempParams = new Dictionary<Identifier, CharacterParams.TargetParams>();
private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null)
{
@@ -3369,6 +3386,9 @@ 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)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
{
@@ -3430,7 +3450,7 @@ namespace Barotrauma
{
ChangeParams("wall", state, priority / 2);
}
if (canAttackDoors)
if (canAttackDoors && IsAggressiveBoarder)
{
ChangeParams("door", state, priority / 2);
}
@@ -3468,7 +3488,7 @@ namespace Barotrauma
disableTailCoroutine = null;
}
Character.AnimController.ReleaseStuckLimbs();
AttackingLimb = null;
AttackLimb = null;
movementMargin = 0;
ResetEscape();
if (isStateChanged && to == AIState.Idle && from != to)
@@ -62,9 +62,9 @@ namespace Barotrauma
private float enemycheckTimer;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders).
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
/// </summary>
public float ReportRange { get; set; }
public float ReportRange { get; set; } = float.PositiveInfinity;
private float _aimSpeed = 1;
public float AimSpeed
@@ -166,7 +166,6 @@ namespace Barotrauma
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
SortTimer = Rand.Range(0f, sortObjectiveInterval);
ReportRange = Character.IsOnPlayerTeam ? float.PositiveInfinity : 1000;
}
public override void Update(float deltaTime)
@@ -569,7 +568,8 @@ namespace Barotrauma
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
@@ -833,10 +833,9 @@ namespace Barotrauma
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
{
if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; }
if (!i.HasAccess(character)) { return 0; }
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.HasAccess(character)) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer();
if (rootContainer?.GetComponent<Fabricator>() != null || rootContainer?.GetComponent<Fabricator>() != null) { return 0; }
@@ -888,21 +887,21 @@ namespace Barotrauma
{
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportintruders");
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
if (target.IsEscorted)
{
if (!Character.IsPrisoner && target.IsPrisoner)
{
string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
Character.Speak(msg, ChatMessageType.Order);
LocalizedString msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", ("[roomname]", targetHull.DisplayName, FormatCapitals.No));
Character.Speak(msg.Value, ChatMessageType.Order);
speak = false;
}
else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable)
{
string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
Character.Speak(msg, ChatMessageType.Order);
LocalizedString msg = TextManager.GetWithVariables("orderdialog.mentalcase", ("[roomname]", targetHull.DisplayName, FormatCapitals.No));
Character.Speak(msg.Value, ChatMessageType.Order);
speak = false;
}
}
@@ -913,14 +912,14 @@ namespace Barotrauma
{
if (AddTargets<AIObjectiveExtinguishFires, Hull>(Character, hull) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportfire");
var orderPrefab = OrderPrefab.Prefabs["reportfire"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
}
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportballastflora");
var orderPrefab = OrderPrefab.Prefabs["reportballastflora"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
@@ -932,7 +931,7 @@ namespace Barotrauma
{
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
{
var orderPrefab = Order.GetPrefab("reportbreach");
var orderPrefab = OrderPrefab.Prefabs["reportbreach"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
@@ -947,7 +946,7 @@ namespace Barotrauma
{
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
var orderPrefab = Order.GetPrefab("requestfirstaid");
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
@@ -961,7 +960,7 @@ namespace Barotrauma
if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
var orderPrefab = OrderPrefab.Prefabs["reportbrokendevices"];
newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
targetHull = hull;
}
@@ -978,15 +977,18 @@ namespace Barotrauma
{
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
minDurationBetweenSimilar: 60.0f);
}
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
#endif
}
}
@@ -1025,17 +1027,17 @@ namespace Barotrauma
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f);
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f);
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f);
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
}
}
@@ -1191,21 +1193,21 @@ namespace Barotrauma
case AIObjectiveCombat.CombatMode.Retreat:
if (Character.IsSecurity)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f);
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
}
else
{
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f);
Character.Speak(TextManager.Get("DialogAttackedByFriendly").Value, null, 0.5f, "attackedbyfriendly".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
}
break;
case AIObjectiveCombat.CombatMode.Offensive:
case AIObjectiveCombat.CombatMode.Arrest:
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f);
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest").Value, null, 0.5f, "attackedbyfriendlysecurityarrest".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
break;
case AIObjectiveCombat.CombatMode.None:
if (Character.IsSecurity && realDamage > 1)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f);
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
}
break;
}
@@ -1263,15 +1265,15 @@ namespace Barotrauma
{
if (!IsFriendly(attacker))
{
if (Character.Submarine == null)
if (c.Submarine == null)
{
// Outside
return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
if (!c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Attacked from an unconnected submarine.
return Character.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
return c.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
}
return c.AIController is HumanAIController humanAI &&
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
@@ -1283,18 +1285,22 @@ namespace Barotrauma
{
cumulativeDamage = 100;
}
if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID)
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
// In multiplayer, they react only to players attacking them or other crew members
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
}
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Outside or attacked from an unconnected submarine -> don't react.
return AIObjectiveCombat.CombatMode.None;
}
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
if (Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
@@ -1350,18 +1356,19 @@ namespace Barotrauma
Character FindInstigator()
{
if (Character.IsInstigator)
if (attacker.IsInstigator)
{
return Character;
return attacker;
}
else if (c.AIController is HumanAIController humanAi)
if (c.IsInstigator)
{
return c;
}
if (c.AIController is HumanAIController humanAi)
{
return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
}
else
{
return null;
}
return null;
}
}
}
@@ -1415,14 +1422,14 @@ namespace Barotrauma
}
}
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak = true)
public void SetOrder(Order order, bool speak = true)
{
objectiveManager.SetOrder(order, option, priority, orderGiver, speak);
objectiveManager.SetOrder(order, speak);
}
public void SetForcedOrder(Order order, string option, Character orderGiver)
public void SetForcedOrder(Order order)
{
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver);
var objective = ObjectiveManager.CreateObjective(order);
ObjectiveManager.SetForcedOrder(objective);
}
@@ -1495,7 +1502,7 @@ namespace Barotrauma
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
@@ -1528,18 +1535,17 @@ namespace Barotrauma
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
/// To solve this, create a copy of the collection or change the code so that you first handle the first items and only after that query for the next items.
/// </summary>
public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable<Item> items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
{
matchingItems.Clear();
items = matchingItems;
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
if (character?.Inventory == null) { return false; }
matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
i.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems;
return matchingItems.Any(i => i != null && (containedTag == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
}
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
@@ -1594,7 +1600,7 @@ namespace Barotrauma
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
}
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning"), null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls", 10.0f);
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
@@ -1674,7 +1680,7 @@ namespace Barotrauma
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
#if CLIENT
HintManager.OnStoleItem(thief, item);
@@ -1749,7 +1755,7 @@ namespace Barotrauma
public static void RefreshTargets(Character character, Order order, Hull hull)
{
switch (order.Identifier)
switch (order.Identifier.Value.ToLowerInvariant())
{
case "reportfire":
AddTargets<AIObjectiveExtinguishFires, Hull>(character, hull);
@@ -269,6 +269,18 @@ namespace Barotrauma
if (!character.AnimController.InWater || character.Submarine != null) { return; }
if (CurrentPath == null || CurrentPath.Unreachable || CurrentPath.Finished) { return; }
if (CurrentPath.CurrentIndex < 0 || CurrentPath.CurrentIndex >= CurrentPath.Nodes.Count - 1) { return; }
var lastNode = CurrentPath.Nodes.Last();
Submarine targetSub = lastNode.Submarine;
if (targetSub != null)
{
float subSize = Math.Max(targetSub.Borders.Size.X, targetSub.Borders.Size.Y) / 2;
float margin = 500;
if (Vector2.DistanceSquared(character.WorldPosition, targetSub.WorldPosition) < MathUtils.Pow2(subSize + margin))
{
// Don't skip nodes when close to the target submarine.
return;
}
}
// Check if we could skip ahead to NextNode when the character is swimming and using waypoints outside.
// Do this to optimize the old path before creating and evaluating a new path.
// In general, this is to avoid behavior where:
@@ -280,7 +292,7 @@ namespace Barotrauma
{
var waypoint = CurrentPath.Nodes[i];
float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition);
if (directDistance > (pathDistance * pathDistance) || Submarine.PickBody(host.SimPosition, waypoint.SimPosition, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
if (directDistance > MathUtils.Pow2(pathDistance) || !character.CanSeeTarget(waypoint))
{
pathDistance -= CurrentPath.GetLength(startIndex: i - 1, endIndex: i);
continue;
@@ -336,6 +348,7 @@ namespace Barotrauma
return Vector2.Zero;
}
Vector2 pos = host.WorldPosition;
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
@@ -346,7 +359,7 @@ namespace Barotrauma
}
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1);
bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1);
if (useLadders && character.SelectedConstruction != ladders.Item)
{
if (character.CanInteractWith(ladders.Item))
@@ -374,21 +387,18 @@ namespace Barotrauma
}
if (character.IsClimbing && useLadders)
{
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent)
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.height / 2 + collider.radius) * 1.25f;
if (heightDiff < colliderSize)
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
if (heightFromFloor <= 0.0f)
{
diff.Y = Math.Max(diff.Y, 100);
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
@@ -400,12 +410,15 @@ namespace Barotrauma
else if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
if (character.SelectedConstruction != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
{
nextLadder.Item.TryInteract(character, forceSelectKey: true);
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
{
NextNode(!doorsChecked);
}
}
}
if (isAboveFloor || nextLadderSameAsCurrent)
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)
{
NextNode(!doorsChecked);
}
@@ -461,12 +474,16 @@ namespace Barotrauma
bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
if (currentPath.CurrentNode.Stairs != null && currentPath.NextNode?.Stairs == null)
if (currentPath.CurrentNode.Stairs != null)
{
margin = 1;
if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
if (!isNextNodeInSameStairs)
{
isTargetTooLow = true;
margin = 1;
if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
{
isTargetTooLow = true;
}
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
@@ -479,7 +496,7 @@ namespace Barotrauma
{
return Vector2.Zero;
}
return ConvertUnits.ToSimUnits(currentPath.CurrentNode.WorldPosition - pos);
return ConvertUnits.ToSimUnits(diff);
}
private void NextNode(bool checkDoors)
@@ -148,14 +148,14 @@ namespace Barotrauma
}
if (TargetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
if (enemyAI.AttackLimb?.attack == null)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
float range = enemyAI.AttackLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
@@ -265,11 +265,11 @@ namespace Barotrauma
if (enemyAI.IsSteeringThroughGap) { break; }
if (_attachPos == Vector2.Zero) { break; }
if (!AttachToSub && !AttachToCharacters) { break; }
if (enemyAI.AttackingLimb == null) { break; }
if (enemyAI.AttackLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
}
@@ -128,7 +128,7 @@ namespace Barotrauma
possibleTarget => HumanAIController.IsActive(possibleTarget) &&
(possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) &&
humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) &&
possibleTarget != character).GetRandom();
possibleTarget != character).GetRandomUnsynced();
if (mentalAttackTarget == null)
{
@@ -154,8 +154,8 @@ namespace Barotrauma
// using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that
mentalBehaviorTimer = MentalBehaviorInterval;
humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f);
string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}";
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f);
Identifier textIdentifier = $"dialogmentalstatereaction{combatMode}".ToIdentifier();
character.Speak(TextManager.Get(textIdentifier).Value, delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f);
if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange))
{
@@ -169,8 +169,8 @@ namespace Barotrauma
public void CreateDialogueBehavior(MentalType mentalType)
{
if (mentalType == MentalType.Normal) { return; }
string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}";
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f);
Identifier textIdentifier = $"dialogmentalstate{mentalType}".ToIdentifier();
character.Speak(TextManager.Get(textIdentifier).Value, delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f);
}
}
}
@@ -3,182 +3,91 @@ using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Immutable;
namespace Barotrauma
{
class NPCConversationCollection : Prefab
{
public static readonly Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
public readonly LanguageIdentifier Language;
public readonly List<NPCConversation> Conversations;
public readonly Dictionary<Identifier, NPCPersonalityTrait> PersonalityTraits;
public NPCConversationCollection(NPCConversationsFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
Language = element.GetAttributeIdentifier("language", "English").ToLanguageIdentifier();
Conversations = new List<NPCConversation>();
PersonalityTraits = new Dictionary<Identifier, NPCPersonalityTrait>();
foreach (var subElement in element.Elements())
{
Identifier elemName = new Identifier(subElement.Name.LocalName);
if (elemName == "Conversation")
{
Conversations.Add(new NPCConversation(subElement));
}
else if (elemName == "PersonalityTrait")
{
var personalityTrait = new NPCPersonalityTrait(subElement);
PersonalityTraits.Add(personalityTrait.Name, personalityTrait);
}
}
}
public override void Dispose() { }
}
class NPCConversation
{
const int MaxPreviousConversations = 20;
private class ConversationCollection
{
public readonly string Identifier;
public readonly Dictionary<string, List<NPCConversation>> Conversations;
public ConversationCollection(string identifier)
{
Identifier = identifier;
Conversations = new Dictionary<string, List<NPCConversation>>();
}
public void Add(string language, string filePath, XElement subElement)
{
if (!Conversations.ContainsKey(language))
{
Conversations.Add(language, new List<NPCConversation>());
}
Conversations[language].Add(new NPCConversation(subElement, filePath));
}
public void RemoveByFile(string filePath)
{
List<string> keysToRemove = new List<string>();
foreach (var kpv in Conversations)
{
kpv.Value.RemoveAll(c => c.FilePath == filePath);
if (kpv.Value.Count == 0) { keysToRemove.Add(kpv.Key); }
}
foreach (var key in keysToRemove)
{
Conversations.Remove(key);
}
}
}
private static Dictionary<string, ConversationCollection> allConversations = new Dictionary<string, ConversationCollection>();
public readonly string FilePath;
public readonly string Line;
public readonly List<JobPrefab> AllowedJobs;
public readonly ImmutableHashSet<Identifier> AllowedJobs;
public readonly List<string> Flags;
public readonly ImmutableHashSet<Identifier> Flags;
//The line can only be selected when eventmanager intensity is between these values
//null = no restriction
public float? maxIntensity, minIntensity;
public readonly float? maxIntensity, minIntensity;
public readonly List<NPCConversation> Responses;
public readonly ImmutableArray<NPCConversation> Responses;
private readonly int speakerIndex;
private readonly List<string> allowedSpeakerTags;
private readonly ImmutableHashSet<Identifier> allowedSpeakerTags;
private readonly bool requireNextLine;
// used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere)
private readonly bool requireSight;
public static void LoadAll(IEnumerable<ContentFile> files)
public NPCConversation(XElement element)
{
foreach (var file in files)
{
if (Path.GetExtension(file.Path) == ".csv") continue; // .csv files are not supported
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Conversations file '{file.Path}' has no identifier!");
return;
}
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conversation":
if (!allConversations.ContainsKey(identifier))
{
allConversations.Add(identifier, new ConversationCollection(identifier));
}
allConversations[identifier].Add(language, file.Path, subElement);
break;
case "personalitytrait":
new NPCPersonalityTrait(subElement, file.Path);
break;
}
}
}
public static void RemoveByFile(string filePath)
{
List<string> keysToRemove = new List<string>();
foreach (var kpv in allConversations)
{
kpv.Value.RemoveByFile(filePath);
if (!kpv.Value.Conversations.Any())
{
keysToRemove.Add(kpv.Key);
}
}
foreach (string key in keysToRemove)
{
allConversations.Remove(key);
}
NPCPersonalityTrait.List.RemoveAll(npt => npt.FilePath == filePath);
}
public NPCConversation(XElement element, string filePath)
{
FilePath = filePath;
Line = element.GetAttributeString("line", "");
speakerIndex = element.GetAttributeInt("speaker", 0);
AllowedJobs = new List<JobPrefab>();
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
string key = allowedJobIdentifier.ToLowerInvariant();
if (JobPrefab.Prefabs.ContainsKey(key))
{
AllowedJobs.Add(JobPrefab.Prefabs[key]);
}
}
Flags = new List<string>(element.GetAttributeStringArray("flags", new string[0]));
allowedSpeakerTags = new List<string>();
string allowedSpeakerTagsStr = element.GetAttributeString("speakertags", "");
foreach (string tag in allowedSpeakerTagsStr.Split(','))
{
if (string.IsNullOrEmpty(tag)) continue;
allowedSpeakerTags.Add(tag.Trim().ToLowerInvariant());
}
AllowedJobs = element.GetAttributeIdentifierArray("allowedjobs", Array.Empty<Identifier>()).ToImmutableHashSet();
Flags = element.GetAttributeIdentifierArray("flags", Array.Empty<Identifier>()).ToImmutableHashSet();
allowedSpeakerTags = element.GetAttributeIdentifierArray("speakertags", Array.Empty<Identifier>()).ToImmutableHashSet();
if (element.Attribute("minintensity") != null) minIntensity = element.GetAttributeFloat("minintensity", 0.0f);
if (element.Attribute("maxintensity") != null) maxIntensity = element.GetAttributeFloat("maxintensity", 1.0f);
Responses = new List<NPCConversation>();
foreach (XElement subElement in element.Elements())
{
Responses.Add(new NPCConversation(subElement, filePath));
}
Responses = element.Elements().Select(s => new NPCConversation(s)).ToImmutableArray();
requireNextLine = element.GetAttributeBool("requirenextline", false);
requireSight = element.GetAttributeBool("requiresight", false);
}
private static List<string> GetCurrentFlags(Character speaker)
private static List<Identifier> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<string>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep"); }
var currentFlags = new List<Identifier>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); }
if (GameMain.GameSession != null && Level.Loaded != null)
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial"); }
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
@@ -187,30 +96,30 @@ namespace Barotrauma
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost");
currentFlags.Add("EnterOutpost".ToIdentifier());
}
}
if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
{
currentFlags.Add("Casual");
currentFlags.Add("Casual".ToIdentifier());
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
currentFlags.Add("InRadiation");
currentFlags.Add("InRadiation".ToIdentifier());
}
}
if (speaker != null)
{
if (speaker.AnimController.InWater) { currentFlags.Add("Underwater"); }
currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside");
if (speaker.AnimController.InWater) { currentFlags.Add("Underwater".ToIdentifier()); }
currentFlags.Add((speaker.CurrentHull == null ? "Outside" : "Inside").ToIdentifier());
if (Character.Controlled != null)
{
if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null)
{
currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf");
currentFlags.Add((speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf").ToIdentifier());
}
}
@@ -218,7 +127,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
}
@@ -226,27 +135,27 @@ namespace Barotrauma
if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
{
currentFlags.Add("OutpostNPC");
currentFlags.Add("OutpostNPC".ToIdentifier());
}
if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None)
{
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
currentFlags.Add($"CampaignNPC.{speaker.CampaignInteractionType}".ToIdentifier());
}
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
(campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false))
(campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned"))
{
if (speaker.TeamID == CharacterTeamType.None)
{
currentFlags.Add("Bandit");
currentFlags.Add("Bandit".ToIdentifier());
}
else if (speaker.TeamID == CharacterTeamType.FriendlyNPC)
{
currentFlags.Add("Hostage");
currentFlags.Add("Hostage".ToIdentifier());
}
}
if (speaker.IsEscorted)
{
currentFlags.Add("escort");
currentFlags.Add("escort".ToIdentifier());
}
}
@@ -261,16 +170,16 @@ namespace Barotrauma
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
CreateConversation(availableSpeakers, assignedSpeakers, null, lines,
availableConversations: allConversations.Values.SelectMany(cc => cc.Conversations.Where(kpv => kpv.Key == TextManager.Language).SelectMany(kpv => kpv.Value)).ToList());
availableConversations: NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].SelectMany(cc => cc.Conversations).ToList());
return lines;
}
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, IEnumerable<string> requiredFlags)
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, IEnumerable<Identifier> requiredFlags)
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
var availableConversations = allConversations.Values.SelectMany(cc => cc.Conversations.SelectMany(
kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList();
var availableConversations = NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language]
.SelectMany(cc => cc.Conversations.Where(c => requiredFlags.All(f => c.Flags.Contains(f)))).ToList();
if (availableConversations.Count > 0)
{
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: false);
@@ -282,11 +191,11 @@ namespace Barotrauma
List<Character> availableSpeakers,
Dictionary<int, Character> assignedSpeakers,
NPCConversation baseConversation,
List<Pair<Character, string>> lineList,
List<NPCConversation> availableConversations,
IList<Pair<Character, string>> lineList,
IList<NPCConversation> availableConversations,
bool ignoreFlags = false)
{
List<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
IList<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
if (conversations.Count == 0) { return; }
int conversationIndex = Rand.Int(conversations.Count);
@@ -390,7 +299,8 @@ namespace Barotrauma
//check if the character has an appropriate job to say the line
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0)
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; }
if (!(potentialSpeaker.Info?.Job?.Prefab is { } speakerJobPrefab)
|| !selectedConversation.AllowedJobs.Contains(speakerJobPrefab.Identifier)) { return false; }
}
//check if the character has all required flags to say the line
@@ -450,17 +360,13 @@ namespace Barotrauma
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (string key in allConversations.Keys)
foreach (Identifier identifier in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].Keys)
{
foreach (string lang in allConversations[key].Conversations.Keys)
foreach (var current in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language][identifier].Conversations)
{
if (lang != TextManager.Language) { continue; }
foreach (var current in allConversations[key].Conversations[lang])
{
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
}
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
}
}
@@ -480,15 +386,7 @@ namespace Barotrauma
sb.Append(string.Join(",", conv.Flags)); // Flags
sb.Append('*');
for (int i = 0; i < conv.AllowedJobs.Count; i++) // Jobs
{
sb.Append(conv.AllowedJobs[i].Identifier);
if (i < conv.AllowedJobs.Count - 1)
{
sb.Append(",");
}
}
sb.Append(string.Join(',', conv.AllowedJobs));
sb.Append('*');
sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits
@@ -501,13 +399,13 @@ namespace Barotrauma
sb.AppendLine();
}
private static void WriteSubConversations(System.Text.StringBuilder sb, List<NPCConversation> responses, int depthIndex)
private static void WriteSubConversations(System.Text.StringBuilder sb, IList<NPCConversation> responses, int depthIndex)
{
for (int i = 0; i < responses.Count; i++)
{
WriteConversation(sb, responses[i], depthIndex);
if (responses[i].Responses != null && responses[i].Responses.Count > 0)
if (responses[i].Responses != null && responses[i].Responses.Length > 0)
{
WriteSubConversations(sb, responses[i].Responses, depthIndex + 1);
}
@@ -10,8 +10,8 @@ namespace Barotrauma
{
public virtual float Devotion => AIObjectiveManager.baseDevotion;
public abstract string Identifier { get; set; }
public virtual string DebugTag => Identifier;
public abstract Identifier Identifier { get; set; }
public virtual string DebugTag => Identifier.Value;
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
@@ -83,7 +83,7 @@ namespace Barotrauma
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
public string Option { get; private set; }
public readonly Identifier Option;
private bool _abandon;
public bool Abandon
@@ -157,11 +157,11 @@ namespace Barotrauma
return subObjective == null ? this : subObjective.GetActiveObjective();
}
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default)
{
this.objectiveManager = objectiveManager;
this.character = character;
Option = option ?? string.Empty;
Option = option;
PriorityModifier = priorityModifier;
}
@@ -9,11 +9,11 @@ namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
public override string Identifier { get; set; } = "charge batteries";
public override Identifier Identifier { get; set; } = "charge batteries".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
private IEnumerable<PowerContainer> batteryList;
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
protected override bool Filter(PowerContainer battery)
@@ -30,6 +30,7 @@ namespace Barotrauma
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
if (item.ConditionPercentage <= 0) { return false; }
if (item.IsClaimedByBallastFlora) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(battery)) { return false; }
return true;
@@ -55,7 +56,7 @@ namespace Barotrauma
{
if (character == null || character.Submarine == null)
{
return new PowerContainer[0];
return Array.Empty<PowerContainer>();
}
batteryList = character.Submarine.GetItems(true).Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveCleanupItem : AIObjective
{
public override string Identifier { get; set; } = "cleanup item";
public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
@@ -61,21 +61,6 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (item.IgnoreByAI(character))
{
Abandon = true;
return;
}
if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
{
// Target was picked up or moved by someone.
Abandon = true;
return;
}
}
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
@@ -133,7 +118,24 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveSpecific()
{
if (item.IgnoreByAI(character))
{
Abandon = true;
return false;
}
if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
{
// Target was picked up or moved by someone.
Abandon = true;
return false;
}
}
return IsCompleted;
}
public override void Reset()
{
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveCleanupItems : AIObjectiveLoop<Item>
{
public override string Identifier { get; set; } = "cleanup items";
public override Identifier Identifier { get; set; } = "cleanup items".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
protected override bool ForceOrderPriority => false;
@@ -79,18 +79,17 @@ namespace Barotrauma
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
!container.IgnoreByAI(character) &&
container.IsInteractable(character) &&
container.HasAccess(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() is ItemContainer itemContainer && itemContainer.HasAccess(character) &&
IsItemInsideValidSubmarine(container, character);
container.GetComponent<ItemContainer>() != null &&
IsItemInsideValidSubmarine(container, character) &&
!container.IsClaimedByBallastFlora;
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (!item.HasAccess(character)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -102,6 +101,7 @@ namespace Barotrauma
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (pickable is Holdable h && h.Attachable && h.Attached) { return false; }
@@ -10,7 +10,7 @@ namespace Barotrauma
{
class AIObjectiveCombat : AIObjective
{
public override string Identifier { get; set; } = "combat";
public override Identifier Identifier { get; set; } = "combat".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
@@ -212,10 +212,14 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (sqrDistance > maxDistance * maxDistance)
if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
{
// The target escaped from us.
return true;
// Can't lose the target in friendly outposts.
if (sqrDistance > maxDistance * maxDistance)
{
// The target escaped from us.
return true;
}
}
return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
}
@@ -250,17 +254,17 @@ namespace Barotrauma
case CombatMode.Offensive:
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
{
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true))
if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true))
{
IsCompleted = true;
}
else if (Enemy.IsKnockedDown &&
!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() &&
!HumanAIController.HasItem(character, "handlocker", out _, requireEquipped: false))
!HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false))
{
IsCompleted = true;
}
@@ -399,7 +403,7 @@ namespace Barotrauma
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekWeaponObjective,
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
EvaluateCombatPriority = false, // Use a custom formula instead
@@ -636,7 +640,7 @@ namespace Barotrauma
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = "mobilebattery";
var mobileBatteryTag = "mobilebattery".ToIdentifier();
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
@@ -777,7 +781,7 @@ namespace Barotrauma
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true)
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
{
UsePathingOutside = false
},
@@ -848,7 +852,7 @@ namespace Barotrauma
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
{
if (HumanAIController.HasItem(character, "handlocker", out _))
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _))
{
if (!arrestingRegistered)
{
@@ -861,10 +865,10 @@ namespace Barotrauma
{
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
if (prefab != null)
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
}
}
RemoveFollowTarget();
@@ -914,7 +918,7 @@ namespace Barotrauma
}
}
}
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
@@ -928,7 +932,7 @@ namespace Barotrauma
return;
}
}
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
character.Speak(TextManager.Get("DialogTargetArrested").Value, null, 3.0f, "targetarrested".ToIdentifier(), 30.0f);
}
if (!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
{
@@ -939,7 +943,7 @@ namespace Barotrauma
/// <summary>
/// Seeks for more ammunition. Creates a new subobjective.
/// </summary>
private void SeekAmmunition(string[] ammunitionIdentifiers)
private void SeekAmmunition(Identifier[] ammunitionIdentifiers)
{
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
@@ -974,7 +978,7 @@ namespace Barotrauma
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
Identifier[] ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
@@ -1212,17 +1216,17 @@ namespace Barotrauma
retreatTarget = null;
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20);
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(string textIdentifier, float delay, float minDuration)
private void Speak(Identifier textIdentifier, float delay, float minDuration)
{
if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
{
string msg = TextManager.Get(textIdentifier, true);
if (msg != null)
LocalizedString msg = TextManager.Get(textIdentifier);
if (!msg.IsNullOrEmpty())
{
character.Speak(msg, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
}
}
}
@@ -7,18 +7,18 @@ namespace Barotrauma
{
class AIObjectiveContainItem: AIObjective
{
public override string Identifier { get; set; } = "contain item";
public override Identifier Identifier { get; set; } = "contain item".ToIdentifier();
public Func<Item, float> GetItemPriority;
public string[] ignoredContainerIdentifiers;
public Identifier[] ignoredContainerIdentifiers;
public bool checkInventory = true;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates)
private readonly bool spawnItemIfNotFound;
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly Identifier[] itemIdentifiers;
public readonly ItemContainer container;
private readonly Item item;
public Item ItemToContain { get; private set; }
@@ -60,25 +60,21 @@ namespace Barotrauma
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveContainItem(Character character, Identifier itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new Identifier[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
public AIObjectiveContainItem(Character character, Identifier[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
}
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character)))
if (container?.Item == null || !container.Item.HasAccess(character))
{
Abandon = true;
return false;
@@ -89,23 +85,28 @@ namespace Barotrauma
}
else
{
int containedItemCount = 0;
foreach (Item it in container.Inventory.AllItems)
{
if (CheckItem(it))
{
containedItemCount++;
}
}
return containedItemCount >= ItemCount;
return CountItems();
}
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character);
private bool CountItems()
{
int containedItemCount = 0;
foreach (Item it in container.Inventory.AllItems)
{
if (CheckItem(it))
{
containedItemCount++;
}
}
return containedItemCount >= ItemCount;
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && i.HasAccess(character);
protected override void Act(float deltaTime)
{
if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character))
if (container?.Item == null)
{
Abandon = true;
return;
@@ -141,8 +142,8 @@ namespace Barotrauma
container.Inventory.TryPutItem(item, null);
}
}
IsCompleted = true;
}
IsCompleted = item != null || CountItems();
}
else
{
@@ -159,7 +160,7 @@ namespace Barotrauma
{
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveDecontainItem : AIObjective
{
public override string Identifier { get; set; } = "decontain item";
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
public Func<Item, float> GetItemPriority;
@@ -127,7 +127,7 @@ namespace Barotrauma
RemoveExistingPredicate = RemoveExistingPredicate,
RemoveMax = RemoveExistingMax,
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
ignoredContainerIdentifiers = sourceContainer != null ? new Identifier[] { sourceContainer.Item.Prefab.Identifier } : null
},
onCompleted: () => IsCompleted = true,
onAbandon: () => Abandon = true);
@@ -6,7 +6,7 @@ namespace Barotrauma
class AIObjectiveEscapeHandcuffs : AIObjective
{
// Used for prisoner escorts to allow them to escape their binds
public override string Identifier { get; set; } = "escape handcuffs";
public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
@@ -88,7 +88,7 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag("handlocker");
Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier());
if (handcuffs != null)
{
handcuffs.Drop(character);
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
public override string Identifier { get; set; } = "extinguish fire";
public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier();
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
@@ -77,16 +77,16 @@ namespace Barotrauma
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher");
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier());
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false))
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false))
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f);
}
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true)
{
AllowStealing = true,
// If the item is inside an unsafe hull, decrease the priority
@@ -94,7 +94,7 @@ namespace Barotrauma
};
if (objectiveManager.HasOrder<AIObjectiveExtinguishFires>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f);
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher").Value, null, 0.0f, "dialogcannotfindfireextinguisher".ToIdentifier(), 10.0f);
};
return getItemObjective;
});
@@ -112,6 +112,13 @@ namespace Barotrauma
}
foreach (FireSource fs in targetHull.FireSources)
{
if (fs == null) { continue; }
if (fs.Removed) { continue; }
if (character.CurrentHull == null)
{
Abandon = true;
break;
}
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
bool inRange = xDist + yDist < extinguisher.Range;
@@ -139,7 +146,7 @@ namespace Barotrauma
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
}
}
if (move)
@@ -147,13 +154,13 @@ namespace Barotrauma
//go to the first firesource
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f))
{
DialogueIdentifier = "dialogcannotreachfire",
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull);
gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull);
}
}
else
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
{
public override string Identifier { get; set; } = "extinguish fires";
public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier();
public override bool ForceRun => true;
public override bool AllowInAnySub => true;
@@ -27,7 +27,7 @@ namespace Barotrauma
/// </summary>
public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 500, hull.FireSources.Sum(fs => fs.Size.X)));
protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override IEnumerable<Hull> GetList() => Hull.HullList;
protected override AIObjective ObjectiveConstructor(Hull target)
=> new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier);
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
{
public override string Identifier { get; set; } = "fight intruders";
public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier();
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
@@ -45,9 +45,9 @@ namespace Barotrauma
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase));
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f);
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
}
return combatObjective;
@@ -7,13 +7,13 @@ namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string Identifier { get; set; } = "find diving gear";
public override Identifier Identifier { get; set; } = "find diving gear".ToIdentifier();
public override string DebugTag => $"{Identifier} ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly string gearTag;
private readonly Identifier gearTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
@@ -21,13 +21,13 @@ namespace Barotrauma
public const float MIN_OXYGEN = 10;
public const string HEAVY_DIVING_GEAR = "deepdiving";
public const string LIGHT_DIVING_GEAR = "lightdiving";
public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier();
public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier();
/// <summary>
/// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear)
/// </summary>
public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors";
public const string OXYGEN_SOURCE = "oxygensource";
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
@@ -54,7 +54,7 @@ namespace Barotrauma
{
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
@@ -92,15 +92,15 @@ namespace Barotrauma
{
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f);
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1)
{
character.Speak(TextManager.Get("dialoglastoxygentank"), null, 0.0f, "dialoglastoxygentank", 30.0f);
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
}
}
else
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f);
}
}
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
@@ -130,7 +130,7 @@ namespace Barotrauma
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f);
character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f);
}
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
@@ -147,11 +147,11 @@ namespace Barotrauma
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f);
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
}
else if (remainingOxygenTanks < 10)
{
character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f);
character.Speak(TextManager.Get("DialogLowOnOxygenTanks").Value, null, 0.0f, "lowonoxygentanks".ToIdentifier(), 30.0f);
}
return remainingOxygenTanks;
}
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveFindSafety : AIObjective
{
public override string Identifier { get; set; } = "find safety";
public override Identifier Identifier { get; set; } = "find safety".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
@@ -317,7 +317,7 @@ namespace Barotrauma
Hull bestHull = null;
float bestValue = 0;
bool bestIsAirlock = false;
foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h)))
foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h)))
{
if (hull.Submarine == null) { continue; }
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
@@ -342,7 +342,7 @@ namespace Barotrauma
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
//avoid airlock modules if not allowed to change the sub
if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t.Equals("airlock", StringComparison.OrdinalIgnoreCase)))
if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock"))
{
continue;
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveFixLeak : AIObjective
{
public override string Identifier { get; set; } = "fix leak";
public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
@@ -64,15 +64,15 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
{
character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f);
character.Speak(TextManager.Get("dialogcannotfindweldingequipment").Value, null, 0.0f, "dialogcannotfindweldingequipment".ToIdentifier(), 10.0f);
}
Abandon = true;
},
@@ -91,7 +91,7 @@ namespace Barotrauma
}
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
@@ -112,11 +112,11 @@ namespace Barotrauma
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f);
character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f);
}
else if (remainingOxygenTanks < 4)
{
character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f);
character.Speak(TextManager.Get("DialogLowOnWeldingFuel").Value, null, 0.0f, "lowonweldingfuel".ToIdentifier(), 30.0f);
}
}
return;
@@ -142,7 +142,7 @@ namespace Barotrauma
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak),
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
@@ -160,10 +160,11 @@ namespace Barotrauma
{
UseDistanceRelativeToAimSourcePos = true,
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty,
TargetName = Leak.FlowTargetHull?.DisplayName,
CheckVisibility = false,
requiredCondition = () => Leak.Submarine == character.Submarine,
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h),
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{
public override string Identifier { get; set; } = "fix leaks";
public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveGetItem : AIObjective
{
public override string Identifier { get; set; } = "get item";
public override Identifier Identifier { get; set; } = "get item".ToIdentifier();
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowMultipleInstances => true;
@@ -21,7 +21,7 @@ namespace Barotrauma
public float TargetCondition { get; set; } = 1;
public bool AllowDangerousPressure { get; set; }
public readonly ImmutableArray<string> IdentifiersOrTags;
public readonly ImmutableArray<Identifier> IdentifiersOrTags;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
@@ -32,8 +32,8 @@ namespace Barotrauma
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
public string[] ignoredIdentifiersOrTags;
public Identifier[] ignoredContainerIdentifiers;
public Identifier[] ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private readonly bool checkInventory;
@@ -83,10 +83,10 @@ namespace Barotrauma
moveToTarget = targetItem?.GetRootInventoryOwner();
}
public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, Identifier identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new Identifier[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, IEnumerable<string> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
public AIObjectiveGetItem(Character character, IEnumerable<Identifier> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
@@ -97,27 +97,27 @@ namespace Barotrauma
ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray();
}
public static IEnumerable<string> ParseGearTags(IEnumerable<string> identifiersOrTags)
public static IEnumerable<Identifier> ParseGearTags(IEnumerable<Identifier> identifiersOrTags)
{
var tags = new List<string>();
foreach (string tag in identifiersOrTags)
var tags = new List<Identifier>();
foreach (Identifier tag in identifiersOrTags)
{
if (!tag.Contains('!'))
if (!tag.Contains("!"))
{
tags.Add(tag.ToLowerInvariant());
tags.Add(tag);
}
}
return tags;
}
public static IEnumerable<string> ParseIgnoredTags(IEnumerable<string> identifiersOrTags)
public static IEnumerable<Identifier> ParseIgnoredTags(IEnumerable<Identifier> identifiersOrTags)
{
var ignoredTags = new List<string>();
foreach (string tag in identifiersOrTags)
var ignoredTags = new List<Identifier>();
foreach (Identifier tag in identifiersOrTags)
{
if (tag.Contains('!'))
if (tag.Contains("!"))
{
ignoredTags.Add(tag.Remove("!").ToLowerInvariant());
ignoredTags.Add(tag.Remove("!"));
}
}
return ignoredTags;
@@ -177,7 +177,7 @@ namespace Barotrauma
if (dangerousPressure)
{
#if DEBUG
string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault();
string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault().Value;
DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow);
#endif
Abandon = true;
@@ -480,7 +480,7 @@ namespace Barotrauma
}
else
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
@@ -528,14 +528,13 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; }
if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id));
}
public override void Reset()
@@ -575,9 +574,9 @@ namespace Barotrauma
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; }
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg == null) { return; }
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
LocalizedString msg = TextManager.Get(CannotFindDialogueIdentifierOverride, "dialogcannotfinditem");
if (msg.IsNullOrEmpty() || !msg.Loaded) { return; }
character.Speak(msg.Value, identifier: "dialogcannotfinditem".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
}
}
}
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveGetItems : AIObjective
{
public override string Identifier { get; set; } = "get items";
public override Identifier Identifier { get; set; } = "get items".ToIdentifier();
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool AllowMultipleInstances => true;
@@ -24,13 +24,13 @@ namespace Barotrauma
public bool RequireLoaded { get; set; }
public bool RequireAllItems { get; set; }
private readonly ImmutableArray<string> gearTags;
private readonly string[] ignoredTags;
private readonly ImmutableArray<Identifier> gearTags;
private readonly Identifier[] ignoredTags;
private bool subObjectivesCreated;
public readonly HashSet<Item> achievedItems = new HashSet<Item>();
public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<Identifier> identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray();
@@ -47,7 +47,7 @@ namespace Barotrauma
}
if (!subObjectivesCreated)
{
foreach (string tag in gearTags)
foreach (Identifier tag in gearTags)
{
if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; }
int count = gearTags.Count(t => t == tag);
@@ -8,7 +8,9 @@ namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
public override string Identifier { get; set; } = "go to";
public override Identifier Identifier { get; set; } = "go to".ToIdentifier();
public override bool KeepDivingGearOn => GetTargetHull() == null;
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
@@ -73,14 +75,6 @@ namespace Barotrauma
_closeEnough = Math.Max(minDistance, value);
}
}
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
// NOTE: This seemes to have caused an issue now Regalis11/Barotrauma#8067: namely, the bot was trying to use a waypoint that was obstructed by a shuttle
// because obstruction was only checked when checking visibility in PathFinder. Changed that so that obstructed nodes are no longer used.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -96,8 +90,8 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget";
public string TargetName { get; set; }
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
public LocalizedString TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
@@ -180,9 +174,11 @@ namespace Barotrauma
if (DialogueIdentifier == null) { return; }
if (!SpeakIfFails) { return; }
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg == null) { return; }
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
LocalizedString msg = TargetName == null ?
TextManager.Get(DialogueIdentifier) :
TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes);
if (msg.IsNullOrEmpty() || !msg.Loaded) { return; }
character.Speak(msg.Value, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
public void ForceAct(float deltaTime) => Act(deltaTime);
@@ -265,15 +261,15 @@ namespace Barotrauma
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit));
if (Mimic)
{
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir)
else if (HumanAIController.HasDivingMask(followTarget))
{
needsDivingGear = true;
}
@@ -382,13 +378,23 @@ namespace Barotrauma
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
string scooterTag = "scooter";
string batteryTag = "mobilebattery";
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!Mimic ||
(targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
if (isInside)
{
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
@@ -424,8 +430,7 @@ namespace Barotrauma
}
}
}
bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter);
if (scooter != null && isScooterEquipped)
if (scooter != null && character.HasEquippedItem(scooter))
{
if (shouldUseScooter)
{
@@ -493,7 +498,7 @@ namespace Barotrauma
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: CheckVisibility);
checkVisiblity: Target is Item || Target is Character);
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
@@ -534,6 +539,7 @@ namespace Barotrauma
void UseScooter(Vector2 targetWorldPos)
{
if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
SteeringManager.Reset();
character.CursorPosition = targetWorldPos;
if (character.Submarine != null)
@@ -542,19 +548,26 @@ namespace Barotrauma
}
Vector2 diff = character.CursorPosition - character.Position;
Vector2 dir = Vector2.Normalize(diff);
float sqrDist = diff.LengthSquared();
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
if (character.CurrentHull == null && IsFollowOrderObjective)
{
SteeringManager.SteeringManual(1.0f, dir);
}
else
{
float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2));
bool isFacing = dot > 0.9f;
if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough))
float sqrDist = diff.LengthSquared();
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
{
SteeringManager.SteeringManual(1.0f, dir);
}
else
{
float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2));
bool isFacing = dot > 0.9f;
if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough))
{
SteeringManager.SteeringManual(1.0f, dir);
}
}
}
else
{
SteeringManager.SteeringManual(1.0f, dir);
}
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Shoot, false, true);
@@ -10,7 +10,7 @@ namespace Barotrauma
{
class AIObjectiveIdle : AIObjective
{
public override string Identifier { get; set; } = "idle";
public override Identifier Identifier { get; set; } = "idle".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowInAnySub => true;
@@ -93,7 +93,7 @@ namespace Barotrauma
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
public readonly HashSet<string> PreferredOutpostModuleTypes = new HashSet<string>();
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
public void CalculatePriority(float max = 0)
{
@@ -391,7 +391,7 @@ namespace Barotrauma
{
targetHulls.Clear();
hullWeights.Clear();
foreach (var hull in Hull.hullList)
foreach (var hull in Hull.HullList)
{
if (character.Submarine == null) { break; }
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
@@ -10,7 +10,7 @@ namespace Barotrauma
{
class AIObjectiveLoadItem : AIObjective
{
public override string Identifier { get; set; } = "load item";
public override Identifier Identifier { get; set; } = "load item".ToIdentifier();
public override bool IsLoop
{
get => true;
@@ -20,9 +20,9 @@ namespace Barotrauma
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
private Item Container { get; }
private ItemContainer ItemContainer { get; }
private ImmutableArray<string> TargetContainerTags { get; }
private ImmutableHashSet<string> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<string>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<string>>();
private ImmutableArray<Identifier> TargetContainerTags { get; }
private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
private int itemIndex = 0;
private AIObjectiveDecontainItem decontainObjective;
@@ -30,7 +30,7 @@ namespace Barotrauma
private Item targetItem;
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
public AIObjectiveLoadItem(Item container, ImmutableArray<string> targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, string option, Character character, AIObjectiveManager objectiveManager, float priorityModifier)
public AIObjectiveLoadItem(Item container, ImmutableArray<Identifier> targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, Identifier option, Character character, AIObjectiveManager objectiveManager, float priorityModifier)
: base(character, objectiveManager, priorityModifier)
{
Container = container;
@@ -42,7 +42,7 @@ namespace Barotrauma
}
TargetContainerTags = targetTags;
TargetItemCondition = targetCondition;
if (!string.IsNullOrEmpty(option))
if (!option.IsEmpty)
{
string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}";
if (TextManager.ContainsTag(optionSpecificDialogueIdentifier))
@@ -63,7 +63,7 @@ namespace Barotrauma
private enum CheckStatus { Unfinished, Finished }
private ImmutableHashSet<string> GetValidContainableItemIdentifiers()
private ImmutableHashSet<Identifier> GetValidContainableItemIdentifiers()
{
if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers))
{
@@ -75,7 +75,7 @@ namespace Barotrauma
var potentialContainablePrefabs = MapEntityPrefab.List
.Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i)))
.Cast<ItemPrefab>();
var validContainableItemIdentifiers = new HashSet<string>();
var validContainableItemIdentifiers = new HashSet<Identifier>();
foreach (var component in Container.Components)
{
if (CheckComponent() == CheckStatus.Finished)
@@ -125,7 +125,7 @@ namespace Barotrauma
useDefaultContainableItemIdentifiers = false;
if (statusEffect.TargetIdentifiers != null)
{
foreach (string target in statusEffect.TargetIdentifiers)
foreach (Identifier target in statusEffect.TargetIdentifiers)
{
foreach (var prefab in potentialContainablePrefabs)
{
@@ -308,11 +308,9 @@ namespace Barotrauma
if (rootInventoryOwner is Item parentItem)
{
if (parentItem.HasTag("donttakeitems")) { return false; }
if (!(parentItem.GetComponent<ItemContainer>()?.HasAccess(character) ?? true)) { return false; }
}
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item)) { return false; }
if (!ItemContainer.HasAccess(character)) { return false; }
if (!ItemContainer.CanBeContained(item)) { return false; }
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
@@ -9,11 +9,11 @@ namespace Barotrauma
{
class AIObjectiveLoadItems : AIObjectiveLoop<Item>
{
public override string Identifier { get; set; } = "load items";
public override Identifier Identifier { get; set; } = "load items".ToIdentifier();
protected override float IgnoreListClearInterval => 20.0f;
protected override bool ResetWhenClearingIgnoreList => false;
private ImmutableArray<string> TargetContainerTags { get; }
private ImmutableArray<Identifier> TargetContainerTags { get; }
private List<Item> TargetContainers { get; } = new List<Item>();
private ItemCondition TargetCondition { get; }
@@ -23,7 +23,7 @@ namespace Barotrauma
Full
}
public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, string option, ImmutableArray<string> containerTags, Item targetContainer = null, float priorityModifier = 1)
public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, Identifier option, ImmutableArray<Identifier> containerTags, Item targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option)
{
if ((containerTags == null || containerTags.None()) && targetContainer == null)
@@ -50,19 +50,18 @@ namespace Barotrauma
return true;
}
public static bool IsValidTarget(Item item, Character character, ImmutableArray<string>? targetContainerTags = null, ItemCondition? targetCondition = null)
public static bool IsValidTarget(Item item, Character character, ImmutableArray<Identifier>? targetContainerTags = null, ItemCondition? targetCondition = null)
{
if (item == null) { return false; }
if (item.Removed) { return false; }
if (targetContainerTags.HasValue && !Order.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; }
if (container.Inventory == null) { return false; }
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (!container.HasAccess(character)) { return false; }
if (item.IsClaimedByBallastFlora) { return false; }
if (!item.HasAccess(character)) { return false; }
// Ignore items that require power but don't have it
if (item.GetComponent<Powered>() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; }
return true;
@@ -36,7 +36,7 @@ namespace Barotrauma
return false;
}
public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default)
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
@@ -10,6 +10,16 @@ namespace Barotrauma
{
class AIObjectiveManager
{
public enum ObjectiveType
{
None = 0,
Order = 1,
Objective = 2,
MinValue = 0,
MaxValue = 2
}
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
@@ -38,7 +48,7 @@ namespace Barotrauma
}
}
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
public List<Order> CurrentOrders { get; } = new List<Order>();
/// <summary>
/// The AIObjective in <see cref="CurrentOrders"/> with the highest <see cref="AIObjective.Priority"/>
/// </summary>
@@ -132,23 +142,23 @@ namespace Barotrauma
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier];
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); }
Item item = null;
if (orderPrefab.MustSetTarget)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character, orderOption: autonomousObjective.option)?.GetRandom();
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
}
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.ignoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier);
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
@@ -193,28 +203,20 @@ namespace Barotrauma
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority;
if (currentObjectiveIsOrder)
CurrentObjective = currentObjectiveIsOrder ? CurrentOrder : firstObjective;
if (previousObjective == CurrentObjective) { return CurrentObjective; }
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (GameMain.NetworkMember is { IsServer: true })
{
CurrentObjective = CurrentOrder;
}
else
{
CurrentObjective = firstObjective;
}
if (previousObjective != CurrentObjective)
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
{
NetEntityEvent.Type.ObjectiveManagerState,
currentObjectiveIsOrder ? "order" : "objective"
});
}
GameMain.NetworkMember.CreateEntityEvent(character,
new Character.ObjectiveManagerStateEventData(currentObjectiveIsOrder ? ObjectiveType.Order : ObjectiveType.Objective));
}
return CurrentObjective;
}
@@ -333,7 +335,7 @@ namespace Barotrauma
SortObjectives();
}
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
public void SetOrder(Order order, bool speak)
{
if (character.IsDead)
{
@@ -345,13 +347,13 @@ namespace Barotrauma
}
ClearIgnored();
if (order == null || order.Identifier == "dismissed")
if (order == null || order.IsDismissal)
{
if (!string.IsNullOrEmpty(option))
if (order.Option != Identifier.Empty)
{
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option)))
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(order.Option)))
{
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option));
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(order.Option));
CurrentOrders.Remove(dismissedOrderInfo);
}
}
@@ -366,18 +368,18 @@ namespace Barotrauma
{
if (CurrentOrders.Count <= i) { break; }
var currentOrder = CurrentOrders[i];
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option))
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order))
{
CurrentOrders.RemoveAt(i);
continue;
}
var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption);
if (currentOrderInfo.HasValue)
var currentOrderInfo = character.GetCurrentOrder(currentOrder);
if (currentOrderInfo is Order)
{
int currentPriority = currentOrderInfo.Value.ManualPriority;
int currentPriority = currentOrderInfo.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
{
CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority);
CurrentOrders[i] = currentOrder.WithManualPriority(currentPriority);
}
}
else
@@ -386,46 +388,46 @@ namespace Barotrauma
}
}
var newCurrentOrder = CreateObjective(order, option, orderGiver);
if (newCurrentOrder != null)
var newCurrentObjective = CreateObjective(order);
if (newCurrentObjective != null)
{
newCurrentOrder.Abandoned += () => DismissSelf(order, option);
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
newCurrentObjective.Abandoned += () => DismissSelf(order);
CurrentOrders.Add(order.WithObjective(newCurrentObjective));
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else if (newCurrentOrder != null)
else if (newCurrentObjective != null)
{
if (speak && character.IsOnPlayerTeam)
{
string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
character.Speak(msg, delay: 1.0f);
LocalizedString msg = newCurrentObjective.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
character.Speak(msg.Value, delay: 1.0f);
}
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
public AIObjective CreateObjective(Order order, float priorityModifier = 1)
{
if (order == null || order.Identifier == "dismissed") { return null; }
if (order == null || order.IsDismissal) { return null; }
AIObjective newObjective;
switch (order.Identifier.ToLowerInvariant())
switch (order.Identifier.Value.ToLowerInvariant())
{
case "follow":
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
if (order.OrderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
IsFollowOrderObjective = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = "dialogcannotreachplace"
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
};
break;
case "wait":
@@ -435,14 +437,14 @@ namespace Barotrauma
};
break;
case "return":
newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier);
newObjective.Completed += () => DismissSelf(order, option);
newObjective = new AIObjectiveReturn(character, order.OrderGiver, this, priorityModifier: priorityModifier);
newObjective.Completed += () => DismissSelf(order);
break;
case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull);
break;
case "chargebatteries":
newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier);
newObjective = new AIObjectiveChargeBatteries(character, this, order.Option, priorityModifier);
break;
case "rescue":
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
@@ -459,16 +461,16 @@ namespace Barotrauma
if (order.TargetItemComponent is Pump targetPump)
{
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = orderGiver != null && orderGiver.IsCommanding
Override = order.OrderGiver is { IsCommanding: true }
};
newObjective.Completed += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order);
}
else
{
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
newObjective = new AIObjectivePumpWater(character, this, order.Option, priorityModifier: priorityModifier);
}
break;
case "extinguishfires":
@@ -488,22 +490,22 @@ namespace Barotrauma
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsCommanding
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
break;
case "setchargepct":
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier)
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = !character.IsDismissed,
completionCondition = () =>
{
if (float.TryParse(option, out float pct))
if (float.TryParse(order.Option.Value, out float pct))
{
var targetRatio = Math.Clamp(pct, 0f, 1f);
var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio;
@@ -541,7 +543,7 @@ namespace Barotrauma
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
KeepActiveWhenReady = true,
CheckInventory = true,
@@ -557,7 +559,7 @@ namespace Barotrauma
}
else
{
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
KeepActiveWhenReady = false,
CheckInventory = false,
@@ -568,20 +570,20 @@ namespace Barotrauma
prepareObjective.KeepActiveWhenReady = false;
prepareObjective.Equip = true;
newObjective = prepareObjective;
newObjective.Completed += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order);
break;
case "loaditems":
newObjective = new AIObjectiveLoadItems(character, this, option, order.GetTargetItems(option), order.TargetEntity as Item, priorityModifier);
newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsCommanding
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
if (newObjective.Abandon) { return null; }
break;
@@ -594,27 +596,26 @@ namespace Barotrauma
return newObjective;
}
private void DismissSelf(Order order, string option)
private void DismissSelf(Order order)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order, option));
if (currentOrder.Order == null)
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option));
if (currentOrder == null)
{
#if DEBUG
DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found");
#endif
return;
}
Order dismissOrder = Order.GetPrefab("dismissed");
var orderOption = Order.GetDismissOrderOption(currentOrder);
int priority = currentOrder.ManualPriority;
Order dismissOrder = currentOrder.GetDismissal();
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder, orderOption, priority, character);
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, orderOption, priority, currentOrder.Order.TargetSpatialEntity, character, character));
SetOrder(dismissOrder, orderOption, priority, character, speak: false);
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character));
SetOrder(dismissOrder, speak: false);
#endif
}
@@ -638,7 +639,7 @@ namespace Barotrauma
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T).Objective as T;
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Returns the last active objective of the specific type.
@@ -704,7 +705,7 @@ namespace Barotrauma
return 0;
}
public OrderInfo? GetCurrentOrderInfo()
public Order GetCurrentOrderInfo()
{
if (currentOrder == null) { return null; }
return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder);
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string Identifier { get; set; } = "operate item";
public override Identifier Identifier { get; set; } = "operate item".ToIdentifier();
public override string DebugTag => $"{Identifier} {component.Name}";
public override bool AllowAutomaticItemUnequipping => true;
@@ -67,6 +67,11 @@ namespace Barotrauma
Priority = 0;
return Priority;
}
else if (targetItem.IsClaimedByBallastFlora)
{
Priority = 0;
return Priority;
}
var reactor = component?.Item.GetComponent<Reactor>();
if (reactor != null)
{
@@ -79,7 +84,7 @@ namespace Barotrauma
return Priority;
}
}
switch (Option)
switch (Option.Value.ToLowerInvariant())
{
case "shutdown":
if (!reactor.PowerOn)
@@ -146,7 +151,7 @@ namespace Barotrauma
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip,
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, Identifier option, bool requireEquip,
Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option)
{
@@ -181,7 +186,7 @@ namespace Barotrauma
{
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name).Value, delay: 2.0f, identifier: "cantfindcontroller".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
Abandon = true;
return;
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectivePrepare : AIObjective
{
public override string Identifier { get; set; } = "prepare";
public override Identifier Identifier { get; set; } = "prepare".ToIdentifier();
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
@@ -19,8 +19,8 @@ namespace Barotrauma
private AIObjectiveGetItems getMultipleItemsObjective;
private bool subObjectivesCreated;
private readonly Item targetItem;
private readonly ImmutableArray<string> requiredItems;
private readonly ImmutableArray<string> optionalItems;
private readonly ImmutableArray<Identifier> requiredItems;
private readonly ImmutableArray<Identifier> optionalItems;
private readonly HashSet<Item> items = new HashSet<Item>();
public bool KeepActiveWhenReady { get; set; }
public bool CheckInventory { get; set; }
@@ -43,7 +43,7 @@ namespace Barotrauma
this.targetItem = targetItem;
}
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> optionalItems, IEnumerable<string> requiredItems = null, float priorityModifier = 1)
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<Identifier> optionalItems, IEnumerable<Identifier> requiredItems = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.optionalItems = optionalItems.ToImmutableArray();
@@ -98,7 +98,7 @@ namespace Barotrauma
{
getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true);
}
AIObjectiveGetItems CreateObjectives(IEnumerable<string> itemTags, bool requireAll)
AIObjectiveGetItems CreateObjectives(IEnumerable<Identifier> itemTags, bool requireAll)
{
AIObjectiveGetItems objectiveReference = null;
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
@@ -148,7 +148,7 @@ namespace Barotrauma
}
else
{
IEnumerable<string> allItems = optionalItems;
IEnumerable<Identifier> allItems = optionalItems;
if (requiredItems != null && requiredItems.Any())
{
allItems = requiredItems;
@@ -9,13 +9,13 @@ namespace Barotrauma
{
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
public override string Identifier { get; set; } = "pump water";
public override Identifier Identifier { get; set; } = "pump water".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
private IEnumerable<Pump> pumpList;
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option) { }
protected override void FindTargets()
@@ -41,6 +41,7 @@ namespace Barotrauma
if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (pump.Item.IsClaimedByBallastFlora) { return false; }
if (IsReady(pump)) { return false; }
return true;
}
@@ -48,7 +49,7 @@ namespace Barotrauma
{
if (pumpList == null)
{
if (character == null || character.Submarine == null) { return new Pump[0]; }
if (character == null || character.Submarine == null) { return Array.Empty<Pump>(); }
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
}
return pumpList;
@@ -1,7 +1,6 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
@@ -9,9 +8,10 @@ namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
public override string Identifier { get; set; } = "repair item";
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
public override bool AllowInAnySub => true;
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
public Item Item { get; private set; }
@@ -52,6 +52,10 @@ namespace Barotrauma
Priority = 0;
IsCompleted = true;
}
else if (Item.IsClaimedByBallastFlora)
{
Priority = 0;
}
else
{
float distanceFactor = 1;
@@ -70,7 +74,7 @@ namespace Barotrauma
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
float max = AIObjectiveManager.LowestOrderPriority - reduction;
float highestWeight = -1;
foreach (string tag in Item.Prefab.Tags)
foreach (Identifier tag in Item.Prefab.Tags)
{
if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight)
{
@@ -92,7 +96,7 @@ namespace Barotrauma
IsCompleted = Item.IsFullCondition;
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "itemrepaired".ToIdentifier(), 10.0f);
}
return IsCompleted;
}
@@ -118,7 +122,7 @@ namespace Barotrauma
{
if (character.IsOnPlayerTeam)
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair").Value, null, 0.0f, "dialogcannotfindrequireditemtorepair".ToIdentifier(), 10.0f);
}
}
subObjectives.Add(getItemObjective);
@@ -206,7 +210,7 @@ namespace Barotrauma
{
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f);
}
repairable.StopRepairing(character);
}
@@ -243,7 +247,7 @@ namespace Barotrauma
Abandon = true;
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f);
}
});
}
@@ -9,12 +9,12 @@ namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{
public override string Identifier { get; set; } = "repair items";
public override Identifier Identifier { get; set; } = "repair items".ToIdentifier();
/// <summary>
/// If set, only fix items where required skill matches this.
/// </summary>
public string RelevantSkill;
public Identifier RelevantSkill;
public Item PrioritizedItem { get; private set; }
@@ -72,9 +72,9 @@ namespace Barotrauma
if (NearlyFullCondition(item)) { return false; }
}
}
if (!string.IsNullOrWhiteSpace(RelevantSkill))
if (!RelevantSkill.IsEmpty)
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
}
return !HumanAIController.IsItemRepairedByAnother(item, out _);
}
@@ -151,6 +151,7 @@ namespace Barotrauma
if (!item.IsInteractable(character)) { return false; }
if (item.IsFullCondition) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
if (item.IsClaimedByBallastFlora) { return false; }
//player crew ignores items in outposts
if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveRescue : AIObjective
{
public override string Identifier { get; set; } = "rescue";
public override Identifier Identifier { get; set; } = "rescue".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
@@ -30,6 +30,7 @@ namespace Barotrauma
private float findHullTimer;
private bool ignoreOxygen;
private readonly float findHullInterval = 1.0f;
private bool performedCpr;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -146,9 +147,10 @@ namespace Barotrauma
{
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
@@ -158,7 +160,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
@@ -216,15 +218,15 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
@@ -233,18 +235,19 @@ namespace Barotrauma
{
if (targetCharacter.CurrentHull?.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f);
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f);
}
}
GiveTreatment(deltaTime);
}
}
private readonly List<string> suitableItemIdentifiers = new List<string>();
private readonly List<string> itemNameList = new List<string>();
private readonly Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private readonly List<Identifier> suitableItemIdentifiers = new List<Identifier>();
private readonly List<LocalizedString> itemNameList = new List<LocalizedString>();
private readonly Dictionary<Identifier, float> currentTreatmentSuitabilities = new Dictionary<Identifier, float>();
private void GiveTreatment(float deltaTime)
{
if (targetCharacter == null)
@@ -281,7 +284,7 @@ namespace Barotrauma
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
@@ -311,12 +314,12 @@ namespace Barotrauma
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
{
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
@@ -327,7 +330,7 @@ namespace Barotrauma
}
if (itemNameList.Any())
{
string itemListStr = "";
LocalizedString itemListStr = "";
if (itemNameList.Count == 1)
{
itemListStr = itemNameList[0];
@@ -337,33 +340,34 @@ namespace Barotrauma
//[treatment1] or [treatment2]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
("[treatment1]", itemNameList[0]),
("[treatment2]", itemNameList[1]));
}
else
{
//[treatment1], [treatment2], [treatment3] ... or [treatmentx]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
("[treatment1]", itemNameList[0]),
("[treatment2]", itemNameList[1]));
for (int i = 2; i < itemNameList.Count - 1; i++)
{
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList[i] });
("[treatment1]", itemListStr),
("[treatment2]", itemNameList[i]));
}
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList.Last() });
("[treatment1]", itemListStr),
("[treatment2]", itemNameList.Last()));
}
if (targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f);
}
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
@@ -374,13 +378,13 @@ namespace Barotrauma
Abandon = true;
if (character != targetCharacter && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
}
});
}
else if (cprSuitability <= 0)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
Abandon = true;
}
}
@@ -388,7 +392,7 @@ namespace Barotrauma
else if (!targetCharacter.IsUnconscious)
{
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
Abandon = true;
return;
}
@@ -398,6 +402,7 @@ namespace Barotrauma
{
character.SelectCharacter(targetCharacter);
character.AnimController.Anim = AnimController.Animation.CPR;
performedCpr = true;
}
else
{
@@ -425,7 +430,7 @@ namespace Barotrauma
}
if (remove)
{
Entity.Spawner?.AddToRemoveQueue(item);
Entity.Spawner?.AddItemToRemoveQueue(item);
}
}
@@ -433,9 +438,10 @@ namespace Barotrauma
{
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
{
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
return isCompleted;
}
@@ -7,7 +7,7 @@ namespace Barotrauma
{
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
{
public override string Identifier { get; set; } = "rescue all";
public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier();
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
@@ -112,12 +112,15 @@ namespace Barotrauma
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine != character.Submarine) { return false; }
if (character.Submarine != null)
{
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
}
else
{
return target.Submarine == null;
}
if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveReturn : AIObjective
{
public override string Identifier { get; set; } = "return";
public override Identifier Identifier { get; set; } = "return".ToIdentifier();
public Submarine ReturnTarget { get; }
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
@@ -93,7 +93,7 @@ namespace Barotrauma
// Target the closest airlock
float closestDist = 0;
Hull airlock = null;
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != targetHull.Submarine) { continue; }
if (!hull.IsTaggedAirlock()) { continue; }
@@ -210,10 +210,10 @@ namespace Barotrauma
SteeringManager?.Reset();
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotreturn", returnNull: true);
if (msg != null)
string msg = TextManager.Get("dialogcannotreturn").Value;
if (!msg.IsNullOrEmpty())
{
character.Speak(msg, identifier: "dialogcannotreturn", minDurationBetweenSimilar: 5.0f);
character.Speak(msg, identifier: "dialogcannotreturn".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -93,15 +93,15 @@ namespace Barotrauma
}
Rate = element.GetAttributeFloat("rate", 0.016f);
totalCommonness = 0.0f;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.LocalName.ToLowerInvariant())
{
case "item":
string identifier = subElement.GetAttributeString("identifier", "");
Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
Item newItemToProduce = new Item
{
Prefab = string.IsNullOrEmpty(identifier) ? null : ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")),
Prefab = identifier.IsEmpty ? null : ItemPrefab.Find("", subElement.GetAttributeIdentifier("identifier", Identifier.Empty)),
Commonness = subElement.GetAttributeFloat("commonness", 0.0f)
};
totalCommonness += newItemToProduce.Commonness;
@@ -134,8 +134,8 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddToSpawnQueue(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;
}
}
@@ -174,7 +174,7 @@ namespace Barotrauma
PlayForce = element.GetAttributeFloat("playforce", 15.0f);
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.LocalName.ToLowerInvariant())
{
@@ -202,7 +202,7 @@ namespace Barotrauma
}
}
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName);
}
public StatusIndicatorType GetCurrentStatusIndicatorType()
@@ -218,7 +218,7 @@ namespace Barotrauma
bool success = OnEat(item.GetTags());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "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;
}
@@ -226,28 +226,28 @@ namespace Barotrauma
public bool OnEat(Character character)
{
if (character == null || !character.IsDead) { return false; }
bool success = OnEat("dead");
bool success = OnEat("dead".ToIdentifier());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "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;
}
private bool OnEat(IEnumerable<string> tags)
private bool OnEat(IEnumerable<Identifier> tags)
{
foreach (string tag in tags)
foreach (Identifier tag in tags)
{
if (OnEat(tag)) { return true; }
}
return false;
}
private bool OnEat(string tag)
public bool OnEat(Identifier tag)
{
for (int i = 0; i < foods.Count; i++)
{
if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase))
if (tag == foods[i].Tag)
{
Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness;
@@ -352,7 +352,7 @@ namespace Barotrauma
}
else if (Hunger < MaxHunger * 0.1f)
{
character.CharacterHealth.ReduceAffliction(null, null, 8.0f * deltaTime);
character.CharacterHealth.ReduceAllAfflictionsOnAllLimbs(8.0f * deltaTime);
}
if (character.SelectedBy != null)
@@ -404,7 +404,7 @@ namespace Barotrauma
public static void LoadPets(XElement petsElement)
{
foreach (XElement subElement in petsElement.Elements())
foreach (var subElement in petsElement.Elements())
{
string speciesName = subElement.GetAttributeString("speciesname", "");
string seed = subElement.GetAttributeString("seed", "123");
@@ -418,9 +418,9 @@ namespace Barotrauma
else
{
//try to find a spawnpoint in the main sub
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandom();
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
//if not found, try any player sub (shuttle/drone etc)
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
@@ -439,7 +439,7 @@ namespace Barotrauma
var inventoryElement = subElement.Element("inventory");
if (inventoryElement != null)
{
pet.SpawnInventoryItems(pet.Inventory, inventoryElement);
pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null));
}
}
}
@@ -8,7 +8,7 @@ namespace Barotrauma
{
public const float MaxImportance = 100f;
public const float MinImportance = 0f;
public Order SuggestedOrderPrefab { get; }
public Order SuggestedOrder { get; }
private float importance;
public float Importance
@@ -25,11 +25,11 @@ namespace Barotrauma
public float CurrentRedundancy { get; set; }
public readonly ShipCommandManager shipCommandManager;
public string Option { get; set; }
public Identifier Option => SuggestedOrder.Option;
public Character OrderedCharacter { get; set; }
public Order CurrentOrder { get; private set; }
public ItemComponent TargetItemComponent { get; protected set; }
public Item TargetItem { get; protected set; }
public ItemComponent TargetItemComponent => SuggestedOrder.TargetItemComponent;
public Item TargetItem => SuggestedOrder.TargetEntity as Item;
public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected
protected virtual Character CommandingCharacter => shipCommandManager.character;
@@ -38,25 +38,28 @@ namespace Barotrauma
public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies
public virtual bool AllowEasySwitching => false;
public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null)
public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrder)
{
this.shipCommandManager = shipCommandManager;
SuggestedOrderPrefab = suggestedOrderPrefab;
Option = option;
SuggestedOrder = suggestedOrder;
}
public void SetOrder(Character orderedCharacter)
{
OrderedCharacter = orderedCharacter;
if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrderPrefab, Option)))
if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option)))
{
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
}
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5);
CurrentOrder = SuggestedOrder
.WithOption(Option)
.WithItemComponent(TargetItem, TargetItemComponent)
.WithOrderGiver(CommandingCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5);
}
TimeSinceLastAttempt = 0f;
}
@@ -113,7 +116,7 @@ namespace Barotrauma
}
// accept only the highest priority order
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder)
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
{
#if DEBUG
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
@@ -4,11 +4,7 @@ namespace Barotrauma
{
abstract class ShipIssueWorkerItem : ShipIssueWorker
{
public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option)
{
TargetItemComponent = targetItemComponent;
TargetItem = targetItem;
}
public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { }
protected override bool IsIssueViable()
{
@@ -12,7 +12,7 @@ namespace Barotrauma
public override bool AllowEasySwitching => true;
public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { }
public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { }
float GetTargetingImportance(Entity entity)
{
@@ -4,9 +4,7 @@ namespace Barotrauma
{
class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem
{
public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option)
{
}
public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { }
public override void CalculateImportanceSpecific()
{
@@ -7,7 +7,7 @@ namespace Barotrauma
// The AI could be set to steer automatically through a specialized job or autonomous objectives
// but the logic involved doesn't really allow that without some annoyingly specific changes
// hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong
public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { }
public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { }
public override void CalculateImportanceSpecific()
{
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
@@ -95,7 +95,7 @@ namespace Barotrauma
public static void ShipCommandLog(string text)
{
if (GameSettings.VerboseLogging)
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage(text);
}
@@ -251,14 +251,14 @@ namespace Barotrauma
if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold)
{
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true);
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true);
foreach (Character orderedCharacter in bestCharacters)
{
float issueApplicability = mostImportantIssue.Importance;
// prefer not to switch if not qualified
issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f;
issueApplicability *= mostImportantIssue.SuggestedOrder.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f;
ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter);
@@ -276,7 +276,7 @@ namespace Barotrauma
}
// give slight preference if not qualified for current job
issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f;
issueApplicability += occupiedIssue.SuggestedOrder.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f;
// prefer not to switch orders unless considerably more important
issueApplicability -= IssueDevotionBuffer;
@@ -312,9 +312,8 @@ namespace Barotrauma
#if DEBUG
ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter);
#endif
Order orderPrefab = Order.GetPrefab("dismissed");
//character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character);
var order = new Order(OrderPrefab.Dismissal, null).WithManualPriority(3).WithOrderGiver(character);
shipIssueWorker.OrderedCharacter.SetOrder(order, isNewOrder: true);
shipIssueWorker.RemoveOrder();
break;
}
@@ -346,18 +345,21 @@ namespace Barotrauma
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
var order = new Order(OrderPrefab.Prefabs["operatereactor"], "powerup".ToIdentifier(), reactor.Item, reactor);
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, order));
}
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent<Steering>() is Steering steeringComponent)
{
steering = steeringComponent;
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical"));
var order = new Order(OrderPrefab.Prefabs["steer"], "navigatetactical".ToIdentifier(), nav, steeringComponent);
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order));
}
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
{
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent<Turret>()));
var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>());
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order));
}
int crewSizeModifier = 2;
@@ -365,14 +367,16 @@ namespace Barotrauma
ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this);
for (int i = 0; i < crewSizeModifier; i++)
{
ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks));
var order = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, order, shipGlobalIssueFixLeaks));
}
shipGlobalIssues.Add(shipGlobalIssueFixLeaks);
ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this);
for (int i = 0; i < crewSizeModifier; i++)
{
ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems));
var order = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, order, shipGlobalIssueRepairSystems));
}
shipGlobalIssues.Add(shipGlobalIssueRepairSystems);
@@ -31,11 +31,11 @@ namespace Barotrauma
private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, string tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag));
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public static WreckAI Create(Submarine wreck)
{
@@ -54,14 +54,14 @@ namespace Barotrauma
return;
}
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.Server);
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient);
if (brainPrefab == null)
{
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
return;
}
allItems = Wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab));
thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab));
hulls.AddRange(Wreck.GetHulls(false));
var potentialBrainHulls = new List<(Hull hull, float weight)>();
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
@@ -103,12 +103,12 @@ namespace Barotrauma
potentialBrainHulls.Add((hull, weight));
}
}
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.Server);
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p));
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient);
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus);
if (brainHull == null)
{
DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
brainHull = hulls.GetRandom(Rand.RandSync.Server);
brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient);
}
if (brainHull == null)
{
@@ -118,12 +118,12 @@ namespace Barotrauma
brainHull.WaterVolume = brainHull.Volume;
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
brain.CurrentHull = brainHull;
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.Server);
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient);
if (backgroundPrefab != null)
{
new Structure(brainHull.Rect, backgroundPrefab, Wreck);
}
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.Server);
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient);
if (horizontalWallPrefab != null)
{
int height = (int)horizontalWallPrefab.Size.Y;
@@ -132,7 +132,7 @@ namespace Barotrauma
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
}
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.Server);
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient);
if (verticalWallPrefab != null)
{
int width = (int)verticalWallPrefab.Size.X;
@@ -162,7 +162,7 @@ namespace Barotrauma
{
if (container.Inventory.GetItemAt(i) != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) &&
Config.ForbiddenAmmunition.None(id => id.Equals(ip.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab)
Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
@@ -272,7 +272,7 @@ namespace Barotrauma
cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside);
for (int i = 0; i < cellsOutside; i++)
{
ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null);
ISpatialEntity targetEntity = wayPoints.GetRandomUnsynced(wp => wp.CurrentHull == null);
if (targetEntity == null) { break; }
if (!TrySpawnCell(out _, targetEntity)) { break; }
}
@@ -310,7 +310,7 @@ namespace Barotrauma
// but as long as spawning is handled via status effects, I don't know if there is any better way.
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
// And if there was, the distance check should prevent killing the agents of a different organism.
if (character.SpeciesName.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase))
if (character.SpeciesName == Config.OffensiveAgent)
{
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
float maxDistance = Sonar.DefaultSonarRange;
@@ -341,7 +341,7 @@ namespace Barotrauma
public static void RemoveThalamusItems(Submarine wreck)
{
List<MapEntity> thalamusItems = new List<MapEntity>();
foreach (var wreckAiConfig in WreckAIConfig.List)
foreach (var wreckAiConfig in WreckAIConfig.Prefabs)
{
thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity));
}
@@ -391,7 +391,7 @@ namespace Barotrauma
cellSpawnTimer -= deltaTime;
if (cellSpawnTimer < 0)
{
TrySpawnCell(out _, spawnOrgans.GetRandom());
TrySpawnCell(out _, spawnOrgans.GetRandomUnsynced());
cellSpawnTimer = GetSpawnTime();
}
}
@@ -403,8 +403,8 @@ namespace Barotrauma
if (targetEntity == null)
{
targetEntity =
wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ??
hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity;
wayPoints.GetRandomUnsynced(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ??
hulls.GetRandomUnsynced(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity;
}
if (targetEntity == null) { return false; }
if (targetEntity is Hull h)
@@ -442,7 +442,7 @@ namespace Barotrauma
}
#if SERVER
public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null)
{
msg.Write(IsAlive);
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using System;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
@@ -6,131 +7,97 @@ using System.Xml.Linq;
namespace Barotrauma
{
class WreckAIConfig : ISerializableEntity
class WreckAIConfig : PrefabWithUintIdentifier, ISerializableEntity
{
public readonly static PrefabCollection<WreckAIConfig> Prefabs = new PrefabCollection<WreckAIConfig>();
public string Name => "Wreck AI Config";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
[Serialize("", false)]
public string Entity { get; private set; }
public Identifier Entity => Identifier;
[Serialize("", false)]
public string DefensiveAgent { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier DefensiveAgent { get; private set; }
[Serialize("", false)]
[Serialize("", IsPropertySaveable.No)]
public string OffensiveAgent { get; private set; }
[Serialize("", false)]
[Serialize("", IsPropertySaveable.No)]
public string Brain { get; private set; }
[Serialize("", false)]
[Serialize("", IsPropertySaveable.No)]
public string Spawner { get; private set; }
[Serialize("", false)]
[Serialize("", IsPropertySaveable.No)]
public string BrainRoomBackground { get; private set; }
[Serialize("", false)]
[Serialize("", IsPropertySaveable.No)]
public string BrainRoomVerticalWall { get; private set; }
[Serialize("", false)]
[Serialize("", IsPropertySaveable.No)]
public string BrainRoomHorizontalWall { get; private set; }
[Serialize(60f, false)]
[Serialize(60f, IsPropertySaveable.No)]
public float AgentSpawnDelay { get; private set; }
[Serialize(0.5f, false)]
[Serialize(0.5f, IsPropertySaveable.No)]
public float AgentSpawnDelayRandomFactor { get; private set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float AgentSpawnDelayDifficultyMultiplier { get; private set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float AgentSpawnCountDifficultyMultiplier { get; private set; }
[Serialize(0, false)]
[Serialize(0, IsPropertySaveable.No)]
public int MinAgentsPerBrainRoom { get; private set; }
[Serialize(3, false)]
[Serialize(3, IsPropertySaveable.No)]
public int MaxAgentsPerRoom { get; private set; }
[Serialize(2, false)]
[Serialize(2, IsPropertySaveable.No)]
public int MinAgentsOutside { get; private set; }
[Serialize(5, false)]
[Serialize(5, IsPropertySaveable.No)]
public int MaxAgentsOutside { get; private set; }
[Serialize(3, false)]
[Serialize(3, IsPropertySaveable.No)]
public int MinAgentsInside { get; private set; }
[Serialize(10, false)]
[Serialize(10, IsPropertySaveable.No)]
public int MaxAgentsInside { get; private set; }
[Serialize(15, false)]
[Serialize(15, IsPropertySaveable.No)]
public int MaxAgentCount { get; private set; }
[Serialize(100f, false)]
[Serialize(100f, IsPropertySaveable.No)]
public float MinWaterLevel { get; private set; }
[Serialize(true, false)]
[Serialize(true, IsPropertySaveable.No)]
public bool KillAgentsWhenEntityDies { get; private set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float DeadEntityColorMultiplier { get; private set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float DeadEntityColorFadeOutTime { get; private set; }
public readonly string[] ForbiddenAmmunition;
public readonly Identifier[] ForbiddenAmmunition;
public static List<WreckAIConfig> List
public static WreckAIConfig GetRandom() => Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
protected override Identifier DetermineIdentifier(XElement element)
{
get
{
if (paramsList == null)
{
LoadAll();
}
return paramsList;
}
return element.GetAttributeIdentifier("Entity", base.DetermineIdentifier(element));
}
private static List<WreckAIConfig> paramsList;
public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server);
public WreckAIConfig(XElement element)
public WreckAIConfig(ContentXElement element, WreckAIConfigFile file) : base(file, element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true);
ForbiddenAmmunition = XMLExtensions.GetAttributeIdentifierArray(element, "ForbiddenAmmunition", Array.Empty<Identifier>());
}
public static void LoadAll()
{
paramsList = new List<WreckAIConfig>();
var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig);
if (files.None())
{
DebugConsole.ThrowError("Cannot find any Wreck AI config!");
return;
}
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (mainElement.IsOverride())
{
mainElement = doc.Root.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'");
}
paramsList.Add(new WreckAIConfig(mainElement));
}
}
public override void Dispose() { }
}
}
@@ -11,8 +11,8 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
{
InitProjSpecific();
}
@@ -13,14 +13,14 @@ namespace Barotrauma
/// An arbitrary identifier that can be used to determine what kind of a message this is
/// and prevent characters from saying the same kind of line too often.
/// </summary>
public readonly string Identifier;
public readonly Identifier Identifier;
public ChatMessageType? MessageType;
public float SendDelay;
public double SendTime;
public AIChatMessage(string message, ChatMessageType? type, string identifier = "", float delay = 0.0f)
public AIChatMessage(string message, ChatMessageType? type, Identifier identifier = default, float delay = 0.0f)
{
Message = message;
MessageType = type;
@@ -24,6 +24,8 @@ namespace Barotrauma
public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee;
protected bool Aiming => aiming || aimingMelee;
public float ArmLength => upperArmLength + forearmLength;
public abstract GroundedMovementParams WalkParams { get; set; }
@@ -99,8 +101,7 @@ namespace Barotrauma
{
if (InWater || !CanWalk)
{
float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
return TargetMovement.LengthSquared() > avg * avg;
return TargetMovement.LengthSquared() > MathUtils.Pow2(SwimSlowParams.MovementSpeed);
}
else
{
@@ -22,8 +22,9 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName);
if (character.VariantOf != null)
#warning TODO: this is kinda janky, this should probably be done better
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName));
if (!character.VariantOf.IsEmpty)
{
_ragdollParams.ApplyVariantScale(character.Params.VariantFile);
}
@@ -192,7 +193,7 @@ namespace Barotrauma
strongestImpact = 0.0f;
}
if (aiming)
if (Aiming)
{
TargetMovement = TargetMovement.ClampLength(2);
}
@@ -232,7 +233,7 @@ namespace Barotrauma
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !aiming)
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming)
{
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
@@ -421,7 +422,7 @@ namespace Barotrauma
}
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
Entity.Spawner?.AddEntityToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
@@ -432,10 +433,10 @@ namespace Barotrauma
}
else //sever a random joint
{
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandomUnsynced());
}
}
}
}
}
}
@@ -447,21 +448,18 @@ namespace Barotrauma
movement = TargetMovement;
bool isMoving = movement.LengthSquared() > 0.00001f;
var mainLimb = MainLimb;
if (isMoving)
float t = 0.5f;
if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement)
{
float t = 0.5f;
if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement)
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
mainLimb.PullJointEnabled = true;
@@ -1016,24 +1014,29 @@ namespace Barotrauma
{
if (l.IsSevered) { continue; }
float rotation = l.body.Rotation;
if (l.DoesFlip)
{
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle
rotation = -(l.body.Rotation + MathHelper.Pi);
}
else
{
//vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards)
rotation = -l.body.Rotation;
}
}
TrySetLimbPosition(l,
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
rotation,
lerp);
l.body.PositionSmoothingFactor = 0.8f;
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle
l.body.SetTransform(l.SimPosition, -(l.body.Rotation + MathHelper.Pi));
}
else
{
//vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards)
l.body.SetTransform(l.SimPosition, -l.body.Rotation);
}
}
if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter))
{
@@ -25,7 +25,7 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
_ragdollParams = RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(character.VariantOf ?? character.SpeciesName);
_ragdollParams = RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(character.SpeciesName);
}
return _ragdollParams;
}
@@ -178,6 +178,14 @@ namespace Barotrauma
else if (Crouching)
{
shoulderHeight -= 0.15f;
if (Crouching)
{
bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f);
if (!movingHorizontally)
{
shoulderHeight -= HumanCrouchParams.MoveDownAmountWhenStationary;
}
}
}
return Collider.SimPosition + new Vector2(
@@ -435,8 +443,6 @@ namespace Barotrauma
if (CurrentGroundedParams == null) { return; }
Vector2 handPos;
//if you're allergic to magic numbers, stop reading now
Limb leftFoot = GetLimb(LimbType.LeftFoot);
Limb rightFoot = GetLimb(LimbType.RightFoot);
Limb head = GetLimb(LimbType.Head);
@@ -591,16 +597,20 @@ namespace Barotrauma
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
}
if (HeadAngle.HasValue)
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)
{
@@ -807,48 +817,16 @@ namespace Barotrauma
Limb torso = GetLimb(LimbType.Torso);
if (head == null) { return; }
if (torso == null) { return; }
//check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
const float DisableMovementAboveSurfaceThreshold = 50.0f;
if (currentHull != null && character.CurrentHull != null)
{
float surfacePos = currentHull.Surface;
float surfacePos = GetSurfaceY();
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
//if the hull is almost full of water, check if there's a water-filled hull above it
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
GetSurfacePos(currentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
if (prevSurfacePos > surfaceThreshold) { return; }
foreach (Gap gap in hull.ConnectedGaps)
{
if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; }
if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; }
//if the gap is above us and leads outside, there's no surface to limit the movement
if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y)
{
prevSurfacePos += 100000.0f;
return;
}
foreach (var linkedTo in gap.linkedTo)
{
if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
break;
}
}
}
}
}
surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos);
if (surfaceLimiter > 50.0f) { return; }
}
if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; }
}
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
@@ -862,37 +840,36 @@ namespace Barotrauma
{
rotation += 360;
}
if (!character.IsRemotelyControlled && !aiming && Anim != Animation.UsingConstruction &&
!(character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false))
{
if (rotation > 20 && rotation < 170)
{
TargetDir = Direction.Left;
}
else if (rotation > 190 && rotation < 340)
{
TargetDir = Direction.Right;
}
}
float targetSpeed = TargetMovement.Length();
if (targetSpeed > 0.1f)
if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !character.IsKeyDown(InputType.Aim))
{
if (!aiming)
if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false))
{
float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2;
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
if (rotation > 20 && rotation < 170)
{
TargetDir = Direction.Left;
}
else if (rotation > 190 && rotation < 340)
{
TargetDir = Direction.Right;
}
}
}
else
if (Aiming)
{
if (aiming)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * Dir;
if (diff.LengthSquared() > MathUtils.Pow2(0.4f))
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * Dir;
float newRotation = MathUtils.VectorToAngle(diff);
float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
}
else if (targetSpeed > 0.1f)
{
float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2;
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f);
@@ -906,13 +883,14 @@ namespace Barotrauma
{
torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque);
}
if (HeadAngle.HasValue)
if (!Aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue)
{
head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque);
}
else
{
head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque);
RotateHead(head);
}
//dont try to move upwards if head is already out of water
@@ -935,7 +913,7 @@ namespace Barotrauma
head.body.ApplyTorque(Dir);
}
movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / 50.0f));
movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold));
}
bool isNotRemote = true;
@@ -943,7 +921,18 @@ namespace Barotrauma
if (isNotRemote)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp);
float t = movementLerp;
if (targetSpeed > 0.00001f && !SimplePhysicsEnabled)
{
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
WalkPos += movement.Length();
@@ -1125,10 +1114,9 @@ namespace Barotrauma
bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y);
if (climbFast) { handPos.Y -= stepHeight; }
bool aiming = this.aiming || aimingMelee;
//prevent the hands from going above the top of the ladders
handPos.Y = Math.Min(-0.5f, handPos.Y);
if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(rightHand,
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X,
@@ -1136,7 +1124,7 @@ namespace Barotrauma
5.2f);
rightHand.body.ApplyTorque(Dir * 2.0f);
}
if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(leftHand,
new Vector2(handPos.X - ladderSimSize.X * 0.5f,
@@ -1219,7 +1207,11 @@ namespace Barotrauma
//apply forces to the collider to move the Character up/down
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
if (!aiming)
if (Aiming)
{
RotateHead(head);
}
else
{
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque);
@@ -1401,8 +1393,8 @@ namespace Barotrauma
else
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax);
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
@@ -1426,23 +1418,23 @@ namespace Barotrauma
targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
cprPump = 0;
if (skill < CPRSettings.DamageSkillThreshold)
if (skill < CPRSettings.Active.DamageSkillThreshold)
{
target.LastDamageSource = null;
target.DamageLimb(
targetTorso.WorldPosition, targetTorso,
new[] { CPRSettings.InsufficientSkillAffliction.Instantiate((CPRSettings.DamageSkillThreshold - skill) * CPRSettings.DamageSkillMultiplier, source: character) },
new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) },
0.0f, true, 0.0f, attacker: null);
}
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
{
float reviveChance = skill * CPRSettings.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.ReviveChanceMin, CPRSettings.ReviveChanceMax);
float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
if (powerfulCPR) { reviveChance *= 2.0f; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) <= reviveChance)
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
//increase oxygen and clamp it above zero
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
@@ -1469,7 +1461,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info?.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive);
character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -1512,11 +1504,14 @@ namespace Barotrauma
return;
}
Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso);
if (targetTorso == null) targetTorso = target.AnimController.MainLimb;
if (targetTorso == null)
{
targetTorso = target.AnimController.MainLimb;
}
if (target.AnimController.Dir != Dir)
{
target.AnimController.Flip();
}
Vector2 transformedTorsoPos = torso.SimPosition;
if (character.Submarine == null && target.Submarine != null)
{
@@ -1560,7 +1555,10 @@ namespace Barotrauma
{
//only grab with one hand when swimming
leftHand.Disabled = true;
if (!inWater) rightHand.Disabled = true;
if (!inWater)
{
rightHand.Disabled = true;
}
for (int i = 0; i < 2; i++)
{
@@ -1708,6 +1706,24 @@ namespace Barotrauma
}
}
private void RotateHead(Limb head)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 dir = (mousePos - head.SimPosition) * Dir;
float rot = MathUtils.VectorToAngle(dir);
var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso);
if (neckJoint != null)
{
float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation);
float lowerLimit = neckJoint.LowerLimit + offset;
float upperLimit = neckJoint.UpperLimit + offset;
float min = Math.Min(lowerLimit, upperLimit);
float max = Math.Max(lowerLimit, upperLimit);
rot = Math.Clamp(rot, min, max);
}
head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque);
}
private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle)
{
if (!MathUtils.IsValid(pos))
@@ -1844,11 +1860,9 @@ namespace Barotrauma
}
float angle = flipAngle ? -limb.body.Rotation : limb.body.Rotation;
if (wrapAngle) angle = MathUtils.WrapAnglePi(angle);
if (wrapAngle) { angle = MathUtils.WrapAnglePi(angle); }
TrySetLimbPosition(limb, Collider.SimPosition, position);
limb.body.SetTransform(limb.body.SimPosition, angle);
TrySetLimbPosition(limb, Collider.SimPosition, position, angle);
}
}
@@ -69,7 +69,7 @@ namespace Barotrauma
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
return Array.Empty<Limb>();
}
return limbs;
}
@@ -424,13 +424,13 @@ namespace Barotrauma
#endif
var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath);
if (characterPrefab?.XDocument != null)
if (characterPrefab?.ConfigElement != null)
{
var mainElement = characterPrefab.XDocument.Root.IsOverride() ? characterPrefab.XDocument.Root.FirstElement() : characterPrefab.XDocument.Root;
var mainElement = characterPrefab.ConfigElement;
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
{
if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this);
AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this);
}
}
}
@@ -811,9 +811,9 @@ namespace Barotrauma
}
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData());
}
return true;
}
@@ -1201,13 +1201,9 @@ namespace Barotrauma
headInWater = false;
inWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
if (currentHull.WaterVolume > currentHull.Volume * 0.95f)
if (currentHull.WaterPercentage > 0.001f)
{
inWater = true;
}
else
{
float waterSurface = ConvertUnits.ToSimUnits(currentHull.Surface);
float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY());
if (targetMovement.Y < 0.0f)
{
Vector2 colliderBottom = GetColliderBottom();
@@ -1220,11 +1216,8 @@ namespace Barotrauma
if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
}
}
float standHeight =
HeadPosition.HasValue ? HeadPosition.Value :
TorsoPosition.HasValue ? TorsoPosition.Value :
Collider.GetMaxExtent() * 0.5f;
if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.95f)
float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f;
if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f)
{
inWater = true;
}
@@ -1416,7 +1409,7 @@ namespace Barotrauma
#else
DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red);
#endif
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName));
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName.Value));
if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f)
{
@@ -1529,7 +1522,6 @@ namespace Barotrauma
}
}
private float GetFloorY(Vector2 simPosition, bool ignoreStairs = false)
{
onGround = false;
@@ -1648,6 +1640,51 @@ namespace Barotrauma
}
}
public float GetSurfaceY()
{
//check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
if (currentHull == null || character.CurrentHull == null)
{
return float.PositiveInfinity;
}
float surfacePos = currentHull.Surface;
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
//if the hull is almost full of water, check if there's a water-filled hull above it
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
GetSurfacePos(currentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
if (prevSurfacePos > surfaceThreshold) { return; }
foreach (Gap gap in hull.ConnectedGaps)
{
if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; }
if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; }
//if the gap is above us and leads outside, there's no surface to limit the movement
if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y)
{
prevSurfacePos += 100000.0f;
return;
}
foreach (var linkedTo in gap.linkedTo)
{
if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
break;
}
}
}
}
}
return surfacePos;
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
{
if (!MathUtils.IsValid(simPosition))
@@ -1701,7 +1738,7 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
//check visibility from the new position of the collider to the new position of this limb
Vector2 movePos = limb.SimPosition + limbMoveAmount;
TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms);
TrySetLimbPosition(limb, simPosition, movePos, limb.Rotation, lerp, ignorePlatforms);
}
}
}
@@ -1716,7 +1753,7 @@ namespace Barotrauma
IsHanging = true;
}
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true)
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true)
{
Vector2 movePos = simPosition;
@@ -1738,11 +1775,12 @@ namespace Barotrauma
if (lerp)
{
limb.body.TargetPosition = movePos;
limb.body.MoveToTargetPosition(true);
limb.body.TargetRotation = rotation;
limb.body.MoveToTargetPosition(true);
}
else
{
limb.body.SetTransform(movePos, limb.Rotation);
limb.body.SetTransform(movePos, rotation);
limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA;
limb.PullJointEnabled = false;
}
@@ -37,7 +37,9 @@ namespace Barotrauma
Pursue,
FollowThrough,
FollowThroughUntilCanAttack,
IdleUntilCanAttack
IdleUntilCanAttack,
Reverse,
ReverseUntilCanAttack
}
struct AttackResult
@@ -77,32 +79,32 @@ namespace Barotrauma
partial class Attack : ISerializableEntity
{
[Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable]
[Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable]
public AttackContext Context { get; private set; }
[Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable]
[Serialize(AttackTarget.Any, IsPropertySaveable.Yes, description: "Does the attack target only specific targets?"), Editable]
public AttackTarget TargetType { get; private set; }
[Serialize(LimbType.None, true, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable]
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable]
public LimbType TargetLimbType { get; private set; }
[Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable]
[Serialize(HitDetection.Distance, IsPropertySaveable.Yes, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable]
public HitDetection HitDetectionType { get; private set; }
[Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable]
[Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes, description: "The preferred AI behavior after the attack."), Editable]
public AIBehaviorAfterAttack AfterAttack { get; set; }
[Serialize(0f, true, description: "A delay before reacting after performing an attack."), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable]
public float AfterAttackDelay { get; set; }
[Serialize(false, true, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
public bool Reverse { get; private set; }
[Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
public bool Retreat { get; private set; }
private float _range;
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float Range
{
get => _range * RangeMultiplier;
@@ -110,48 +112,51 @@ namespace Barotrauma
}
private float _damageRange;
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float DamageRange
{
get => _damageRange * RangeMultiplier;
set => _damageRange = value;
}
[Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float MinRange { get; private set; }
[Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Duration { get; private set; }
[Serialize(5f, true, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
[Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float CoolDown { get; set; } = 5;
[Serialize(0f, true, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float SecondaryCoolDown { get; set; } = 0;
[Serialize(0f, true, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float CoolDownRandomFactor { get; private set; } = 0;
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool FullSpeedAfterAttack { get; private set; }
private float _structureDamage;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float StructureDamage
{
get => _structureDamage * DamageMultiplier;
set => _structureDamage = value;
}
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool EmitStructureDamageParticles { get; private set; }
private float _itemDamage;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage
{
get =>_itemDamage * DamageMultiplier;
set => _itemDamage = value;
}
[Serialize(0.0f, true, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
public float Penetration { get; private set; }
/// <summary>
@@ -169,28 +174,28 @@ namespace Barotrauma
/// </summary>
public float ImpactMultiplier { get; set; } = 1;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float LevelWallDamage { get; set; }
[Serialize(false, true)]
[Serialize(false, IsPropertySaveable.Yes)]
public bool Ranged { get; set; }
[Serialize(false, true, description:"Only affects ranged attacks.")]
[Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")]
public bool AvoidFriendlyFire { get; set; }
[Serialize(20f, true)]
[Serialize(20f, IsPropertySaveable.Yes)]
public float RequiredAngle { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float Stun { get; private set; }
[Serialize(false, true, description: "Can damage only Humans."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; private set; }
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string ApplyForceOnLimbs
{
get
@@ -211,54 +216,54 @@ namespace Barotrauma
}
}
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldStart { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldMiddle { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldEnd { get; private set; }
[Serialize(TransitionMode.Linear, true, description:""), Editable]
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable]
public TransitionMode RootTransitionEasing { get; private set; }
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
public float Torque { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool ApplyForcesOnlyOnce { get; private set; }
[Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float TargetImpulse { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
public Vector2 TargetImpulseWorld { get; private set; }
[Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)]
public float TargetForce { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float SubmarineImpactMultiplier { get; private set; }
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SeverLimbsProbability { get; set; }
// TODO: disabled because not synced
//[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
//[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
//public float StickChance { get; set; }
public float StickChance => 0f;
[Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Priority { get; private set; }
[Serialize(false, true, description: ""), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: ""), Editable]
public bool Blink { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
@@ -268,11 +273,11 @@ namespace Barotrauma
public string Name => "Attack";
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
} = new Dictionary<Identifier, SerializableProperty>();
//the indices of the limbs Force is applied on
//(if none, force is applied only to the limb the attack is attached to)
@@ -297,7 +302,7 @@ namespace Barotrauma
}
// used for talents/ability conditions
public Item SourceItem { get; }
public Item SourceItem { get; set; }
public List<Affliction> GetMultipliedAfflictions(float multiplier)
{
@@ -344,35 +349,35 @@ namespace Barotrauma
DamageRange = range;
StructureDamage = LevelWallDamage = structureDamage;
ItemDamage = itemDamage;
Penetration = Penetration;
}
public Attack(XElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName)
public Attack(ContentXElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName)
{
SourceItem = sourceItem;
}
public Attack(XElement element, string parentDebugName)
public Attack(ContentXElement element, string parentDebugName)
{
Deserialize(element);
Deserialize(element, parentDebugName);
if (element.Attribute("damage") != null ||
element.Attribute("bluntdamage") != null ||
element.Attribute("burndamage") != null ||
element.Attribute("bleedingdamage") != null)
if (element.GetAttribute("damage") != null ||
element.GetAttribute("bluntdamage") != null ||
element.GetAttribute("burndamage") != null ||
element.GetAttribute("bleedingdamage") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).");
}
//if level wall damage is not defined, default to the structure damage
if (element.Attribute("LevelWallDamage") == null &&
element.Attribute("levelwalldamage") == null)
if (element.GetAttribute("LevelWallDamage") == null &&
element.GetAttribute("levelwalldamage") == null)
{
LevelWallDamage = StructureDamage;
}
InitProjSpecific(element);
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -381,7 +386,7 @@ namespace Barotrauma
break;
case "affliction":
AfflictionPrefab afflictionPrefab;
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
@@ -395,7 +400,7 @@ namespace Barotrauma
else
{
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier];
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
@@ -415,22 +420,22 @@ namespace Barotrauma
}
}
}
partial void InitProjSpecific(XElement element = null);
partial void InitProjSpecific(ContentXElement element);
public void ReloadAfflictions(XElement element)
public void ReloadAfflictions(XElement element, string parentDebugName)
{
Afflictions.Clear();
foreach (var subElement in element.GetChildElements("affliction"))
{
AfflictionPrefab afflictionPrefab;
Affliction affliction;
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
DebugConsole.ThrowError($"Couldn't find the affliction with the identifier {afflictionIdentifier} referenced in {element.Document.ParseContentPathFromUri()}");
DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".");
continue;
}
afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier];
affliction = afflictionPrefab.Instantiate(0.0f);
affliction.Deserialize(subElement);
//backwards compatibility
@@ -455,10 +460,10 @@ namespace Barotrauma
}
}
public void Deserialize(XElement element)
public void Deserialize(XElement element, string parentDebugName)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ReloadAfflictions(element);
ReloadAfflictions(element, parentDebugName);
}
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null)
@@ -491,6 +496,10 @@ namespace Barotrauma
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (targetCharacter != null)
{
if (effect.HasTargetType(StatusEffect.TargetType.Character))
@@ -555,6 +564,10 @@ namespace Barotrauma
{
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
}
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
@@ -682,7 +695,7 @@ namespace Barotrauma
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(IDamageable target)
public bool IsValidTarget(Entity target)
{
return TargetType switch
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,172 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class Character
{
public enum EventType
{
InventoryState = 0,
Control = 1,
Status = 2,
Treatment = 3,
SetAttackTarget = 4,
ExecuteAttack = 5,
AssignCampaignInteraction = 6,
ObjectiveManagerState = 7,
TeamChange = 8,
AddToCrew = 9,
UpdateExperience = 10,
UpdateTalents = 11,
UpdateSkills = 12,
UpdateMoney = 13,
UpdatePermanentStats = 14,
MinValue = 0,
MaxValue = 14
}
private interface IEventData : NetEntityEvent.IData
{
public EventType EventType { get; }
}
public struct InventoryStateEventData : IEventData
{
public EventType EventType => EventType.InventoryState;
}
public struct ControlEventData : IEventData
{
public EventType EventType => EventType.Control;
public readonly Client Owner;
public ControlEventData(Client owner)
{
Owner = owner;
}
}
public struct CharacterStatusEventData : IEventData
{
public EventType EventType => EventType.Status;
}
public struct TreatmentEventData : IEventData
{
public EventType EventType => EventType.Treatment;
}
private interface IAttackEventData : IEventData
{
public Limb AttackLimb { get; }
public IDamageable TargetEntity { get; }
public Limb TargetLimb { get; }
public Vector2 TargetSimPos { get; }
}
public struct SetAttackTargetEventData : IAttackEventData
{
public EventType EventType => EventType.SetAttackTarget;
public Limb AttackLimb { get; }
public IDamageable TargetEntity { get; }
public Limb TargetLimb { get; }
public Vector2 TargetSimPos { get; }
public SetAttackTargetEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos)
{
AttackLimb = attackLimb;
TargetEntity = targetEntity;
TargetLimb = targetLimb;
TargetSimPos = targetSimPos;
}
}
public struct ExecuteAttackEventData : IAttackEventData
{
public EventType EventType => EventType.ExecuteAttack;
public Limb AttackLimb { get; }
public IDamageable TargetEntity { get; }
public Limb TargetLimb { get; }
public Vector2 TargetSimPos { get; }
public ExecuteAttackEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos)
{
AttackLimb = attackLimb;
TargetEntity = targetEntity;
TargetLimb = targetLimb;
TargetSimPos = targetSimPos;
}
}
public struct AssignCampaignInteractionEventData : IEventData
{
public EventType EventType => EventType.AssignCampaignInteraction;
}
public struct ObjectiveManagerStateEventData : IEventData
{
public EventType EventType => EventType.ObjectiveManagerState;
public readonly AIObjectiveManager.ObjectiveType ObjectiveType;
public ObjectiveManagerStateEventData(AIObjectiveManager.ObjectiveType objectiveType)
{
ObjectiveType = objectiveType;
}
}
private struct TeamChangeEventData : IEventData
{
public EventType EventType => EventType.TeamChange;
}
public struct AddToCrewEventData : IEventData
{
public EventType EventType => EventType.AddToCrew;
public readonly CharacterTeamType TeamType;
public readonly ImmutableArray<Item> InventoryItems;
public AddToCrewEventData(CharacterTeamType teamType, IEnumerable<Item> inventoryItems)
{
TeamType = teamType;
InventoryItems = inventoryItems.ToImmutableArray();
}
}
public struct UpdateExperienceEventData : IEventData
{
public EventType EventType => EventType.UpdateExperience;
}
public struct UpdateTalentsEventData : IEventData
{
public EventType EventType => EventType.UpdateTalents;
}
public struct UpdateSkillsEventData : IEventData
{
public EventType EventType => EventType.UpdateSkills;
}
private struct UpdateMoneyEventData : IEventData
{
public EventType EventType => EventType.UpdateMoney;
}
public struct UpdatePermanentStatsEventData : IEventData
{
public EventType EventType => EventType.UpdatePermanentStats;
public readonly StatTypes StatType;
public UpdatePermanentStatsEventData(StatTypes statType)
{
StatType = statType;
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,19 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using static Barotrauma.CharacterInfo;
namespace Barotrauma
{
class CharacterPrefab : IPrefab, IDisposable
class CharacterPrefab : PrefabWithUintIdentifier, IImplementsVariants<CharacterPrefab>
{
public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>();
private bool disposed = false;
public void Dispose()
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
@@ -19,36 +21,51 @@ namespace Barotrauma
Character.RemoveByPrefab(this);
}
public string OriginalName { get; private set; }
public string Name { get; private set; }
public string Identifier { get; private set; }
public string FilePath { get; private set; }
public string VariantOf { get; private set; }
public string Name => Identifier.Value;
public Identifier VariantOf { get; }
public void InheritFrom(CharacterPrefab parent)
{
ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage);
ParseConfigElement();
}
public ContentPackage ContentPackage { get; private set; }
private void ParseConfigElement()
{
var headsElement = ConfigElement.GetChildElement("Heads");
var varsElement = ConfigElement.GetChildElement("Vars");
var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory");
var pronounsElement = ConfigElement.GetChildElement("Pronouns");
public XDocument XDocument { get; private set; }
if (headsElement != null)
{
CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement);
}
}
public static IEnumerable<string> ConfigFilePaths => Prefabs.Select(p => p.FilePath);
public static IEnumerable<XDocument> ConfigFiles => Prefabs.Select(p => p.XDocument);
private XElement originalElement;
public ContentXElement ConfigElement { get; private set; }
public const string HumanSpeciesName = "human";
public static string HumanConfigFile => FindBySpeciesName(HumanSpeciesName).FilePath;
public CharacterInfoPrefab CharacterInfoPrefab { get; private set; }
public static IEnumerable<ContentXElement> ConfigElements => Prefabs.Select(p => p.ConfigElement);
public static readonly Identifier HumanSpeciesName = "human".ToIdentifier();
public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile;
public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName);
/// <summary>
/// Searches for a character config file from all currently selected content packages,
/// or from a specific package if the contentPackage parameter is given.
/// </summary>
public static CharacterPrefab FindBySpeciesName(string speciesName)
public static CharacterPrefab FindBySpeciesName(Identifier speciesName)
{
speciesName = speciesName.ToLowerInvariant();
if (!Prefabs.ContainsKey(speciesName)) { return null; }
return Prefabs[speciesName];
}
public static CharacterPrefab FindByFilePath(string filePath)
{
return Prefabs.Find(p => p.FilePath.CleanUpPath() == filePath.CleanUpPath());
return Prefabs.Find(p => p.ContentFile.Path == filePath);
}
public static CharacterPrefab Find(Predicate<CharacterPrefab> predicate)
@@ -56,91 +73,38 @@ namespace Barotrauma
return Prefabs.Find(predicate);
}
public static void RemoveByFile(string file)
public CharacterPrefab(ContentXElement mainElement, CharacterFile file) : base(file, ParseName(mainElement, file))
{
Prefabs.RemoveByFile(file);
originalElement = mainElement;
ConfigElement = mainElement;
VariantOf = mainElement.VariantOf();
ParseConfigElement();
}
public static bool LoadFromFile(ContentFile file, bool forceOverride=false)
public static Identifier ParseName(XElement element, CharacterFile file)
{
return LoadFromFile(file.Path, file.ContentPackage, forceOverride);
}
public static bool LoadFromFile(string filePath, ContentPackage contentPackage, bool forceOverride=false)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null)
string name = element.GetAttributeString("name", null);
if (!string.IsNullOrEmpty(name))
{
DebugConsole.ThrowError($"Loading character file failed: {filePath}");
return false;
}
if (Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.FilePath == filePath)))
{
DebugConsole.ThrowError($"Duplicate path: {filePath}");
return false;
}
XElement mainElement = doc.Root;
if (doc.Root.IsCharacterVariant())
{
if (!CheckSpeciesName(mainElement, filePath, out string n)) { return false; }
string inherit = mainElement.GetAttributeString("inherit", null);
string id = n.ToLowerInvariant();
Prefabs.Add(new CharacterPrefab
{
Name = n,
OriginalName = n,
Identifier = id,
FilePath = filePath,
ContentPackage = contentPackage,
XDocument = doc,
VariantOf = inherit
}, isOverride: false);
return true;
}
else if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
}
if (!CheckSpeciesName(mainElement, filePath, out string name)) { return false; }
string identifier = name.ToLowerInvariant();
Prefabs.Add(new CharacterPrefab
{
Name = name,
OriginalName = name,
Identifier = identifier,
FilePath = filePath,
ContentPackage = contentPackage,
XDocument = doc
}, forceOverride || doc.Root.IsOverride());
return true;
}
public static bool CheckSpeciesName(XElement mainElement, string filePath, out string name)
{
name = mainElement.GetAttributeString("name", null);
if (name != null)
{
DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
name = mainElement.GetAttributeString("speciesname", string.Empty);
name = element.GetAttributeString("speciesname", string.Empty);
}
if (string.IsNullOrWhiteSpace(name))
return new Identifier(name);
}
public static bool CheckSpeciesName(XElement mainElement, CharacterFile file, out Identifier name)
{
name = ParseName(mainElement, file);
if (name == Identifier.Empty)
{
DebugConsole.ThrowError($"No species name defined for: {filePath}");
DebugConsole.ThrowError($"No species name defined for: {file.Path}");
return false;
}
return true;
}
public static void LoadAll()
{
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character))
{
LoadFromFile(file);
}
}
}
}
@@ -7,19 +7,19 @@ using System.Xml.Linq;
namespace Barotrauma
{
class CorpsePrefab : HumanPrefab, IPrefab, IDisposable
class CorpsePrefab : HumanPrefab
{
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
private bool disposed = false;
public void Dispose()
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public static CorpsePrefab Get(string identifier)
public static CorpsePrefab Get(Identifier identifier)
{
if (Prefabs == null)
{
@@ -37,99 +37,11 @@ namespace Barotrauma
}
}
[Serialize(Level.PositionType.Wreck, false)]
[Serialize(Level.PositionType.Wreck, IsPropertySaveable.No)]
public Level.PositionType SpawnPosition { get; private set; }
public ContentPackage ContentPackage { get; private set; }
public CorpsePrefab(XElement element, string filePath, bool allowOverriding) : base(element, filePath)
{
Prefabs.Add(this, allowOverriding);
}
public CorpsePrefab(ContentXElement element, CorpsesFile file) : base(element, file) { }
public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync);
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
DebugConsole.Log("*** " + file.Path + " ***");
RemoveByFile(file.Path);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
{
case "corpse":
new CorpsePrefab(rootElement, file.Path, false)
{
ContentPackage = file.ContentPackage
};
break;
case "corpses":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var itemElement = element.GetChildElement("item");
if (itemElement != null)
{
new CorpsePrefab(itemElement, file.Path, true)
{
ContentPackage = file.ContentPackage
};
}
else
{
DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}");
}
}
else
{
new CorpsePrefab(element, file.Path, false)
{
ContentPackage = file.ContentPackage
};
}
}
break;
case "override":
var corpses = rootElement.GetChildElement("corpses");
if (corpses != null)
{
foreach (var element in corpses.Elements())
{
new CorpsePrefab(element, file.Path, true)
{
ContentPackage = file.ContentPackage,
};
}
}
foreach (var element in rootElement.GetChildElements("corpse"))
{
new CorpsePrefab(element, file.Path, true)
{
ContentPackage = file.ContentPackage
};
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
}
}
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
}
}
@@ -12,7 +12,7 @@ namespace Barotrauma
public string Name => ToString();
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
public float PendingAdditionStrength { get; set; }
public float AdditionStrength { get; set; }
@@ -21,7 +21,7 @@ namespace Barotrauma
protected float _strength;
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public virtual float Strength
{
get { return _strength; }
@@ -43,10 +43,10 @@ namespace Barotrauma
private float _nonClampedStrength = -1;
public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength;
[Serialize("", true), Editable]
public string Identifier { get; private set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
public Identifier Identifier { get; private set; }
[Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
public float Probability { get; set; } = 1.0f;
public float DamagePerSecond;
@@ -73,7 +73,7 @@ namespace Barotrauma
Prefab = prefab;
PendingAdditionStrength = Prefab.GrainBurst;
_strength = strength;
Identifier = prefab?.Identifier;
Identifier = prefab.Identifier;
foreach (var periodicEffect in prefab.PeriodicEffects)
{
@@ -269,14 +269,15 @@ namespace Barotrauma
}
}
public float GetResistance(AfflictionPrefab affliction)
public float GetResistance(Identifier afflictionId)
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
var affliction = AfflictionPrefab.Prefabs[afflictionId];
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (!currentEffect.ResistanceFor.Any(r =>
r.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) ||
r.Equals(affliction.AfflictionType, StringComparison.OrdinalIgnoreCase)))
r == affliction.Identifier ||
r == affliction.AfflictionType))
{
return 0.0f;
}
@@ -26,7 +26,7 @@ namespace Barotrauma
private readonly List<Affliction> huskInfection = new List<Affliction>();
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public override float Strength
{
get { return _strength; }
@@ -41,9 +41,11 @@ namespace Barotrauma
if (previousValue > 0.0f && value <= 0.0f)
{
DeactivateHusk();
highestStrength = 0;
}
}
}
private float highestStrength;
public InfectionState State
{
@@ -75,6 +77,7 @@ namespace Barotrauma
{
if (HuskPrefab == null) { return; }
base.Update(characterHealth, targetLimb, deltaTime);
highestStrength = Math.Max(_strength, highestStrength);
character = characterHealth.Character;
if (character == null) { return; }
@@ -98,7 +101,7 @@ namespace Barotrauma
DeactivateHusk();
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
character.SpeechImpediment = 100;
character.SpeechImpediment = 30;
}
State = InfectionState.Transition;
}
@@ -108,6 +111,10 @@ namespace Barotrauma
{
character.SetStun(Rand.Range(2f, 3f));
}
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
character.SpeechImpediment = 100;
}
State = InfectionState.Active;
ActivateHusk();
}
@@ -120,7 +127,57 @@ namespace Barotrauma
}
}
partial void UpdateMessages();
private InfectionState? prevDisplayedMessage;
private void UpdateMessages()
{
if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; }
if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; }
if (highestStrength > Strength) { return; }
switch (State)
{
case InfectionState.Dormant:
if (Strength < DormantThreshold * 0.5f)
{
return;
}
if (character == Character.Controlled)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red);
#endif
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier());
}
break;
case InfectionState.Transition:
if (character == Character.Controlled)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red);
#endif
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier());
}
break;
case InfectionState.Active:
#if CLIENT
if (character == Character.Controlled && character.Params.UseHuskAppendage)
{
GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red);
}
#endif
break;
case InfectionState.Final:
default:
break;
}
prevDisplayedMessage = State;
}
private void ApplyDamage(float deltaTime, bool applyForce)
{
@@ -209,12 +266,14 @@ namespace Barotrauma
{
yield return CoroutineStatus.Success;
}
#if SERVER
var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character);
#endif
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
Entity.Spawner.AddEntityToRemoveQueue(character);
UnsubscribeFromDeathEvent();
string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
Identifier huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (prefab == null)
@@ -230,8 +289,8 @@ namespace Barotrauma
if (huskCharacterInfo != null)
{
var bodyTint = GetBodyTint();
huskCharacterInfo.SkinColor =
Color.Lerp(huskCharacterInfo.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f);
huskCharacterInfo.Head.SkinColor =
Color.Lerp(huskCharacterInfo.Head.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f);
}
var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true);
@@ -246,7 +305,6 @@ namespace Barotrauma
if (huskPrefab.ControlHusk || GameMain.Lua.game.enableControlHusk)
{
#if SERVER
var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character);
if (client != null)
{
GameMain.Server.SetClientCharacter(client, husk);
@@ -307,7 +365,7 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
public static List<Limb> AttachHuskAppendage(Character character, string afflictionIdentifier, XElement appendageDefinition = null, Ragdoll ragdoll = null)
public static List<Limb> AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
{
var appendage = new List<Limb>();
if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction))
@@ -315,26 +373,26 @@ namespace Barotrauma
DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!");
return appendage;
}
string nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
string huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (huskPrefab?.XDocument == null)
if (huskPrefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!");
return appendage;
}
var mainElement = huskPrefab.XDocument.Root.IsOverride() ? huskPrefab.XDocument.Root.FirstElement() : huskPrefab.XDocument.Root;
var mainElement = huskPrefab.ConfigElement;
var element = appendageDefinition;
if (element == null)
{
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier));
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier);
}
if (element == null)
{
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!");
return appendage;
}
string pathToAppendage = element.GetAttributeString("path", string.Empty);
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
if (doc == null) { return appendage; }
if (ragdoll == null)
@@ -345,10 +403,12 @@ namespace Barotrauma
{
ragdoll.Flip();
}
var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
foreach (var jointElement in doc.Root.Elements("joint"))
var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
foreach (var jointElement in root.GetChildElements("joint"))
{
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement))
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement))
{
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
@@ -389,15 +449,15 @@ namespace Barotrauma
return appendage;
}
public static string GetHuskedSpeciesName(string speciesName, AfflictionPrefabHusk prefab)
public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab)
{
return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName);
}
public static string GetNonHuskedSpeciesName(string huskedSpeciesName, AfflictionPrefabHusk prefab)
public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab)
{
string nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag);
return huskedSpeciesName.ToLowerInvariant().Remove(nonTag);
Identifier nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag);
return huskedSpeciesName.Remove(nonTag);
}
}
}
@@ -2,28 +2,29 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
static class CPRSettings
class CPRSettings : Prefab
{
public static string FilePath { get; private set; }
public static bool IsLoaded { get; private set; }
public static float ReviveChancePerSkill { get; private set; }
public static float ReviveChanceExponent { get; private set; }
public static float ReviveChanceMin { get; private set; }
public static float ReviveChanceMax { get; private set; }
public static float StabilizationPerSkill { get; private set; }
public static float StabilizationMin { get; private set; }
public static float StabilizationMax { get; private set; }
public static float DamageSkillThreshold { get; private set; }
public static float DamageSkillMultiplier { get; private set; }
public readonly static PrefabSelector<CPRSettings> Prefabs = new PrefabSelector<CPRSettings>();
public static CPRSettings Active => Prefabs.ActivePrefab;
private static string insufficientSkillAfflictionIdentifier { get; set; }
public static AfflictionPrefab InsufficientSkillAffliction
public readonly float ReviveChancePerSkill;
public readonly float ReviveChanceExponent;
public readonly float ReviveChanceMin;
public readonly float ReviveChanceMax;
public readonly float StabilizationPerSkill;
public readonly float StabilizationMin;
public readonly float StabilizationMax;
public readonly float DamageSkillThreshold;
public readonly float DamageSkillMultiplier;
private readonly string insufficientSkillAfflictionIdentifier;
public AfflictionPrefab InsufficientSkillAffliction
{
get
{
@@ -34,7 +35,7 @@ namespace Barotrauma
}
}
public static void Load(XElement element, string filePath)
public CPRSettings(XElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier())
{
ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f);
ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f);
@@ -49,33 +50,26 @@ namespace Barotrauma
DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f);
insufficientSkillAfflictionIdentifier = element.GetAttributeString("insufficientskillaffliction", "");
IsLoaded = true;
FilePath = filePath;
}
public static void Unload()
{
IsLoaded = false;
FilePath = null;
}
public override void Dispose() { }
}
class AfflictionPrefabHusk : AfflictionPrefab
{
public AfflictionPrefabHusk(XElement element, string filePath, Type type = null) : base(element, filePath, type)
public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type)
{
HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant();
if (HuskedSpeciesName == null)
HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty);
if (HuskedSpeciesName.IsEmpty)
{
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
HuskedSpeciesName = "[speciesname]husk";
HuskedSpeciesName = "[speciesname]husk".ToIdentifier();
}
TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true);
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
TargetSpecies = new string[] { "human" };
TargetSpecies = new Identifier[] { CharacterPrefab.HumanSpeciesName };
}
var attachElement = element.GetChildElement("attachlimb");
if (attachElement != null)
@@ -112,9 +106,9 @@ namespace Barotrauma
public float ActiveThreshold, DormantThreshold, TransitionThreshold;
public float TransformThresholdOnDeath;
public readonly string HuskedSpeciesName;
public readonly string[] TargetSpecies;
public const string Tag = "[speciesname]";
public readonly Identifier HuskedSpeciesName;
public readonly Identifier[] TargetSpecies;
public static readonly Identifier Tag = "[speciesname]".ToIdentifier();
public readonly bool TransferBuffs;
public readonly bool SendMessages;
@@ -123,124 +117,122 @@ namespace Barotrauma
public readonly bool ControlHusk;
}
partial class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier
class AfflictionPrefab : PrefabWithUintIdentifier
{
public class Effect
{
//this effect is applied when the strength is within this range
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinStrength { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxStrength { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinVitalityDecrease { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxVitalityDecrease { get; private set; }
//how much the strength of the affliction changes per second
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float StrengthChange { get; private set; }
[Serialize(false, false)]
[Serialize(false, IsPropertySaveable.No)]
public bool MultiplyByMaxVitality { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinScreenBlur { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxScreenBlur { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinScreenDistort { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxScreenDistort { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinRadialDistort { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxRadialDistort { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinChromaticAberration { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxChromaticAberration { get; private set; }
[Serialize("255,255,255,255", false)]
[Serialize("255,255,255,255", IsPropertySaveable.No)]
public Color GrainColor { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinGrainStrength { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxGrainStrength { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float ScreenEffectFluctuationFrequency { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MinAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MaxAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MinBuffMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MaxBuffMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MinSpeedMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MaxSpeedMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MinSkillMultiplier { get; private set; }
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float MaxSkillMultiplier { get; private set; }
private readonly Identifier[] resistanceFor;
public IReadOnlyList<Identifier> ResistanceFor => resistanceFor;
private readonly string[] resistanceFor;
public IEnumerable<string> ResistanceFor
{
get { return resistanceFor; }
}
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinResistance { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MaxResistance { get; private set; }
[Serialize("", false)]
public string DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier DialogFlag { get; private set; }
[Serialize("", false)]
public string Tag { get; private set; }
[Serialize("0,0,0,0", false)]
[Serialize("", IsPropertySaveable.No)]
public Identifier Tag { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
public Color MinFaceTint { get; private set; }
[Serialize("0,0,0,0", false)]
[Serialize("0,0,0,0", IsPropertySaveable.No)]
public Color MaxFaceTint { get; private set; }
[Serialize("0,0,0,0", false)]
[Serialize("0,0,0,0", IsPropertySaveable.No)]
public Color MinBodyTint { get; private set; }
[Serialize("0,0,0,0", false)]
[Serialize("0,0,0,0", IsPropertySaveable.No)]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character
/// </summary>
public string[] BlockTransformation { get; private set; }
public Identifier[] BlockTransformation { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
@@ -248,14 +240,14 @@ namespace Barotrauma
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public Effect(XElement element, string parentDebugName)
public Effect(ContentXElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true);
BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true);
resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>());
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>());
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -288,14 +280,14 @@ namespace Barotrauma
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
public PeriodicEffect(XElement element, string parentDebugName)
public PeriodicEffect(ContentXElement element, string parentDebugName)
{
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
if (element.Attribute("interval") != null)
if (element.GetAttribute("interval") != null)
{
MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f);
}
@@ -307,48 +299,28 @@ namespace Barotrauma
}
}
public static AfflictionPrefab InternalDamage;
public static AfflictionPrefab ImpactDamage;
public static AfflictionPrefab Bleeding;
public static AfflictionPrefab Burn;
public static AfflictionPrefab OxygenLow;
public static AfflictionPrefab Bloodloss;
public static AfflictionPrefab Pressure;
public static AfflictionPrefab Stun;
public static AfflictionPrefab RadiationSickness;
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"];
public static AfflictionPrefab Bleeding => Prefabs["bleeding"];
public static AfflictionPrefab Burn => Prefabs["burn"];
public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"];
public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"];
public static AfflictionPrefab Pressure => Prefabs["pressure"];
public static AfflictionPrefab Stun => Prefabs["stun"];
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override void Dispose() { }
public static IEnumerable<AfflictionPrefab> List
{
get
{
foreach (var prefab in Prefabs)
{
yield return prefab;
}
}
}
public string FilePath { get; private set; }
/// <summary>
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier { get; set; }
public static IEnumerable<AfflictionPrefab> List => Prefabs;
// Arbitrary string that is used to identify the type of the affliction.
public readonly string AfflictionType;
public readonly Identifier AfflictionType;
private readonly ContentXElement configElement;
//Does the affliction affect a specific limb or the whole character
public readonly bool LimbSpecific;
@@ -356,18 +328,14 @@ namespace Barotrauma
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public string Identifier { get; private set; }
public string OriginalName { get { return Identifier; } }
public ContentPackage ContentPackage { get; private set; }
public readonly string Name, Description;
public readonly string TranslationOverride;
public readonly LocalizedString Name, Description;
public readonly Identifier TranslationIdentifier;
public readonly bool IsBuff;
public readonly bool HealableInMedicalClinic;
public readonly float HealCostMultiplier;
public readonly int BaseHealCost;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
//how high the strength has to be for the affliction to take affect
public readonly float ActivationThreshold = 0.0f;
@@ -392,7 +360,7 @@ namespace Barotrauma
public float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character
public readonly string AchievementOnRemoved;
public readonly Identifier AchievementOnRemoved;
public readonly Sprite Icon;
public readonly Color[] IconColors;
@@ -407,11 +375,9 @@ namespace Barotrauma
public IList<PeriodicEffect> PeriodicEffects => periodicEffects;
private readonly string typeName;
private readonly ConstructorInfo constructor;
public IEnumerable<KeyValuePair<string, float>> TreatmentSuitability
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{
get
{
@@ -420,255 +386,32 @@ namespace Barotrauma
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
if (suitability > 0.0f)
{
yield return new KeyValuePair<string, float>(itemPrefab.Identifier, suitability);
yield return new KeyValuePair<Identifier, float>(itemPrefab.Identifier, suitability);
}
}
}
}
public static void LoadAll(IEnumerable<ContentFile> files)
public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
CPRSettings.Unload();
InternalDamage = null;
ImpactDamage = null;
Bleeding = null;
Burn = null;
OxygenLow = null;
Bloodloss = null;
Pressure = null;
Stun = null;
RadiationSickness = null;
#if CLIENT
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
CharacterHealth.DamageOverlayFile = string.Empty;
#endif
var prevPrefabs = Prefabs.AllPrefabs.SelectMany(kvp => kvp.Value).ToList();
foreach (var prefab in prevPrefabs)
{
prefab?.Dispose();
}
System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll");
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
if (InternalDamage == null) { DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs."); }
if (Bleeding == null) { DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs."); }
if (Burn == null) { DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs."); }
if (OxygenLow == null) { DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs."); }
if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); }
if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); }
if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); }
if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); }
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one.");
}
List<(AfflictionPrefab prefab, XElement element)> loadedAfflictions = new List<(AfflictionPrefab prefab, XElement element)>();
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) &&
!elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{file.Path}'");
continue;
}
if (Prefabs.ContainsKey(identifier))
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{file.Path}'");
continue;
}
}
}
string type = sourceElement.GetAttributeString("type", "");
switch (sourceElement.Name.ToString().ToLowerInvariant())
{
case "cprsettings":
type = "cprsettings";
break;
case "damageoverlay":
type = "damageoverlay";
break;
}
AfflictionPrefab prefab = null;
switch (type)
{
case "damageoverlay":
#if CLIENT
if (CharacterHealth.DamageOverlay != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding damage overlay with '{file.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{file.Path}': damage overlay already loaded. Add <override></override> tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one.");
break;
}
}
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = new Sprite(element);
CharacterHealth.DamageOverlayFile = file.Path;
#endif
break;
case "bleeding":
prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(AfflictionBleeding));
break;
case "huskinfection":
case "alieninfection":
prefab = new AfflictionPrefabHusk(sourceElement, file.Path, typeof(AfflictionHusk));
break;
case "cprsettings":
if (CPRSettings.IsLoaded)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding the CPR settings with '{file.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{file.Path}': CPR settings already loaded. Add <override></override> tags as the parent of the custom CPRSettings to allow overriding the vanilla values.");
break;
}
}
CPRSettings.Load(sourceElement, file.Path);
break;
case "damage":
case "burn":
case "oxygenlow":
case "bloodloss":
case "stun":
case "pressure":
case "internaldamage":
prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(Affliction))
{
ContentPackage = file.ContentPackage
};
break;
default:
prefab = new AfflictionPrefab(sourceElement, file.Path)
{
ContentPackage = file.ContentPackage
};
break;
}
switch (identifier)
{
case "internaldamage":
InternalDamage = prefab;
break;
case "blunttrauma":
ImpactDamage = prefab;
break;
case "bleeding":
Bleeding = prefab;
break;
case "burn":
Burn = prefab;
break;
case "oxygenlow":
OxygenLow = prefab;
break;
case "bloodloss":
Bloodloss = prefab;
break;
case "pressure":
Pressure = prefab;
break;
case "stun":
Stun = prefab;
break;
case "radiationsickness":
RadiationSickness = prefab;
break;
}
if (ImpactDamage == null) { ImpactDamage = InternalDamage; }
if (prefab != null)
{
loadedAfflictions.Add((prefab, sourceElement));
Prefabs.Add(prefab, isOverride);
prefab.CalculatePrefabUIntIdentifier(Prefabs);
}
}
//load the effects after all the afflictions in the file have been instantiated
//otherwise afflictions can't inflict other afflictions that are defined at a later point in the file
foreach ((AfflictionPrefab prefab, XElement element) in loadedAfflictions)
{
prefab.LoadEffects(element);
}
}
public static void RemoveByFile(string filePath)
{
if (CPRSettings.FilePath == filePath) { CPRSettings.Unload(); }
#if CLIENT
if (CharacterHealth.DamageOverlayFile == filePath)
{
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
}
#endif
Prefabs.RemoveByFile(filePath);
}
public AfflictionPrefab(XElement element, string filePath, Type type = null)
{
FilePath = filePath;
typeName = type == null ? element.Name.ToString() : type.Name;
if (typeName == "InternalDamage" && type == null)
{
type = typeof(Affliction);
}
Identifier = element.GetAttributeString("identifier", "");
AfflictionType = element.GetAttributeString("type", "");
TranslationOverride = element.GetAttributeString("translationoverride", null);
string translationId = TranslationOverride ?? Identifier;
Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", "");
configElement = element;
AfflictionType = element.GetAttributeIdentifier("type", "");
TranslationIdentifier = element.GetAttributeIdentifier("translationoverride", Identifier);
Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}").Fallback(element.GetAttributeString("name", ""));
Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}").Fallback(element.GetAttributeString("description", ""));
IsBuff = element.GetAttributeBool("isbuff", false);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
!IsBuff &&
!AfflictionType.Equals("geneticmaterialbuff", StringComparison.OrdinalIgnoreCase) &&
!AfflictionType.Equals("geneticmaterialdebuff", StringComparison.OrdinalIgnoreCase));
AfflictionType != "geneticmaterialbuff" &&
AfflictionType != "geneticmaterialdebuff");
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0);
if (element.Attribute("nameidentifier") != null)
if (element.GetAttribute("nameidentifier") != null)
{
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name;
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name);
}
LimbSpecific = element.GetAttributeBool("limbspecific", false);
@@ -687,7 +430,8 @@ namespace Barotrauma
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f));
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold",
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
@@ -695,14 +439,14 @@ namespace Barotrauma
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
IconColors = element.GetAttributeColorArray("iconcolors", null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -724,43 +468,42 @@ namespace Barotrauma
}
}
try
{
if (type == null)
{
type = Type.GetType("Barotrauma." + typeName, true, true);
if (type == null)
{
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
return;
}
}
}
catch
{
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
type = typeof(Affliction);
}
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
private void LoadEffects(XElement element)
public static void LoadAllEffects()
{
foreach (XElement subElement in element.Elements())
Prefabs.ForEach(p => p.LoadEffects());
}
public static void ClearAllEffects()
{
Prefabs.ForEach(p => p.ClearEffects());
}
public void LoadEffects()
{
ClearEffects();
foreach (var subElement in configElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "effect":
effects.Add(new Effect(subElement, Name));
effects.Add(new Effect(subElement, Name.Value));
break;
case "periodiceffect":
periodicEffects.Add(new PeriodicEffect(subElement, Name));
periodicEffects.Add(new PeriodicEffect(subElement, Name.Value));
break;
}
}
}
public void ClearEffects()
{
effects.Clear();
periodicEffects.Clear();
}
#if CLIENT
public void ReloadSoundsIfNeeded()
{
@@ -770,7 +513,7 @@ namespace Barotrauma
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); }
if (sound.Sound == null) { RoundSound.Reload(sound); }
}
}
}
@@ -780,7 +523,7 @@ namespace Barotrauma
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); }
if (sound.Sound == null) { RoundSound.Reload(sound); }
}
}
}
@@ -789,7 +532,7 @@ namespace Barotrauma
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
return $"AfflictionPrefab ({Name})";
}
public Affliction Instantiate(float strength, Character source = null)
@@ -41,7 +41,7 @@ namespace Barotrauma
invertControlsToggleTimer = 5.0f;
if (Rand.Range(0.0f, 1.0f) < 0.5f)
{
characterHealth.ReduceAffliction(null, "invertcontrols", 100);
characterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100);
}
else
{
@@ -20,23 +20,23 @@ namespace Barotrauma
public Rectangle HighlightArea;
public readonly string Name;
public readonly LocalizedString Name;
//public readonly List<Affliction> Afflictions = new List<Affliction>();
public readonly Dictionary<string, float> VitalityMultipliers = new Dictionary<string, float>();
public readonly Dictionary<string, float> VitalityTypeMultipliers = new Dictionary<string, float>();
public readonly Dictionary<Identifier, float> VitalityMultipliers = new Dictionary<Identifier, float>();
public readonly Dictionary<Identifier, float> VitalityTypeMultipliers = new Dictionary<Identifier, float>();
public LimbHealth() { }
public LimbHealth(XElement element, CharacterHealth characterHealth)
public LimbHealth(ContentXElement element, CharacterHealth characterHealth)
{
string limbName = element.GetAttributeString("name", null) ?? "generic";
if (limbName != "generic")
{
Name = TextManager.Get("HealthLimbName." + limbName);
}
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -48,22 +48,26 @@ namespace Barotrauma
HighlightSprite = new Sprite(subElement);
break;
case "vitalitymultiplier":
if (subElement.Attribute("name") != null)
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.");
continue;
}
string afflictionIdentifier = subElement.GetAttributeString("identifier", "");
string afflictionType = subElement.GetAttributeString("type", "");
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
if (!string.IsNullOrEmpty(afflictionIdentifier))
var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
if (vitalityMultipliers != null)
{
VitalityMultipliers.Add(afflictionIdentifier.ToLowerInvariant(), multiplier);
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier));
}
else
var vitalityTypeMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null);
if (vitalityTypeMultipliers != null)
{
VitalityTypeMultipliers.Add(afflictionType.ToLowerInvariant(), multiplier);
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
vitalityTypeMultipliers.ForEach(i => VitalityTypeMultipliers.Add(i, multiplier));
}
if (vitalityMultipliers == null && VitalityTypeMultipliers == null)
{
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!");
}
break;
}
@@ -220,7 +224,7 @@ namespace Barotrauma
InitProjSpecific(null, character);
}
public CharacterHealth(XElement element, Character character, XElement limbHealthElement = null)
public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null)
{
this.Character = character;
InitIrremovableAfflictions();
@@ -231,7 +235,7 @@ namespace Barotrauma
limbHealths.Clear();
limbHealthElement ??= element;
foreach (XElement subElement in limbHealthElement.Elements())
foreach (var subElement in limbHealthElement.Elements())
{
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
@@ -256,7 +260,7 @@ namespace Barotrauma
}
}
partial void InitProjSpecific(XElement element, Character character);
partial void InitProjSpecific(ContentXElement element, Character character);
public IReadOnlyCollection<Affliction> GetAllAfflictions()
{
@@ -283,10 +287,13 @@ namespace Barotrauma
private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) =>
GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions);
public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true)
public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
@@ -411,7 +418,7 @@ namespace Barotrauma
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab);
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
}
@@ -438,37 +445,58 @@ namespace Barotrauma
}
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAffliction(Limb targetLimb, string afflictionIdentifier, float amount, ActionType? treatmentAction = null)
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
{
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
if (targetLimb == null)
{
matchingAfflictions.AddRange(afflictions.Keys);
}
else
{
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
if (kvp.Value == null)
{
matchingAfflictions.Add(affliction);
}
else if (limbHealths[targetLimb.HealthIndex] == kvp.Value)
{
matchingAfflictions.Add(affliction);
}
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null)
{
if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier != affliction &&
a.Prefab.AfflictionType != affliction);
ReduceMatchingAfflictions(amount, treatmentAction);
}
if (!string.IsNullOrEmpty(afflictionIdentifier))
{
matchingAfflictions.RemoveAll(a =>
!a.Prefab.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase));
}
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
=> afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]);
public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null)
{
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null)
{
if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier != affliction &&
a.Prefab.AfflictionType != affliction);
ReduceMatchingAfflictions(amount, treatmentAction);
}
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction)
{
if (matchingAfflictions.Count == 0) { return; }
float reduceAmount = amount / matchingAfflictions.Count;
@@ -640,9 +668,10 @@ namespace Barotrauma
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase)))
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
{
return;
}
@@ -719,48 +748,47 @@ namespace Barotrauma
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
if (Character.GodMode) { return; }
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
if (!Character.GodMode)
{
var affliction = kvp.Key;
if (affliction.Strength <= 0.0f)
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
var affliction = kvp.Key;
if (affliction.Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
afflictionsToUpdate.Add(kvp);
}
afflictionsToUpdate.Add(kvp);
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
{
var affliction = kvp.Key;
Limb targetLimb = null;
if (kvp.Value != null)
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
{
int healthIndex = limbHealths.IndexOf(kvp.Value);
targetLimb =
Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
Character.AnimController.MainLimb;
var affliction = kvp.Key;
Limb targetLimb = null;
if (kvp.Value != null)
{
int healthIndex = limbHealths.IndexOf(kvp.Value);
targetLimb =
Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
Character.AnimController.MainLimb;
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding bleeding)
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding bleeding)
foreach (var affliction in afflictionsToRemove)
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
afflictions.Remove(affliction);
}
}
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
if (Character.InWater)
{
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed));
@@ -770,13 +798,16 @@ namespace Barotrauma
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed));
}
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
if (!Character.GodMode)
{
Kill();
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
}
}
@@ -965,7 +996,7 @@ namespace Barotrauma
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
/// <param name="normalize">If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable.</param>
/// <param name="predictFutureDuration">If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds.</param>
public void GetSuitableTreatments(Dictionary<string, float> treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
{
//key = item identifier
//float = suitability
@@ -991,7 +1022,7 @@ namespace Barotrauma
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
if (ignoreHiddenAfflictions && strength < affliction.Prefab.ShowIconThreshold) { continue; }
foreach (KeyValuePair<string, float> treatment in affliction.Prefab.TreatmentSuitability)
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitability)
{
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
@@ -1008,23 +1039,23 @@ namespace Barotrauma
//normalize the suitabilities to a range of 0 to 1
if (normalize)
{
foreach (string treatment in treatmentSuitability.Keys.ToList())
foreach (Identifier treatment in treatmentSuitability.Keys.ToList())
{
treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability);
}
}
}
public IEnumerable<string> GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys);
public IEnumerable<Identifier> GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys);
private readonly HashSet<string> afflictionTags = new HashSet<string>();
public IEnumerable<string> GetActiveAfflictionTags(IEnumerable<Affliction> afflictions)
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags(IEnumerable<Affliction> afflictions)
{
afflictionTags.Clear();
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.Tag))
if (currentEffect != null && !currentEffect.Tag.IsEmpty)
{
afflictionTags.Add(currentEffect.Tag);
}
@@ -1048,10 +1079,10 @@ namespace Barotrauma
}
foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction)
{
if (statusEffectAffliction.affliction.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) ||
statusEffectAffliction.affliction.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase))
if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier ||
statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType)
{
strength -= statusEffectAffliction.amount * statusEffectDuration;
strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration;
}
}
}
@@ -1076,7 +1107,7 @@ namespace Barotrauma
msg.Write((byte)activeAfflictions.Count);
foreach (Affliction affliction in activeAfflictions)
{
msg.Write(affliction.Prefab.UIntIdentifier);
msg.Write(affliction.Prefab.UintIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
@@ -1101,7 +1132,7 @@ namespace Barotrauma
foreach (var (limbHealth, affliction) in limbAfflictions)
{
msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1);
msg.Write(affliction.Prefab.UIntIdentifier);
msg.Write(affliction.Prefab.UintIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
@@ -1156,7 +1187,7 @@ namespace Barotrauma
public void Load(XElement element)
{
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -2,6 +2,7 @@
using System;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
@@ -10,23 +11,23 @@ namespace Barotrauma
{
public string Name => "Damage Modifier";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
[Serialize(1.0f, false), Editable(DecimalCount = 2)]
[Serialize(1.0f, IsPropertySaveable.No), Editable(DecimalCount = 2)]
public float DamageMultiplier
{
get;
private set;
}
[Serialize(1.0f, false), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)]
[Serialize(1.0f, IsPropertySaveable.No), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)]
public float ProbabilityMultiplier
{
get;
private set;
}
[Serialize("0.0,360", false), Editable]
[Serialize("0.0,360", IsPropertySaveable.No), Editable]
public Vector2 ArmorSector
{
get;
@@ -35,14 +36,14 @@ namespace Barotrauma
public Vector2 ArmorSectorInRadians => new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
[Serialize(false, false), Editable]
[Serialize(false, IsPropertySaveable.No), Editable]
public bool DeflectProjectiles
{
get;
private set;
}
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string AfflictionIdentifiers
{
get
@@ -56,7 +57,7 @@ namespace Barotrauma
}
}
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string AfflictionTypes
{
get
@@ -72,22 +73,11 @@ namespace Barotrauma
private string rawAfflictionIdentifierString;
private string rawAfflictionTypeString;
private string[] parsedAfflictionIdentifiers;
private string[] parsedAfflictionTypes;
public string[] ParsedAfflictionIdentifiers
{
get
{
return parsedAfflictionIdentifiers;
}
}
public string[] ParsedAfflictionTypes
{
get
{
return parsedAfflictionTypes;
}
}
private ImmutableArray<Identifier> parsedAfflictionIdentifiers;
private ImmutableArray<Identifier> parsedAfflictionTypes;
public ref readonly ImmutableArray<Identifier> ParsedAfflictionIdentifiers => ref parsedAfflictionIdentifiers;
public ref readonly ImmutableArray<Identifier> ParsedAfflictionTypes => ref parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName)
{
@@ -102,55 +92,58 @@ namespace Barotrauma
{
if (string.IsNullOrWhiteSpace(rawAfflictionTypeString))
{
parsedAfflictionTypes = new string[0];
parsedAfflictionTypes = Enumerable.Empty<Identifier>().ToImmutableArray();
return;
}
string[] splitValue = rawAfflictionTypeString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
splitValue[i] = splitValue[i].ToLowerInvariant().Trim();
}
parsedAfflictionTypes = splitValue;
parsedAfflictionTypes = rawAfflictionTypeString.Split(',', '')
.Select(s => s.Trim()).ToIdentifiers().ToImmutableArray();
}
private void ParseAfflictionIdentifiers()
{
if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString))
{
parsedAfflictionIdentifiers = new string[0];
parsedAfflictionIdentifiers = Enumerable.Empty<Identifier>().ToImmutableArray();
return;
}
string[] splitValue = rawAfflictionIdentifierString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
splitValue[i] = splitValue[i].ToLowerInvariant().Trim();
}
parsedAfflictionIdentifiers = splitValue;
parsedAfflictionIdentifiers = rawAfflictionIdentifierString.Split(',', '')
.Select(s => s.Trim()).ToIdentifiers().ToImmutableArray();
}
public bool MatchesAfflictionIdentifier(string identifier)
public bool MatchesAfflictionIdentifier(string identifier) =>
MatchesAfflictionIdentifier(identifier.ToIdentifier());
public bool MatchesAfflictionIdentifier(Identifier identifier)
{
//if no identifiers have been defined, the damage modifier affects all afflictions
if (AfflictionIdentifiers.Length == 0) { return true; }
return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase));
return parsedAfflictionIdentifiers.Any(id => id == identifier);
}
public bool MatchesAfflictionType(string type)
public bool MatchesAfflictionType(string type) =>
MatchesAfflictionType(type.ToIdentifier());
public bool MatchesAfflictionType(Identifier type)
{
//if no types have been defined, the damage modifier affects all afflictions
if (AfflictionTypes.Length == 0) { return true; }
return parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase));
return parsedAfflictionTypes.Any(t => t == type);
}
/// <summary>
/// Returns true if the type or the identifier matches the defined types/identifiers.
/// </summary>
public bool MatchesAffliction(string identifier, string type)
public bool MatchesAffliction(string identifier, string type) =>
MatchesAffliction(identifier.ToIdentifier(), type.ToIdentifier());
public bool MatchesAffliction(Identifier identifier, Identifier type)
{
//if no identifiers or types have been defined, the damage modifier affects all afflictions
if (AfflictionIdentifiers.Length == 0 && AfflictionTypes.Length == 0) { return true; }
return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase))
|| parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase));
return parsedAfflictionIdentifiers.Any(id => id == identifier)
|| parsedAfflictionTypes.Any(t => t == type);
}
public bool MatchesAffliction(Affliction affliction) => MatchesAffliction(affliction.Identifier, affliction.Prefab.AfflictionType);
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using System;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
@@ -6,32 +7,29 @@ using System.Xml.Linq;
namespace Barotrauma
{
class HumanPrefab
class HumanPrefab : PrefabWithUintIdentifier
{
[Serialize("notfound", false)]
public string Identifier { get; protected set; }
[Serialize("any", false)]
[Serialize("any", IsPropertySaveable.No)]
public string Job { get; protected set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float Commonness { get; protected set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float HealthMultiplier { get; protected set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float HealthMultiplierInMultiplayer { get; protected set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float AimSpeed { get; protected set; }
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float AimAccuracy { get; protected set; }
private readonly HashSet<string> moduleFlags = new HashSet<string>();
private readonly HashSet<Identifier> moduleFlags = new HashSet<Identifier>();
[Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")]
[Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")]
public string ModuleFlags
{
get => string.Join(",", moduleFlags);
@@ -43,16 +41,16 @@ namespace Barotrauma
string[] splitFlags = value.Split(',');
foreach (var f in splitFlags)
{
moduleFlags.Add(f);
moduleFlags.Add(f.ToIdentifier());
}
}
}
}
private readonly HashSet<string> spawnPointTags = new HashSet<string>();
private readonly HashSet<Identifier> spawnPointTags = new HashSet<Identifier>();
[Serialize("", true, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")]
[Serialize("", IsPropertySaveable.Yes, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")]
public string SpawnPointTags
{
get => string.Join(",", spawnPointTags);
@@ -64,27 +62,22 @@ namespace Barotrauma
string[] splitTags = value.Split(',');
foreach (var tag in splitTags)
{
spawnPointTags.Add(tag.ToLowerInvariant());
spawnPointTags.Add(tag.ToIdentifier());
}
}
}
}
[Serialize(CampaignMode.InteractionType.None, false)]
[Serialize(CampaignMode.InteractionType.None, IsPropertySaveable.No)]
public CampaignMode.InteractionType CampaignInteractionType { get; protected set; }
[Serialize(AIObjectiveIdle.BehaviorType.Passive, false)]
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
public AIObjectiveIdle.BehaviorType Behavior { get; protected set; }
[Serialize(float.PositiveInfinity, false)]
[Serialize(float.PositiveInfinity, IsPropertySaveable.No)]
public float ReportRange { get; protected set; }
public List<string> PreferredOutpostModuleTypes { get; protected set; }
public string OriginalName { get { return Identifier; } }
public string FilePath { get; protected set; }
public Identifier[] PreferredOutpostModuleTypes { get; protected set; }
public XElement Element { get; protected set; }
@@ -92,24 +85,22 @@ namespace Barotrauma
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
public readonly Dictionary<XElement, float> CustomNPCSets = new Dictionary<XElement, float>();
public HumanPrefab(XElement element, string filePath)
public HumanPrefab(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Identifier = Identifier.ToLowerInvariant();
Job = Job.ToLowerInvariant();
Element = element;
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1)));
PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList();
PreferredOutpostModuleTypes = element.GetAttributeIdentifierArray("preferredoutpostmoduletypes", Array.Empty<Identifier>());
}
public IEnumerable<string> GetModuleFlags()
public IEnumerable<Identifier> GetModuleFlags()
{
return moduleFlags;
}
public IEnumerable<string> GetSpawnPointTags()
public IEnumerable<Identifier> GetSpawnPointTags()
{
return spawnPointTags;
}
@@ -139,7 +130,7 @@ namespace Barotrauma
else
{
idleObjective.Behavior = Behavior;
foreach (string moduleType in PreferredOutpostModuleTypes)
foreach (Identifier moduleType in PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
@@ -180,7 +171,7 @@ namespace Barotrauma
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
@@ -199,7 +190,7 @@ namespace Barotrauma
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
if (itemElement.GetAttributeBool("equip", false))
@@ -217,28 +208,21 @@ namespace Barotrauma
{
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
IdCard idCardComponent = item.GetComponent<IdCard>();
if (idCardComponent != null)
{
item.AddTag("name:" + character.Name);
var job = character.Info?.Job;
if (job != null)
{
item.AddTag("job:" + job.Name);
}
IdCard idCardComponent = item.GetComponent<IdCard>();
idCardComponent?.Initialize(character.Info);
idCardComponent.Initialize(null, character);
if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost))
{
idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag;
}
var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]);
var idCardTags = itemElement.GetAttributeStringArray("tags", Array.Empty<string>());
foreach (string tag in idCardTags)
{
item.AddTag(tag);
}
}
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
@@ -250,5 +234,7 @@ namespace Barotrauma
InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents);
}
}
public override void Dispose() { }
}
}
@@ -9,87 +9,87 @@ namespace Barotrauma
{
private readonly JobPrefab prefab;
private readonly Dictionary<string, Skill> skills;
private readonly Dictionary<Identifier, Skill> skills;
public string Name
{
get { return prefab.Name; }
}
public LocalizedString Name => prefab.Name;
public string Description
{
get { return prefab.Description; }
}
public LocalizedString Description => prefab.Description;
public JobPrefab Prefab
{
get { return prefab; }
}
public List<Skill> Skills
{
get { return skills.Values.ToList(); }
}
public JobPrefab Prefab => prefab;
public List<Skill> Skills => skills.Values.ToList();
public int Variant;
public Skill PrimarySkill { get; }
public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0)
public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { }
public Job(JobPrefab jobPrefab, Rand.RandSync randSync, int variant, params Skill[] s)
{
prefab = jobPrefab;
Variant = variant;
skills = new Dictionary<string, Skill>();
skills = new Dictionary<Identifier, Skill>();
foreach (var skill in s) { skills.Add(skill.Identifier, skill); }
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
var skill = new Skill(skillPrefab, randSync);
skills.Add(skillPrefab.Identifier, skill);
Skill skill;
if (skills.ContainsKey(skillPrefab.Identifier))
{
skill = skills[skillPrefab.Identifier];
skills[skillPrefab.Identifier] = new Skill(skill.Identifier, skill.Level);
}
else
{
skill = new Skill(skillPrefab, randSync);
skills.Add(skillPrefab.Identifier, skill);
}
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
}
}
public Job(XElement element)
{
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
JobPrefab p;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
p = JobPrefab.Random();
p = JobPrefab.Random(Rand.RandSync.Unsynced);
}
else
{
p = JobPrefab.Prefabs[identifier];
}
prefab = p;
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
skills = new Dictionary<Identifier, Skill>();
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
if (subElement.NameAsIdentifier() != "skill") { continue; }
Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", "");
if (skillIdentifier.IsEmpty) { continue; }
var skill = new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0));
skills.Add(skillIdentifier, skill);
if (skillIdentifier == prefab.PrimarySkill?.Identifier) { PrimarySkill = skill; }
}
}
public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced)
public static Job Random(Rand.RandSync randSync)
{
var prefab = JobPrefab.Random(randSync);
var variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, randSync, variant);
}
public float GetSkillLevel(string skillIdentifier)
public float GetSkillLevel(Identifier skillIdentifier)
{
if (string.IsNullOrWhiteSpace(skillIdentifier)) { return 0.0f; }
if (skillIdentifier.IsEmpty) { return 0.0f; }
skills.TryGetValue(skillIdentifier, out Skill skill);
return (skill == null) ? 0.0f : skill.Level;
return skill?.Level ?? 0.0f;
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, bool increasePastMax)
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool increasePastMax)
{
if (skills.TryGetValue(skillIdentifier, out Skill skill))
{
@@ -130,7 +130,7 @@ namespace Barotrauma
else
{
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
@@ -152,7 +152,7 @@ namespace Barotrauma
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
@@ -192,27 +192,16 @@ namespace Barotrauma
if (item.Prefab.Identifier == "idcard")
{
if (spawnPoint != null)
{
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) { item.Description = spawnPoint.IdCardDesc; }
}
}
item.AddTag("name:" + character.Name);
item.AddTag("job:" + Name);
IdCard idCardComponent = item.GetComponent<IdCard>();
idCardComponent?.Initialize(character.Info);
idCardComponent?.Initialize(spawnPoint, character);
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null) parentItem.Combine(item, user: null);
if (parentItem != null) { parentItem.Combine(item, user: null); }
foreach (XElement childItemElement in itemElement.Elements())
{
@@ -227,7 +216,7 @@ namespace Barotrauma
jobElement.Add(new XAttribute("name", Name));
jobElement.Add(new XAttribute("identifier", prefab.Identifier));
foreach (KeyValuePair<string, Skill> skill in skills)
foreach (KeyValuePair<Identifier, Skill> skill in skills)
{
jobElement.Add(new XElement("skill", new XAttribute("identifier", skill.Value.Identifier), new XAttribute("level", skill.Value.Level)));
}
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -9,54 +10,78 @@ namespace Barotrauma
{
public class AutonomousObjective
{
public string identifier;
public string option;
public readonly float priorityModifier;
public readonly bool ignoreAtOutpost;
public readonly Identifier Identifier;
public readonly Identifier Option;
public readonly float PriorityModifier;
public readonly bool IgnoreAtOutpost;
public AutonomousObjective(XElement element)
{
identifier = element.GetAttributeString("identifier", null);
Identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
//backwards compatibility
if (string.IsNullOrEmpty(identifier))
if (Identifier == Identifier.Empty)
{
identifier = element.GetAttributeString("aitag", null);
Identifier = element.GetAttributeIdentifier("aitag", Identifier.Empty);
}
option = element.GetAttributeString("option", null);
priorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
priorityModifier = MathHelper.Max(priorityModifier, 0);
ignoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false);
Option = element.GetAttributeIdentifier("option", Identifier.Empty);
PriorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
PriorityModifier = MathHelper.Max(PriorityModifier, 0);
IgnoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false);
}
}
partial class JobPrefab : IPrefab, IDisposable
class ItemRepairPriority : Prefab
{
public static readonly PrefabCollection<ItemRepairPriority> Prefabs = new PrefabCollection<ItemRepairPriority>();
public readonly float Priority;
public ItemRepairPriority(XElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("tag", Identifier.Empty))
{
Priority = element.GetAttributeFloat("priority", -1f);
if (Priority < 0)
{
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.");
}
}
public override void Dispose() { }
}
class JobVariant
{
public JobPrefab Prefab;
public int Variant;
public JobVariant(JobPrefab prefab, int variant)
{
Prefab = prefab;
Variant = variant;
}
}
partial class JobPrefab : PrefabWithUintIdentifier
{
public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>();
private bool disposed = false;
public void Dispose()
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
private static readonly Dictionary<string, float> _itemRepairPriorities = new Dictionary<string, float>();
private static readonly Dictionary<Identifier, float> _itemRepairPriorities = new Dictionary<Identifier, float>();
/// <summary>
/// Tag -> priority.
/// </summary>
public static IReadOnlyDictionary<string, float> ItemRepairPriorities => _itemRepairPriorities;
public static IReadOnlyDictionary<Identifier, float> ItemRepairPriorities => _itemRepairPriorities;
public static XElement NoJobElement;
public static ContentXElement NoJobElement;
public static JobPrefab Get(string identifier)
{
if (Prefabs == null)
{
DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded.");
return null;
}
if (Prefabs.ContainsKey(identifier))
{
return Prefabs[identifier];
@@ -70,62 +95,41 @@ namespace Barotrauma
public class PreviewItem
{
public readonly string ItemIdentifier;
public readonly Identifier ItemIdentifier;
public readonly bool ShowPreview;
public PreviewItem(string itemIdentifier, bool showPreview)
public PreviewItem(Identifier itemIdentifier, bool showPreview)
{
ItemIdentifier = itemIdentifier;
ShowPreview = showPreview;
}
}
public readonly Dictionary<int, XElement> ItemSets = new Dictionary<int, XElement>();
public readonly Dictionary<int, List<PreviewItem>> PreviewItems = new Dictionary<int, List<PreviewItem>>();
public readonly Dictionary<int, ContentXElement> ItemSets = new Dictionary<int, ContentXElement>();
public readonly ImmutableDictionary<int, ImmutableArray<PreviewItem>> PreviewItems;
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutonomousObjectives = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
public readonly List<Identifier> AppropriateOrders = new List<Identifier>();
[Serialize("1,1,1,1", false)]
[Serialize("1,1,1,1", IsPropertySaveable.No)]
public Color UIColor
{
get;
private set;
}
[Serialize("notfound", false)]
public string Identifier
{
get;
private set;
}
public readonly LocalizedString Name;
[Serialize("notfound", false)]
public string Name
{
get;
private set;
}
[Serialize(AIObjectiveIdle.BehaviorType.Passive, false)]
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
public AIObjectiveIdle.BehaviorType IdleBehavior
{
get;
private set;
}
public string OriginalName { get { return Identifier; } }
public readonly LocalizedString Description;
public ContentPackage ContentPackage { get; private set; }
[Serialize("", false)]
public string Description
{
get;
private set;
}
[Serialize(false, false)]
[Serialize(false, IsPropertySaveable.No)]
public bool OnlyJobSpecificDialog
{
get;
@@ -133,7 +137,7 @@ namespace Barotrauma
}
//the number of these characters in the crew the player starts with in the single player campaign
[Serialize(0, false)]
[Serialize(0, IsPropertySaveable.No)]
public int InitialCount
{
get;
@@ -141,7 +145,7 @@ namespace Barotrauma
}
//if set to true, a client that has chosen this as their preferred job will get it no matter what
[Serialize(false, false)]
[Serialize(false, IsPropertySaveable.No)]
public bool AllowAlways
{
get;
@@ -149,7 +153,7 @@ namespace Barotrauma
}
//how many crew members can have the job (only one captain etc)
[Serialize(100, false)]
[Serialize(100, IsPropertySaveable.No)]
public int MaxNumber
{
get;
@@ -158,21 +162,21 @@ namespace Barotrauma
//how many crew members are REQUIRED to have the job
//(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference)
[Serialize(0, false)]
[Serialize(0, IsPropertySaveable.No)]
public int MinNumber
{
get;
private set;
}
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float MinKarma
{
get;
private set;
}
[Serialize(1.0f, false)]
[Serialize(1.0f, IsPropertySaveable.No)]
public float PriceMultiplier
{
get;
@@ -180,7 +184,7 @@ namespace Barotrauma
}
// TODO: not used
[Serialize(10.0f, false)]
[Serialize(10.0f, IsPropertySaveable.No)]
public float Commonness
{
get;
@@ -188,7 +192,7 @@ namespace Barotrauma
}
//how much the vitality of the character is increased/reduced from the default value
[Serialize(0.0f, false)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float VitalityModifier
{
get;
@@ -196,7 +200,7 @@ namespace Barotrauma
}
//whether the job should be available to NPCs
[Serialize(false, false)]
[Serialize(false, IsPropertySaveable.No)]
public bool HiddenJob
{
get;
@@ -208,35 +212,33 @@ namespace Barotrauma
public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill);
public string FilePath { get; private set; }
public XElement Element { get; private set; }
public XElement ClothingElement { get; private set; }
public ContentXElement Element { get; private set; }
public ContentXElement ClothingElement { get; private set; }
public int Variants { get; private set; }
public JobPrefab(XElement element, string filePath)
public JobPrefab(ContentXElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier, returnNull: true) ?? string.Empty;
Identifier = Identifier.ToLowerInvariant();
Description = TextManager.Get("JobDescription." + Identifier);
Element = element;
var previewItems = new Dictionary<int, List<PreviewItem>>();
int variant = 0;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "itemset":
ItemSets.Add(variant, subElement);
PreviewItems[variant] = new List<PreviewItem>();
previewItems[variant] = new List<PreviewItem>();
loadItemIdentifiers(subElement, variant);
variant++;
break;
case "skills":
foreach (XElement skillElement in subElement.Elements())
foreach (var skillElement in subElement.Elements())
{
Skills.Add(new SkillPrefab(skillElement));
}
@@ -246,7 +248,7 @@ namespace Barotrauma
break;
case "appropriateobjectives":
case "appropriateorders":
subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant()));
subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeIdentifier("identifier", "")));
break;
case "jobicon":
Icon = new Sprite(subElement.FirstElement());
@@ -267,19 +269,22 @@ namespace Barotrauma
continue;
}
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
if (string.IsNullOrWhiteSpace(itemIdentifier))
Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (itemIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier.");
}
else
{
PreviewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true)));
previewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true)));
}
loadItemIdentifiers(itemElement, variant);
}
}
PreviewItems = previewItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray()))
.ToImmutableDictionary();
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
@@ -287,77 +292,7 @@ namespace Barotrauma
// Disabled on purpose, TODO: remove all references?
//ClothingElement = element.GetChildElement("PortraitClothing");
}
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => !p.HiddenJob, sync);
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
DebugConsole.ThrowError($"Error in '{file.Path}': Cannot override all job prefabs, because many of them are required by the main game! Please try overriding jobs one by one.");
}
foreach (XElement element in mainElement.Elements())
{
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement(), file.Path)
{
ContentPackage = file.ContentPackage
};
Prefabs.Add(job, true);
}
else
{
if (!element.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) { continue; }
var job = new JobPrefab(element, file.Path)
{
ContentPackage = file.ContentPackage
};
Prefabs.Add(job, false);
}
}
NoJobElement ??= mainElement.GetChildElement("nojob");
var itemRepairPrioritiesElement = mainElement.GetChildElement("ItemRepairPriorities");
if (itemRepairPrioritiesElement != null)
{
foreach (var subElement in itemRepairPrioritiesElement.Elements())
{
string tag = subElement.GetAttributeString("tag", null);
if (tag != null)
{
float priority = subElement.GetAttributeFloat("priority", -1f);
if (priority >= 0)
{
_itemRepairPriorities.TryAdd(tag, priority);
}
else
{
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}.");
}
}
else
{
DebugConsole.AddWarning($"The 'tag' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}.");
}
}
}
}
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync);
}
}
@@ -4,12 +4,12 @@ namespace Barotrauma
{
class Skill
{
private float level;
public string Identifier { get; }
public readonly Identifier Identifier;
public const float MaximumSkill = 100.0f;
private float level;
public float Level
{
get { return level; }
@@ -21,18 +21,11 @@ namespace Barotrauma
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
}
private Sprite icon;
public Sprite Icon
{
get
{
if (icon == null)
{
icon = GetIcon();
}
return icon;
}
}
private Identifier iconJobId;
public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab)
? jobPrefab.Icon
: null;
public readonly float PriceMultiplier = 1.0f;
@@ -40,39 +33,42 @@ namespace Barotrauma
{
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
icon = GetIcon();
iconJobId = GetIconJobId();
PriceMultiplier = prefab.PriceMultiplier;
}
public Skill(string identifier, float level)
public Skill(Identifier identifier, float level)
{
Identifier = identifier;
this.level = level;
icon = GetIcon();
iconJobId = GetIconJobId();
}
private Sprite GetIcon()
private Identifier GetIconJobId()
{
string jobId = null;
switch (Identifier.ToLowerInvariant())
Identifier jobId = Identifier.Empty;
if (Identifier == "electrical")
{
case "electrical":
jobId = "engineer";
break;
case "helm":
jobId = "captain";
break;
case "mechanical":
jobId = "mechanic";
break;
case "medical":
jobId = "medicaldoctor";
break;
case "weapons":
jobId = "securityofficer";
break;
jobId = "engineer".ToIdentifier();
}
return jobId != null && JobPrefab.Prefabs.ContainsKey(jobId) ? JobPrefab.Prefabs[jobId].IconSmall : null;
else if (Identifier == "helm")
{
jobId = "captain".ToIdentifier();
}
else if (Identifier == "mechanical")
{
jobId = "mechanic".ToIdentifier();
}
else if (Identifier == "medical")
{
jobId = "medicaldoctor".ToIdentifier();
}
else if (Identifier == "weapons")
{
jobId = "securityofficer".ToIdentifier();
}
return jobId;
}
}
}
@@ -5,7 +5,7 @@ namespace Barotrauma
{
class SkillPrefab
{
public readonly string Identifier;
public readonly Identifier Identifier;
public Range<float> LevelRange { get; private set; }
@@ -16,9 +16,9 @@ namespace Barotrauma
public bool IsPrimarySkill { get; }
public SkillPrefab(XElement element)
public SkillPrefab(ContentXElement element)
{
Identifier = element.GetAttributeString("identifier", "");
Identifier = element.GetAttributeIdentifier("identifier", "");
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f);
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
@@ -540,6 +540,8 @@ namespace Barotrauma
private set;
}
public Items.Components.Rope AttachedRope { get; set; }
public string Name => Params.Name;
// These properties are exposed for status effects
@@ -576,7 +578,7 @@ namespace Barotrauma
}
}
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
private set;
@@ -622,7 +624,7 @@ namespace Barotrauma
body.BodyType = BodyType.Dynamic;
foreach (XElement subElement in element.Elements())
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
@@ -644,9 +646,10 @@ namespace Barotrauma
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
if (character.VariantOf != null && character.Params.VariantFile != null)
if (!character.VariantOf.IsEmpty)
{
var attackElement = character.Params.VariantFile.Root.GetChildElement("attack");
var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab)
? basePrefab.ConfigElement.GetChildElement("attack") : null;
if (attackElement != null)
{
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
@@ -668,7 +671,7 @@ namespace Barotrauma
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
partial void InitProjSpecific(ContentXElement element);
public void MoveToPos(Vector2 pos, float force, bool pullFromCenter = false)
{
@@ -1010,15 +1013,9 @@ namespace Barotrauma
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
{
NetEntityEvent.Type.ExecuteAttack,
this,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0,
attackSimPos.X,
attackSimPos.Y
});
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
#endif
}
@@ -1054,7 +1051,10 @@ namespace Barotrauma
if (!attack.IsRunning)
{
// Set the main collider where the body lands after the attack
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f)
{
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
}
}
return wasHit;
}
@@ -1084,7 +1084,7 @@ namespace Barotrauma
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this);
}
}
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient))
{
// TODO: use the hit pos?
var localFront = body.GetLocalFront(Params.GetSpriteOrientation());
@@ -8,15 +8,7 @@ namespace Barotrauma
{
class NPCPersonalityTrait
{
private static List<NPCPersonalityTrait> list = new List<NPCPersonalityTrait>();
public static List<NPCPersonalityTrait> List
{
get { return list; }
}
public readonly string FilePath;
public readonly string Name;
public readonly Identifier Name;
public readonly List<string> AllowedDialogTags;
@@ -26,20 +18,32 @@ namespace Barotrauma
get { return commonness; }
}
public NPCPersonalityTrait(XElement element, string filePath)
public static IEnumerable<NPCPersonalityTrait> GetAll(LanguageIdentifier language)
{
FilePath = filePath;
Name = element.GetAttributeString("name", "");
AllowedDialogTags = new List<string>(element.GetAttributeStringArray("alloweddialogtags", new string[0]));
commonness = element.GetAttributeFloat("commonness", 1.0f);
return NPCConversationCollection.Collections[language]
.SelectMany(cc => cc.PersonalityTraits.Values);
}
list.Add(this);
public static NPCPersonalityTrait Get(LanguageIdentifier language, Identifier traitName)
{
return NPCConversationCollection.Collections[language]
.FirstOrDefault(cc => cc.PersonalityTraits.ContainsKey(traitName))
.PersonalityTraits[traitName];
}
public NPCPersonalityTrait(XElement element)
{
Name = element.GetAttributeIdentifier("name", "");
AllowedDialogTags = new List<string>(element.GetAttributeStringArray("alloweddialogtags", Array.Empty<string>()));
commonness = element.GetAttributeFloat("commonness", 1.0f);
}
public static NPCPersonalityTrait GetRandom(string seed)
{
#warning TODO: implement NPCPersonality content type and revise this for determinism
var rand = new MTRandom(ToolBox.StringToInt(seed));
return ToolBox.SelectWeightedRandom(list, list.Select(t => t.commonness).ToList(), rand);
var list = GetAll(GameSettings.CurrentConfig.Language);
return ToolBox.SelectWeightedRandom(list, t => t.commonness, rand);
}
}
@@ -21,71 +21,72 @@ namespace Barotrauma
abstract class GroundedMovementParams : AnimationParams
{
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
public Vector2 StepSize
{
get;
set;
}
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float HeadPosition { get; set; }
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float TorsoPosition { get; set; }
[Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
[Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
public float StepLiftHeadMultiplier { get; set; }
[Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
public float StepLiftAmount { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool MultiplyByDir { get; set; }
[Serialize(0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
[Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
public float StepLiftOffset { get; set; }
[Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
[Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
public float StepLiftFrequency { get; set; }
[Serialize(0.75f, true, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
[Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
public float BackwardsMovementMultiplier { get; set; }
}
abstract class SwimParams : AnimationParams
{
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, IsPropertySaveable.Yes, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float SteerTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float LegTorque { get; set; }
}
abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
{
public string SpeciesName { get; private set; }
public Identifier SpeciesName { get; private set; }
public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast;
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
protected static Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
/// allAnimations[speciesName][fileName]
private float _movementSpeed;
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed
{
get => _movementSpeed;
set => _movementSpeed = value;
}
[Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
public float CycleSpeed { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float HeadAngle
{
get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians);
@@ -102,7 +103,7 @@ namespace Barotrauma
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float TorsoAngle
{
get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians);
@@ -117,49 +118,44 @@ namespace Barotrauma
public float TorsoAngleInRadians { get; private set; } = float.NaN;
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float FootTorque { get; set; }
[Serialize(AnimationType.NotDefined, true), Editable]
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
public virtual AnimationType AnimationType { get; protected set; }
[Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmIKStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandIKStrength { get; set; }
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}";
public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}";
public static string GetDefaultFile(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
public static string GetFolder(string speciesName)
public static string GetFolder(Identifier speciesName)
{
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab?.XDocument == null)
if (prefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
return string.Empty;
}
return GetFolder(prefab.XDocument, prefab.FilePath);
return GetFolder(prefab.ConfigElement, prefab.FilePath.Value);
}
public static string GetFolder(XDocument doc, string filePath)
private static string GetFolder(ContentXElement root, string filePath)
{
var root = doc.Root;
if (root?.IsOverride() ?? false)
{
root = root.FirstElement();
}
var folder = root?.Element("animations")?.GetAttributeString("folder", string.Empty);
var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value;
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations");
folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations");
}
return folder.CleanUpPathCrossPlatform(true);
}
@@ -167,9 +163,9 @@ namespace Barotrauma
/// <summary>
/// Selects a random filepath from multiple paths, matching the specified animation type.
/// </summary>
public static string GetRandomFilePath(IEnumerable<string> filePaths, AnimationType type)
public static string GetRandomFilePath(IReadOnlyList<string> filePaths, AnimationType type)
{
return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.Server);
return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient);
}
/// <summary>
@@ -194,11 +190,12 @@ namespace Barotrauma
public static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
{
string speciesName = character.VariantOf ?? character.SpeciesName;
if (character.VariantOf != null && character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeString("folder", null) != null)
Identifier speciesName = character.SpeciesName;
if (!character.VariantOf.IsEmpty
&& (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty())
{
// Use the overridden animations defined in the variant definition file.
speciesName = character.SpeciesName;
// Use the base animations defined in the base definition file.
speciesName = character.VariantOf;
}
return GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
}
@@ -207,7 +204,7 @@ namespace Barotrauma
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetAnimParams<T>(string speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new()
public static T GetAnimParams<T>(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new()
{
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
@@ -239,7 +236,7 @@ namespace Barotrauma
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
@@ -257,10 +254,11 @@ namespace Barotrauma
throw new Exception("[AnimationParams] Selected file null!");
}
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T a = new T();
if (a.Load(selectedFile, speciesName))
if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName))
{
fileName = Path.GetFileNameWithoutExtension(selectedFile);
fileName = IO.Path.GetFileNameWithoutExtension(selectedFile);
if (!anims.ContainsKey(fileName))
{
anims.Add(fileName, a);
@@ -277,7 +275,7 @@ namespace Barotrauma
public static void ClearCache() => allAnimations.Clear();
public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type)
public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type type)
{
if (type == typeof(HumanWalkParams))
{
@@ -317,7 +315,7 @@ namespace Barotrauma
/// <summary>
/// Note: Overrides old animations, if found!
/// </summary>
public static T Create<T>(string fullPath, string speciesName, AnimationType animationType) where T : AnimationParams, new()
public static T Create<T>(string fullPath, Identifier speciesName, AnimationType animationType) where T : AnimationParams, new()
{
if (animationType == AnimationType.NotDefined)
{
@@ -328,7 +326,7 @@ namespace Barotrauma
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
var fileName = Path.GetFileNameWithoutExtension(fullPath);
var fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName))
{
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
@@ -337,10 +335,12 @@ namespace Barotrauma
var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
instance.doc = new XDocument(animationElement);
instance.UpdatePath(fullPath);
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath);
instance.UpdatePath(contentPath);
instance.IsLoaded = instance.Deserialize(animationElement);
instance.Save();
instance.Load(fullPath, speciesName);
instance.Load(contentPath, speciesName);
anims.Add(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance;
@@ -349,7 +349,7 @@ namespace Barotrauma
public bool Serialize() => base.Serialize();
public bool Deserialize() => base.Deserialize();
protected bool Load(string file, string speciesName)
protected bool Load(ContentPath file, Identifier speciesName)
{
if (Load(file))
{
@@ -359,7 +359,7 @@ namespace Barotrauma
return false;
}
protected override void UpdatePath(string newPath)
protected override void UpdatePath(ContentPath newPath)
{
if (SpeciesName == null)
{
@@ -464,7 +464,8 @@ namespace Barotrauma
var copy = new T
{
IsLoaded = true,
doc = new XDocument(doc)
doc = new XDocument(doc),
Path = Path
};
copy.Deserialize();
copy.Serialize();
@@ -11,7 +11,7 @@ namespace Barotrauma
}
public static FishWalkParams GetAnimParams(Character character, string fileName = null)
{
return Check(character) ? GetAnimParams<FishWalkParams>(character.VariantOf ?? character.SpeciesName, AnimationType.Walk, fileName) : Empty;
return Check(character) ? GetAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk, fileName) : Empty;
}
protected static FishWalkParams Empty = new FishWalkParams();
@@ -27,7 +27,7 @@ namespace Barotrauma
}
public static FishRunParams GetAnimParams(Character character, string fileName = null)
{
return Check(character) ? GetAnimParams<FishRunParams>(character.VariantOf ?? character.SpeciesName, AnimationType.Run, fileName) : Empty;
return Check(character) ? GetAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run, fileName) : Empty;
}
protected static FishRunParams Empty = new FishRunParams();
@@ -40,7 +40,7 @@ namespace Barotrauma
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character, AnimationType.SwimFast);
public static FishSwimFastParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<FishSwimFastParams>(character.VariantOf ?? character.SpeciesName, AnimationType.SwimFast, fileName);
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimFastParams>();
@@ -51,7 +51,7 @@ namespace Barotrauma
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character, AnimationType.SwimSlow);
public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<FishSwimSlowParams>(character.VariantOf ?? character.SpeciesName, AnimationType.SwimSlow, fileName);
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimSlowParams>();
@@ -69,35 +69,35 @@ namespace Barotrauma
return true;
}
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
[Serialize(1f, IsPropertySaveable.Yes, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
public float FlipCooldown { get; set; }
[Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
public float FlipDelay { get; set; }
[Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float HeadMoveForce { get; set; }
[Serialize(10.0f, true, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float TorsoMoveForce { get; set; }
[Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(8.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveForce { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TailTorque { get; set; }
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float LegTorque { get; set; }
/// <summary>
/// The angle of the collider when standing (i.e. out of water).
/// In degrees.
/// </summary>
[Serialize(0f, true, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
[Serialize(0f, IsPropertySaveable.Yes, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
public float ColliderStandAngle
{
get => MathHelper.ToDegrees(ColliderStandAngleInRadians);
@@ -105,7 +105,7 @@ namespace Barotrauma
}
public float ColliderStandAngleInRadians { get; private set; }
[Serialize(null, true), Editable]
[Serialize(null, IsPropertySaveable.Yes), Editable]
public string FootAngles
{
get => ParseFootAngles(FootAnglesInRadians);
@@ -120,7 +120,7 @@ namespace Barotrauma
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float TailAngle
{
get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians);
@@ -137,41 +137,40 @@ namespace Barotrauma
abstract class FishSwimParams : SwimParams, IFishAnimation
{
[Serialize(false, true, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable]
public bool UseSineMovement { get; set; }
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
[Serialize(1f, IsPropertySaveable.Yes, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
public float FlipCooldown { get; set; }
[Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
[Serialize(0.5f, IsPropertySaveable.Yes, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
public float FlipDelay { get; set; }
[Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
public bool Mirror { get; set; }
[Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Disabling this will make mirroring instantaneous.")]
public bool MirrorLerp { get; set; }
[Serialize(5f, true), Editable]
[Serialize(5f, IsPropertySaveable.Yes), Editable]
public float WaveAmplitude { get; set; }
[Serialize(10.0f, true), Editable]
[Serialize(10.0f, IsPropertySaveable.Yes), Editable]
public float WaveLength { get; set; }
[Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")]
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character face towards the direction it's heading.")]
public bool RotateTowardsMovement { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float TailTorque { get; set; }
[Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
[Serialize(1f, IsPropertySaveable.Yes, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
public float TailTorqueMultiplier { get; set; }
[Serialize(null, true), Editable]
[Serialize(null, IsPropertySaveable.Yes), Editable]
public string FootAngles
{
get => ParseFootAngles(FootAnglesInRadians);
@@ -186,7 +185,7 @@ namespace Barotrauma
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float TailAngle
{
get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians);
@@ -26,13 +26,13 @@ namespace Barotrauma
class HumanCrouchParams : HumanGroundedParams
{
[Serialize(0.0f, true, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)]
public float MoveDownAmountWhenStationary { get; set; }
[Serialize(0.0f, true), Editable(-360f, 360f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float ExtraHeadAngleWhenStationary { get; set; }
[Serialize(0.0f, true), Editable(-360f, 360f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float ExtraTorsoAngleWhenStationary { get; set; }
public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanCrouchParams>(character, AnimationType.Crouch);
@@ -69,25 +69,25 @@ namespace Barotrauma
abstract class HumanSwimParams : SwimParams, IHumanAnimation
{
[Serialize(0.5f, true), Editable(DecimalCount = 2)]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float LegMoveAmount { get; set; }
[Serialize(5.0f, true), Editable]
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float LegCycleLength { get; set; }
[Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)]
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize(5.0f, true), Editable]
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float HandCycleSpeed { get; set; }
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2)]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0.0f, true), Editable(-360f, 360f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float FootAngle
{
get => MathHelper.ToDegrees(FootAngleInRadians);
@@ -98,64 +98,70 @@ namespace Barotrauma
}
public float FootAngleInRadians { get; private set; }
[Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)]
public float ArmMoveStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
{
[Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
[Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float GetUpForce { get; set; }
[Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)]
[Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)]
public float HeadLeanAmount { get; set; }
[Serialize(0.25f, true, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)]
[Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)]
public float TorsoLeanAmount { get; set; }
[Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveStrength { get; set; }
[Serialize(0f, true, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)]
public float FootLiftHorizontalFactor { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0.0f, true), Editable(-360f, 360f)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float FootAngle
{
get => MathHelper.ToDegrees(FootAngleInRadians);
set
{
FootAngleInRadians = MathHelper.ToRadians(value);
FootAngleInRadians = MathHelper.ToRadians(value);
}
}
public float FootAngleInRadians { get; private set; }
[Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)]
public Vector2 FootMoveOffset { get; set; }
[Serialize(10.0f, true, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float LegBendTorque { get; set; }
[Serialize("0.4, 0.15", true, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)]
[Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize("-0.15, 0.0", true, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)]
[Serialize("-0.15, 0.0", IsPropertySaveable.Yes, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
[Serialize(-1.0f, true, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)]
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)]
public float HandClampY { get; set; }
[Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmMoveStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
public interface IHumanAnimation
@@ -166,5 +172,7 @@ namespace Barotrauma
float ArmMoveStrength { get; set; }
float HandMoveStrength { get; set; }
bool FixedHeadAngle { get; set; }
}
}
@@ -5,6 +5,7 @@ using System.Xml.Linq;
using System.Xml;
using System.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
#if CLIENT
using SoundType = Barotrauma.CharacterSound.SoundType;
#endif
@@ -16,94 +17,97 @@ namespace Barotrauma
/// </summary>
class CharacterParams : EditableParams
{
[Serialize("", true), Editable]
public string SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
public Identifier SpeciesName { get; private set; }
[Serialize("", true, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
public string SpeciesTranslationOverride { get; private set; }
[Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
[Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
public string Group { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
public Identifier Group { get; private set; }
[Serialize(false, true), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
public bool Humanoid { get; private set; }
[Serialize(false, true), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
public bool HasInfo { get; private set; }
[Serialize(false, true, description: "Can the creature interact with items?"), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable]
public bool CanInteract { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool Husk { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseHuskAppendage { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, false), Editable]
[Serialize(false, IsPropertySaveable.No), Editable]
public bool CanSpeak { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseBossHealthBar { get; private set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
[Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Noise { get; set; }
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)]
[Serialize(100f, IsPropertySaveable.Yes, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Visibility { get; set; }
[Serialize("blood", true), Editable]
[Serialize("blood", IsPropertySaveable.Yes), Editable]
public string BloodDecal { get; private set; }
[Serialize("blooddrop", true), Editable]
[Serialize("blooddrop", IsPropertySaveable.Yes), Editable]
public string BleedParticleAir { get; private set; }
[Serialize("waterblood", true), Editable]
[Serialize("waterblood", IsPropertySaveable.Yes), Editable]
public string BleedParticleWater { get; private set; }
[Serialize(1f, true), Editable]
[Serialize(1f, IsPropertySaveable.Yes), Editable]
public float BleedParticleMultiplier { get; private set; }
[Serialize(true, true, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable]
public bool CanEat { get; set; }
[Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool UsePathFinding { get; set; }
[Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
[Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
public float PathFinderPriority { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool HideInSonar { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool HideInThermalGoggles { get; set; }
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float SonarDisruption { get; set; }
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float DistantSonarRange { get; set; }
[Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)]
[Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)]
public float DisableDistance { get; set; }
[Serialize(10f, true, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)]
[Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)]
public float SoundInterval { get; set; }
public readonly string File;
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DrawLast { get; set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -116,7 +120,7 @@ namespace Barotrauma
public HealthParams Health { get; private set; }
public AIParams AI { get; private set; }
public CharacterParams(string file)
public CharacterParams(CharacterFile file)
{
File = file;
Load();
@@ -124,45 +128,63 @@ namespace Barotrauma
protected override string GetName() => "Character Config File";
public override XElement MainElement => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement;
public static XElement CreateVariantXml(XElement variantXML, XElement baseXML)
{
XElement newXml = variantXML.CreateVariantXML(baseXML);
XElement variantAi = variantXML.GetChildElement("ai");
XElement baseAi = baseXML.GetChildElement("ai");
if (baseAi is null || baseAi.Elements().None()
|| variantAi is null || variantAi.Elements().None())
{
return newXml;
}
// CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions.
var finalAiElement = newXml.GetChildElement("ai");
var processedTags = new HashSet<string>();
foreach (var aiTarget in finalAiElement.Elements().ToArray())
{
string tag = aiTarget.GetAttributeString("tag", null);
if (tag == null) { continue; }
if (processedTags.Contains(tag))
{
aiTarget.Remove();
continue;
}
processedTags.Add(tag);
var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
if (matchInSelf != null && matchInParent != null)
{
aiTarget.ReplaceWith(new XElement(matchInSelf));
}
}
return newXml;
}
public bool Load()
{
bool success = base.Load(File);
if (doc.Root.IsCharacterVariant())
UpdatePath(File.Path);
doc = XMLExtensions.TryLoadXml(Path);
Identifier variantOf = MainElement.VariantOf();
if (!variantOf.IsEmpty)
{
VariantFile = doc;
var original = CharacterPrefab.FindBySpeciesName(doc.Root.GetAttributeString("inherit", string.Empty));
success = Load(original.FilePath);
CreateSubParams();
TryLoadOverride(this, VariantFile.Root, SerializableProperties);
foreach (XElement subElement in VariantFile.Root.Elements())
{
var matchingParams = SubParams.FirstOrDefault(p => p.Name.Equals(subElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
if (matchingParams != null)
{
TryLoadOverride(matchingParams, subElement, matchingParams.SerializableProperties);
// TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion.
foreach (XElement subSubElement in subElement.Elements())
{
if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
if (matchingSubParams != null)
{
TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties);
}
}
}
}
return success;
VariantFile = new XDocument(doc);
#warning TODO: determine that CreateVariantXML is equipped to do this
XElement newRoot = CreateVariantXml(MainElement, CharacterPrefab.FindBySpeciesName(variantOf).ConfigElement);
var oldElement = MainElement;
var parentElement = (XContainer)oldElement.Parent ?? doc; oldElement.Remove(); parentElement.Add(newRoot);
}
if (string.IsNullOrEmpty(SpeciesName) && MainElement != null)
IsLoaded = Deserialize(MainElement);
OriginalElement = new XElement(MainElement).FromPackage(Path.ContentPackage);
if (SpeciesName.IsEmpty && MainElement != null)
{
//backwards compatibility
SpeciesName = MainElement.GetAttributeString("name", "");
SpeciesName = MainElement.GetAttributeIdentifier("name", "");
}
CreateSubParams();
return success;
return IsLoaded;
}
public bool Save(string fileNameWithoutExtension = null)
@@ -189,7 +211,7 @@ namespace Barotrauma
return true;
}
public bool CompareGroup(string group) => !string.IsNullOrWhiteSpace(group) && !string.IsNullOrWhiteSpace(Group) && group.Equals(Group, StringComparison.OrdinalIgnoreCase);
public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group;
protected void CreateSubParams()
{
@@ -239,26 +261,14 @@ namespace Barotrauma
}
}
private void TryLoadOverride(object parentObject, XElement element, Dictionary<string, SerializableProperty> properties)
{
foreach (var property in properties)
{
var matchingAttribute = element.GetAttribute(property.Key);
if (matchingAttribute != null)
{
property.Value.TrySetValue(parentObject, matchingAttribute.Value);
}
}
}
public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true, bool loadDefaultValues = true)
{
if (base.Deserialize(element))
{
//backwards compatibility
if (string.IsNullOrEmpty(SpeciesName))
if (SpeciesName.IsEmpty)
{
SpeciesName = element.GetAttributeString("name", "[NAME NOT GIVEN]");
SpeciesName = element.GetAttributeIdentifier("name", "[NAME NOT GIVEN]");
}
if (alsoChildren)
{
@@ -300,9 +310,9 @@ namespace Barotrauma
}
#endif
public bool AddSound() => TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds);
public bool AddSound() => TryAddSubParam(CreateElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds);
public void AddInventory() => TryAddSubParam(new XElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories);
public void AddInventory() => TryAddSubParam(CreateElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories);
public void AddBloodEmitter() => AddEmitter("bloodemitter");
public void AddGibEmitter() => AddEmitter("gibemitter");
@@ -313,13 +323,13 @@ namespace Barotrauma
switch (type)
{
case "gibemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters);
TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters);
break;
case "bloodemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters);
TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters);
break;
case "damageemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters);
TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters);
break;
default: throw new NotImplementedException(type);
}
@@ -342,7 +352,7 @@ namespace Barotrauma
return true;
}
protected bool TryAddSubParam<T>(XElement element, Func<XElement, CharacterParams, T> constructor, out T subParam, IList<T> collection = null, Func<IList<T>, bool> filter = null) where T : SubParam
protected bool TryAddSubParam<T>(ContentXElement element, Func<ContentXElement, CharacterParams, T> constructor, out T subParam, IList<T> collection = null, Func<IList<T>, bool> filter = null) where T : SubParam
{
subParam = constructor(element, this);
if (collection != null && filter != null)
@@ -360,24 +370,43 @@ namespace Barotrauma
{
public override string Name => "Sound";
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string File { get; private set; }
#if CLIENT
[Serialize(SoundType.Idle, true), Editable]
[Serialize(SoundType.Idle, IsPropertySaveable.Yes), Editable]
public SoundType State { get; private set; }
#endif
[Serialize(1000f, true), Editable(minValue: 0f, maxValue: 10000f)]
[Serialize(1000f, IsPropertySaveable.Yes), Editable(minValue: 0f, maxValue: 10000f)]
public float Range { get; private set; }
[Serialize(1.0f, true), Editable(minValue: 0f, maxValue: 2.0f)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(minValue: 0f, maxValue: 2.0f)]
public float Volume { get; private set; }
[Serialize(Gender.None, true, description: "Is the sound gender specific?"), Editable()]
public Gender Gender { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "Which tags are required for this sound to play?"), Editable()]
public string Tags
{
get { return string.Join(',', TagSet); }
private set
{
TagSet = value.Split(',')
.ToIdentifiers()
.Where(id => !id.IsEmpty)
.ToImmutableHashSet();
}
}
public SoundParams(XElement element, CharacterParams character) : base(element, character) { }
public ImmutableHashSet<Identifier> TagSet { get; private set; }
public SoundParams(ContentXElement element, CharacterParams character) : base(element, character)
{
Identifier genderFallback = element.GetAttributeIdentifier("gender", "");
if (genderFallback != Identifier.Empty && genderFallback != "None")
{
TagSet = TagSet.Add(genderFallback);
}
}
}
public class ParticleParams : SubParam
@@ -395,83 +424,86 @@ namespace Barotrauma
}
}
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string Particle { get; set; }
[Serialize(0f, true), Editable(-360f, 360f, decimals: 0)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(-360f, 360f, decimals: 0)]
public float AngleMin { get; private set; }
[Serialize(0f, true), Editable(-360f, 360f, decimals: 0)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(-360f, 360f, decimals: 0)]
public float AngleMax { get; private set; }
[Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)]
public float ScaleMin { get; private set; }
[Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)]
public float ScaleMax { get; private set; }
[Serialize(0f, true), Editable(0f, 10000f, decimals: 0)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 10000f, decimals: 0)]
public float VelocityMin { get; private set; }
[Serialize(0f, true), Editable(0f, 10000f, decimals: 0)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 10000f, decimals: 0)]
public float VelocityMax { get; private set; }
[Serialize(0f, true), Editable(0f, 100f, decimals: 2)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)]
public float EmitInterval { get; private set; }
[Serialize(0, true), Editable(0, 1000)]
[Serialize(0, IsPropertySaveable.Yes), Editable(0, 1000)]
public int ParticlesPerSecond { get; private set; }
[Serialize(0, true), Editable(0, 1000)]
[Serialize(0, IsPropertySaveable.Yes), Editable(0, 1000)]
public int ParticleAmount { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool HighQualityCollisionDetection { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool CopyEntityAngle { get; private set; }
public ParticleParams(XElement element, CharacterParams character) : base(element, character) { }
public ParticleParams(ContentXElement element, CharacterParams character) : base(element, character) { }
}
public class HealthParams : SubParam
{
public override string Name => "Health";
[Serialize(100f, true, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)]
[Serialize(100f, IsPropertySaveable.Yes, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)]
public float Vitality { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool DoesBleed { get; set; }
[Serialize(float.NegativeInfinity, true), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
[Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
public float CrushDepth { get; set; }
// Make editable?
[Serialize(false, true)]
[Serialize(false, IsPropertySaveable.Yes)]
public bool UseHealthWindow { get; set; }
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float BleedingReduction { get; private set; }
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float BurnReduction { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ConstantHealthRegeneration { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float HealthRegenerationWhenEating { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool StunImmunity { get; set; }
[Serialize(false, true, description: "Can afflictions affect the face/body tint of the character."), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
// TODO: limbhealths, sprite?
public HealthParams(XElement element, CharacterParams character) : base(element, character) { }
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { }
}
public class InventoryParams : SubParam
@@ -480,26 +512,26 @@ namespace Barotrauma
{
public override string Name => "Item";
[Serialize("", true, description: "Item identifier."), Editable()]
[Serialize("", IsPropertySaveable.Yes, description: "Item identifier."), Editable()]
public string Identifier { get; private set; }
public InventoryItem(XElement element, CharacterParams character) : base(element, character) { }
public InventoryItem(ContentXElement element, CharacterParams character) : base(element, character) { }
}
public override string Name => "Inventory";
[Serialize("Any, Any", true, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()]
[Serialize("Any, Any", IsPropertySaveable.Yes, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()]
public string Slots { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool AccessibleWhenAlive { get; private set; }
[Serialize(1.0f, true, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)]
public float Commonness { get; private set; }
public List<InventoryItem> Items { get; private set; } = new List<InventoryItem>();
public InventoryParams(XElement element, CharacterParams character) : base(element, character)
public InventoryParams(ContentXElement element, CharacterParams character) : base(element, character)
{
foreach (var itemElement in element.GetChildElements("item"))
{
@@ -512,7 +544,7 @@ namespace Barotrauma
public void AddItem(string identifier = null)
{
identifier = identifier ?? "";
var element = new XElement("item", new XAttribute("identifier", identifier));
var element = CreateElement("item", new XAttribute("identifier", identifier));
Element.Add(element);
var item = new InventoryItem(element, Character);
SubParams.Add(item);
@@ -526,89 +558,92 @@ namespace Barotrauma
{
public override string Name => "AI";
[Serialize(1.0f, true, description: "How strong other characters think this character is? Only affects AI."), Editable()]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How strong other characters think this character is? Only affects AI."), Editable()]
public float CombatStrength { get; private set; }
[Serialize(1.0f, true, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Sight { get; private set; }
[Serialize(1.0f, true, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Hearing { get; private set; }
[Serialize(100f, true, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)]
[Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)]
public float AggressionHurt { get; private set; }
[Serialize(10f, true, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(10f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)]
public float AggressionGreed { get; private set; }
[Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)]
[Serialize(0f, IsPropertySaveable.Yes, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)]
public float FleeHealthThreshold { get; private set; }
[Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()]
[Serialize(false, IsPropertySaveable.Yes, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()]
public bool AttackWhenProvoked { get; private set; }
[Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
[Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
[Serialize(0f, IsPropertySaveable.Yes, description: "How much damage is required for single attack to trigger avoiding/releasing targets."), Editable(minValue: 0f, maxValue: 1000f)]
public float DamageThreshold { get; private set; }
[Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
public float AvoidTime { get; private set; }
[Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)]
[Serialize(20f, IsPropertySaveable.Yes, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)]
public float MinFleeTime { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Does the character try to break inside the sub?"), Editable]
public bool AggressiveBoarding { get; private set; }
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable]
public bool EnforceAggressiveBehaviorForMissions { get; private set; }
[Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Should the character target or ignore walls when it's outside the submarine."), Editable]
public bool TargetOuterWalls { get; private set; }
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
public bool RandomAttack { get; private set; }
[Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable]
public bool CanOpenDoors { get; private set; }
[Serialize(false, true, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable]
public bool KeepDoorsClosed { get; private set; }
[Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable]
[Serialize(true, IsPropertySaveable.Yes, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable]
public bool AvoidAbyss { get; set; }
[Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable]
[Serialize(false, IsPropertySaveable.Yes, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable]
public bool StayInAbyss { get; set; }
[Serialize(false, true, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable]
[Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable]
public bool PatrolFlooded { get; set; }
[Serialize(false, true, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable]
[Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable]
public bool PatrolDry { get; set; }
[Serialize(0f, true, description: ""), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable]
public float StartAggression { get; private set; }
[Serialize(100f, true, description: ""), Editable]
[Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable]
public float MaxAggression { get; private set; }
[Serialize(0f, true, description: ""), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable]
public float AggressionCumulation { get; private set; }
[Serialize(WallTargetingMethod.Target, true, description: ""), Editable]
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable]
public WallTargetingMethod WallTargetingMethod { get; private set; }
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();
public AIParams(XElement element, CharacterParams character) : base(element, character)
public AIParams(ContentXElement element, CharacterParams character) : base(element, character)
{
if (element == null) { return; }
element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _));
element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _));
}
private bool TryAddTarget(XElement targetElement, out TargetParams target)
private bool TryAddTarget(ContentXElement targetElement, out TargetParams target)
{
string tag = targetElement.GetAttributeString("tag", null);
if (HasTag(tag))
@@ -628,9 +663,12 @@ namespace Barotrauma
public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams);
public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams)
public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams) =>
TryAddNewTarget(tag.ToIdentifier(), state, priority, out targetParams);
public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams)
{
var element = TargetParams.CreateNewElement(tag, state, priority);
var element = TargetParams.CreateNewElement(Character, tag, state, priority);
if (TryAddTarget(element, out targetParams))
{
Element.Add(element);
@@ -642,17 +680,22 @@ namespace Barotrauma
}
}
public bool HasTag(string tag)
public bool HasTag(string tag) => HasTag(tag.ToIdentifier());
public bool HasTag(Identifier tag)
{
if (tag == null) { return false; }
return targets.Any(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase));
return targets.Any(t => t.Tag == tag);
}
public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets);
public bool TryGetTarget(string targetTag, out TargetParams target)
=> TryGetTarget(targetTag.ToIdentifier(), out target);
public bool TryGetTarget(Identifier targetTag, out TargetParams target)
{
target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetTag, StringComparison.OrdinalIgnoreCase));
target = targets.FirstOrDefault(t => t.Tag == targetTag);
return target != null;
}
@@ -665,7 +708,7 @@ namespace Barotrauma
return target != null;
}
public bool TryGetTarget(IEnumerable<string> tags, out TargetParams target)
public bool TryGetTarget(IEnumerable<Identifier> tags, out TargetParams target)
{
target = null;
if (tags == null || tags.None()) { return false; }
@@ -674,7 +717,7 @@ namespace Barotrauma
{
if (potentialTarget.Priority > priority)
{
if (tags.Any(t => string.Equals(t, potentialTarget.Tag, StringComparison.OrdinalIgnoreCase)))
if (tags.Any(t => t == potentialTarget.Tag))
{
target = potentialTarget;
priority = target.Priority;
@@ -685,7 +728,11 @@ namespace Barotrauma
}
public TargetParams GetTarget(string targetTag, bool throwError = true)
=> GetTarget(targetTag.ToIdentifier(), throwError);
public TargetParams GetTarget(Identifier targetTag, bool throwError = true)
{
if (targetTag.IsEmpty) { return null; }
if (!TryGetTarget(targetTag, out TargetParams target))
{
if (throwError)
@@ -701,102 +748,111 @@ namespace Barotrauma
{
public override string Name => "Target";
[Serialize("", true, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()]
[Serialize("", IsPropertySaveable.Yes, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()]
public string Tag { get; private set; }
[Serialize(AIState.Idle, true), Editable]
[Serialize(AIState.Idle, IsPropertySaveable.Yes), Editable]
public AIState State { get; set; }
[Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)]
[Serialize(0f, IsPropertySaveable.Yes, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)]
public float Priority { get; set; }
[Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float ReactDistance { get; set; }
[Serialize(0f, true, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float AttackDistance { get; set; }
[Serialize(0f, true, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable]
public float Timer { get; set; }
[Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable]
public bool IgnoreContained { get; set; }
[Serialize(false, true, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable]
public bool IgnoreInside { get; set; }
[Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable]
public bool IgnoreOutside { get; set; }
[Serialize(false, true, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable]
public bool IgnoreIfNotInSameSub { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool IgnoreIncapacitated { get; set; }
[Serialize(0f, true, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable]
public float Threshold { get; private set; }
[Serialize(-1f, true, description: "A generic min threshold. Not used if set to negative."), Editable]
[Serialize(-1f, IsPropertySaveable.Yes, description: "A generic min threshold. Not used if set to negative."), Editable]
public float ThresholdMin { get; private set; }
[Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable]
[Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable]
public float ThresholdMax { get; private set; }
[Serialize("0.0, 0.0", true), Editable]
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable]
public Vector2 Offset { get; private set; }
[Serialize(AttackPattern.Straight, true), Editable]
[Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable]
public AttackPattern AttackPattern { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable]
public bool PrioritizeSubCenter { get; set; }
#region Sweep
[Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)]
public float SweepDistance { get; private set; }
[Serialize(10f, true, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)]
[Serialize(10f, IsPropertySaveable.Yes, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)]
public float SweepStrength { get; private set; }
[Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SweepSpeed { get; private set; }
#endregion
#region Circle
[Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
[Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
public float CircleMaxRandomOffset { get; private set; }
#endregion
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { }
public static XElement CreateNewElement(string tag, AIState state, float priority)
public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) =>
CreateNewElement(character, tag.Value, state, priority);
public static ContentXElement CreateNewElement(CharacterParams character, string tag, AIState state, float priority)
{
return new XElement("target",
new XAttribute("tag", tag),
new XAttribute("state", state),
new XAttribute("priority", priority));
new XAttribute("priority", priority)).FromPackage(character.File.ContentPackage);
}
}
public abstract class SubParam : ISerializableEntity
{
public virtual string Name { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public XElement Element { get; set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public ContentXElement Element { get; set; }
public List<SubParam> SubParams { get; set; } = new List<SubParam>();
public CharacterParams Character { get; private set; }
public SubParam(XElement element, CharacterParams character)
protected ContentXElement CreateElement(string name, params object[] attrs)
=> new XElement(name, attrs).FromPackage(Element.ContentPackage);
public SubParam(ContentXElement element, CharacterParams character)
{
Element = element;
Character = character;
@@ -843,12 +899,12 @@ namespace Barotrauma
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, ScalableFont titleFont = null)
public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, GUIFont titleFont = null)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUI.LargeFont);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUIStyle.LargeFont);
if (recursive)
{
SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUI.SmallFont));
SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUIStyle.SmallFont));
}
if (space > 0)
{
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using File = Barotrauma.IO.File;
#if DEBUG
using System.IO;
using System.Xml;
@@ -16,11 +17,13 @@ namespace Barotrauma
public string Name { get; private set; }
public string FileName { get; private set; }
public string Folder { get; private set; }
public string FullPath { get; private set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
public ContentPath Path { get; protected set; } = ContentPath.Empty;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
protected ContentXElement rootElement;
protected XDocument doc;
public XDocument Doc
private XDocument Doc
{
get
{
@@ -31,16 +34,30 @@ namespace Barotrauma
}
return doc;
}
protected set
set
{
doc = value;
}
}
public virtual XElement MainElement => doc.Root;
public XElement OriginalElement { get; protected set; }
public virtual ContentXElement MainElement
{
get
{
if (rootElement?.Element != doc.Root)
{
rootElement = doc.Root.FromPackage(Path.ContentPackage);
}
return rootElement;
}
}
public ContentXElement OriginalElement { get; protected set; }
protected virtual string GetName() => Path.GetFileNameWithoutExtension(FullPath).FormatCamelCaseWithSpaces();
protected ContentXElement CreateElement(string name, params object[] attrs)
=> new XElement(name, attrs).FromPackage(Path.ContentPackage);
protected virtual string GetName() => System.IO.Path.GetFileNameWithoutExtension(Path.Value).FormatCamelCaseWithSpaces();
protected virtual bool Deserialize(XElement element = null)
{
@@ -61,22 +78,22 @@ namespace Barotrauma
return true;
}
protected virtual bool Load(string file)
protected virtual bool Load(ContentPath file)
{
UpdatePath(file);
doc = XMLExtensions.TryLoadXml(FullPath);
doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return false; }
IsLoaded = Deserialize(MainElement);
OriginalElement = new XElement(MainElement);
OriginalElement = new XElement(MainElement).FromPackage(MainElement.ContentPackage);
return IsLoaded;
}
protected virtual void UpdatePath(string fullPath)
protected virtual void UpdatePath(ContentPath fullPath)
{
FullPath = fullPath;
Path = fullPath;
Name = GetName();
FileName = Path.GetFileName(FullPath);
Folder = Path.GetDirectoryName(FullPath);
FileName = System.IO.Path.GetFileName(Path.Value);
Folder = System.IO.Path.GetDirectoryName(Path.Value);
}
public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null)
@@ -98,9 +115,9 @@ namespace Barotrauma
}
if (fileNameWithoutExtension != null)
{
UpdatePath(Path.Combine(Folder, $"{fileNameWithoutExtension}.xml"));
UpdatePath(ContentPath.FromRaw(Path.ContentPackage, System.IO.Path.Combine(Folder, $"{fileNameWithoutExtension}.xml")));
}
using (var writer = XmlWriter.Create(FullPath, settings))
using (var writer = XmlWriter.Create(Path.Value, settings))
{
Doc.WriteTo(writer);
writer.Flush();
@@ -112,7 +129,7 @@ namespace Barotrauma
{
if (forceReload)
{
return Load(FullPath);
return Load(Path);
}
return Deserialize(OriginalElement);
}
@@ -126,7 +143,7 @@ namespace Barotrauma
DebugConsole.ThrowError("[Params] Not loaded!");
return;
}
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUI.LargeFont);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUIStyle.LargeFont);
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
@@ -14,13 +14,13 @@ namespace Barotrauma
{
class HumanRagdollParams : RagdollParams
{
public static HumanRagdollParams GetRagdollParams(string speciesName, string fileName = null) => GetRagdollParams<HumanRagdollParams>(speciesName, fileName);
public static HumanRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams<HumanRagdollParams>(speciesName);
public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams<HumanRagdollParams>(speciesName, fileName);
public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams<HumanRagdollParams>(speciesName);
}
class FishRagdollParams : RagdollParams
{
public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams<FishRagdollParams>(speciesName);
public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams<FishRagdollParams>(speciesName);
}
class RagdollParams : EditableParams, IMemorizable<RagdollParams>
@@ -29,15 +29,15 @@ namespace Barotrauma
public const float MIN_SCALE = 0.1f;
public const float MAX_SCALE = 2;
public string SpeciesName { get; private set; }
public Identifier SpeciesName { get; private set; }
[Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable]
public string Texture { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true), Editable()]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()]
public Color Color { get; set; }
[Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
public float SpritesheetOrientation { get; set; }
public bool IsSpritesheetOrientationHorizontal
@@ -51,85 +51,85 @@ namespace Barotrauma
}
private float limbScale;
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
private float jointScale;
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize?
[Serialize(1f, false)]
[Serialize(1f, IsPropertySaveable.No)]
public float TextureScale { get; set; }
[Serialize(45f, true, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)]
[Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)]
public float ColliderHeightFromFloor { get; set; }
[Serialize(50f, true, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float ImpactTolerance { get; set; }
[Serialize(true, true, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public bool CanEnterSubmarine { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool CanWalk { get; set; }
[Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()]
[Serialize(true, IsPropertySaveable.Yes, description: "Can the character be dragged around by other creatures?"), Editable()]
public bool Draggable { get; set; }
[Serialize(LimbType.Torso, true), Editable]
[Serialize(LimbType.Torso, IsPropertySaveable.Yes), Editable]
public LimbType MainLimb { get; set; }
private readonly static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
/// <summary>
/// key1: Species name
/// key2: File path
/// value: Ragdoll parameters
/// </summary>
private readonly static Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
public List<JointParams> Joints { get; private set; } = new List<JointParams>();
protected IEnumerable<SubParam> GetAllSubParams() =>
Colliders.Select(c => c as SubParam)
.Concat(Limbs.Select(j => j as SubParam)
.Concat(Joints.Select(j => j as SubParam)));
Colliders
.Concat<SubParam>(Limbs)
.Concat(Joints);
public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll";
public static string GetDefaultFile(string speciesName, ContentPackage contentPackage = null)
=> Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml");
public static string GetDefaultFileName(Identifier speciesName) => $"{speciesName.Value.CapitaliseFirstInvariant()}DefaultRagdoll";
public static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage = null)
=> IO.Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml");
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
public static string GetFolder(Identifier speciesName, ContentPackage contentPackage = null)
{
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage));
if (prefab?.XDocument == null)
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
if (prefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
return string.Empty;
}
return GetFolder(prefab.XDocument, prefab.FilePath);
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
}
public static string GetFolder(XDocument doc, string filePath)
private static string GetFolder(ContentXElement root, string filePath)
{
var root = doc.Root;
if (root?.IsOverride() ?? false)
var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
root = root.FirstElement();
}
var folder = root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar;
folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar;
}
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
public static T GetDefaultRagdollParams<T>(string speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName, GetDefaultFileName(speciesName));
public static T GetDefaultRagdollParams<T>(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName, GetDefaultFileName(speciesName));
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetRagdollParams<T>(string speciesName, string fileName = null) where T : RagdollParams, new()
public static T GetRagdollParams<T>(Identifier speciesName, string fileName = null) where T : RagdollParams, new()
{
if (string.IsNullOrWhiteSpace(speciesName))
if (speciesName.IsEmpty)
{
throw new Exception($"Species name null or empty!");
}
@@ -138,66 +138,88 @@ namespace Barotrauma
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
{
string selectedFile = null;
string folder = GetFolder(speciesName);
if (Directory.Exists(folder))
return (T)ragdoll;
}
string selectedFile = null;
void tryFolderForSpecies(Identifier species, out string err)
{
err = null;
string folder = GetFolder(species);
if (!Directory.Exists(folder))
{
List<string> files = Directory.GetFiles(folder).ToList();
if (files.None())
{
DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(speciesName);
}
else
{
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
}
err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
return;
}
string[] files = Directory.GetFiles(folder);
if (files.None())
{
err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(species);
}
else
{
DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
}
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
T r = new T();
if (r.Load(selectedFile, speciesName))
{
if (!ragdolls.ContainsKey(r.Name))
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
ragdolls.Add(r.Name, r);
err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
}
return r;
}
else
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early.
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
}
}
return (T)ragdoll;
tryFolderForSpecies(speciesName, out var error);
Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)
? prefab.VariantOf
: Identifier.Empty;
if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty)
{
tryFolderForSpecies(parentSpeciesName, out error);
}
if (!error.IsNullOrEmpty())
{
DebugConsole.ThrowError(error);
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
}
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T r = new T();
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName))
{
if (!ragdolls.ContainsKey(r.Name))
{
ragdolls.Add(r.Name, r);
}
return r;
}
else
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early.
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
}
}
/// <summary>
/// Creates a default ragdoll for the species using a predefined configuration.
/// Note: Use only to create ragdolls for new characters, because this overrides the old ragdoll!
/// </summary>
public static T CreateDefault<T>(string fullPath, string speciesName, XElement mainElement) where T : RagdollParams, new()
public static T CreateDefault<T>(string fullPath, Identifier speciesName, XElement mainElement) where T : RagdollParams, new()
{
// Remove the old ragdolls, if found.
if (allRagdolls.ContainsKey(speciesName))
@@ -211,10 +233,12 @@ namespace Barotrauma
{
doc = new XDocument(mainElement)
};
instance.UpdatePath(fullPath);
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath);
instance.UpdatePath(contentPath);
instance.IsLoaded = instance.Deserialize(mainElement);
instance.Save();
instance.Load(fullPath, speciesName);
instance.Load(contentPath, speciesName);
ragdolls.Add(instance.Name, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance as T;
@@ -222,7 +246,7 @@ namespace Barotrauma
public static void ClearCache() => allRagdolls.Clear();
protected override void UpdatePath(string fullPath)
protected override void UpdatePath(ContentPath fullPath)
{
if (SpeciesName == null)
{
@@ -259,7 +283,7 @@ namespace Barotrauma
});
}
protected bool Load(string file, string speciesName)
protected bool Load(ContentPath file, Identifier speciesName)
{
if (Load(file))
{
@@ -287,7 +311,7 @@ namespace Barotrauma
{
if (forceReload)
{
return Load(FullPath, SpeciesName);
return Load(Path, SpeciesName);
}
// Don't use recursion, because the reset method might be overriden
Deserialize(OriginalElement, alsoChildren: false, recursive: false);
@@ -401,8 +425,10 @@ namespace Barotrauma
}
var copy = new RagdollParams
{
SpeciesName = SpeciesName,
IsLoaded = true,
doc = new XDocument(doc)
doc = new XDocument(doc),
Path = Path
};
copy.CreateColliders();
copy.CreateLimbs();
@@ -453,7 +479,7 @@ namespace Barotrauma
public class JointParams : SubParam
{
private string name;
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public override string Name
{
get
@@ -472,61 +498,61 @@ namespace Barotrauma
public override string GenerateName() => $"Joint {Limb1} - {Limb2}";
[Serialize(-1, true), Editable]
[Serialize(-1, IsPropertySaveable.Yes), Editable]
public int Limb1 { get; set; }
[Serialize(-1, true), Editable]
[Serialize(-1, IsPropertySaveable.Yes), Editable]
public int Limb2 { get; set; }
/// <summary>
/// Should be converted to sim units.
/// </summary>
[Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb1."), Editable()]
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "Local position of the joint in the Limb1."), Editable()]
public Vector2 Limb1Anchor { get; set; }
/// <summary>
/// Should be converted to sim units.
/// </summary>
[Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb2."), Editable()]
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "Local position of the joint in the Limb2."), Editable()]
public Vector2 Limb2Anchor { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool CanBeSevered { get; set; }
[Serialize(0f, true, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SeveranceProbabilityModifier { get; set; }
[Serialize("gore", true), Editable]
[Serialize("gore", IsPropertySaveable.Yes), Editable]
public string BreakSound { get; set; }
[Serialize(true, true), Editable]
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool LimitEnabled { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float UpperLimit { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float LowerLimit { get; set; }
[Serialize(0.25f, true), Editable]
[Serialize(0.25f, IsPropertySaveable.Yes), Editable]
public float Stiffness { get; set; }
[Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
[Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
public float Scale { get; set; }
[Serialize(false, false), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.No), Editable(ReadOnly = true)]
public bool WeldJoint { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool ClockWiseRotation { get; set; }
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
public JointParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
public class LimbParams : SubParam
@@ -542,7 +568,7 @@ namespace Barotrauma
public List<DamageModifierParams> DamageModifiers { get; private set; } = new List<DamageModifierParams>();
private string name;
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public override string Name
{
get
@@ -563,10 +589,10 @@ namespace Barotrauma
public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams;
[Serialize(-1, true), Editable(ReadOnly = true)]
[Serialize(-1, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
public int ID { get; set; }
[Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
public LimbType Type { get; set; }
/// <summary>
@@ -576,136 +602,136 @@ namespace Barotrauma
public float GetSpriteOrientationInDegrees() => float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation;
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string Notes { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float Scale { get; set; }
[Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()]
[Serialize(true, IsPropertySaveable.Yes, description: "Does the limb flip when the character flips?"), Editable()]
public bool Flip { get; set; }
[Serialize(false, true, description: "Currently only works with non-deformable (normal) sprites."), Editable()]
[Serialize(false, IsPropertySaveable.Yes, description: "Currently only works with non-deformable (normal) sprites."), Editable()]
public bool MirrorVertically { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool MirrorHorizontally { get; set; }
[Serialize(false, true, description: "Disable drawing for this limb."), Editable()]
[Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
[Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(LimbType.None, true, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")]
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")]
public LimbType InheritLimbDepth { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
[Serialize(0f, true, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Radius { get; set; }
[Serialize(0f, true, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Height { get; set; }
[Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
[Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
public float Density { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool IgnoreCollisions { get; set; }
[Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable]
[Serialize(7f, IsPropertySaveable.Yes, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable]
public float AngularDamping { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
[Serialize(1f, IsPropertySaveable.Yes, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()]
[Serialize("0, 0", IsPropertySaveable.Yes, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()]
public Vector2 PullPos { get; set; }
[Serialize("0, 0", true, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()]
[Serialize("0, 0", IsPropertySaveable.Yes, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()]
public Vector2 StepOffset { get; set; }
[Serialize(-1, true, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()]
[Serialize(-1, IsPropertySaveable.Yes, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()]
public int RefJoint { get; set; }
[Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
[Serialize("0, 0", IsPropertySaveable.Yes, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
public Vector2 MouthPos { get; set; }
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float ConstantTorque { get; set; }
[Serialize(0f, true), Editable]
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float ConstantAngle { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
[Serialize(1f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
public float AttackForceMultiplier { get; set; }
[Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(1f, IsPropertySaveable.Yes, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)]
public float MinSeveranceDamage { get; set; }
[Serialize(true, true, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable]
public bool CanBeSeveredAlive { get; set; }
//how long it takes for severed limbs to fade out
[Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
[Serialize(10f, IsPropertySaveable.Yes, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
public float SeveredFadeOutTime { get; set; } = 10.0f;
[Serialize(false, true, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
public bool ApplyTailAngle { get; set; }
[Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineFrequencyMultiplier { get; set; }
[Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)]
[Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineAmplitudeMultiplier { get; set; }
[Serialize(0f, true), Editable(0, 100, ValueStep = 1, DecimalCount = 1)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(0, 100, ValueStep = 1, DecimalCount = 1)]
public float BlinkFrequency { get; set; }
[Serialize(0.2f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
[Serialize(0.2f, IsPropertySaveable.Yes), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkDurationIn { get; set; }
[Serialize(0.5f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkDurationOut { get; set; }
[Serialize(0f, true), Editable(0, 10, ValueStep = 1, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(0, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkHoldTime { get; set; }
[Serialize(0f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
public float BlinkRotationIn { get; set; }
[Serialize(45f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
[Serialize(45f, IsPropertySaveable.Yes), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
public float BlinkRotationOut { get; set; }
[Serialize(50f, true), Editable]
[Serialize(50f, IsPropertySaveable.Yes), Editable]
public float BlinkForce { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool OnlyBlinkInWater { get; set; }
[Serialize(TransitionMode.Linear, true), Editable]
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
[Serialize(TransitionMode.Linear, true), Editable]
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable]
public TransitionMode BlinkTransitionOut { get; private set; }
// Non-editable ->
// TODO: make read-only
[Serialize(0, true)]
[Serialize(0, IsPropertySaveable.Yes)]
public int HealthIndex { get; set; }
[Serialize(0.3f, true)]
[Serialize(0.3f, IsPropertySaveable.Yes)]
public float Friction { get; set; }
[Serialize(0.05f, true)]
[Serialize(0.05f, IsPropertySaveable.Yes)]
public float Restitution { get; set; }
public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
var spriteElement = element.GetChildElement("sprite");
if (spriteElement != null)
@@ -761,7 +787,7 @@ namespace Barotrauma
public bool AddAttack()
{
if (Attack != null) { return false; }
TryAddSubParam(new XElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack);
TryAddSubParam(CreateElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack);
Attack = newAttack;
return Attack != null;
}
@@ -770,7 +796,7 @@ namespace Barotrauma
public bool AddSound()
{
if (Sound != null) { return false; }
TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound);
TryAddSubParam(CreateElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound);
Sound = newSound;
return Sound != null;
}
@@ -778,14 +804,14 @@ namespace Barotrauma
public bool AddLight()
{
if (LightSource != null) { return false; }
var lightSourceElement = new XElement("lightsource",
var lightSourceElement = CreateElement("lightsource",
new XElement("lighttexture", new XAttribute("texture", "Content/Lights/pointlight_bright.png")));
TryAddSubParam(lightSourceElement, (e, c) => new LightSourceParams(e, c), out LightSourceParams newLightSource);
LightSource = newLightSource;
return LightSource != null;
}
public bool AddDamageModifier() => TryAddSubParam(new XElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers);
public bool AddDamageModifier() => TryAddSubParam(CreateElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers);
public bool RemoveAttack()
{
@@ -819,7 +845,7 @@ namespace Barotrauma
public bool RemoveDamageModifier(DamageModifierParams damageModifier) => RemoveSubParam(damageModifier, DamageModifiers);
protected bool TryAddSubParam<T>(XElement element, Func<XElement, RagdollParams, T> constructor, out T subParam, IList<T> collection = null, Func<IList<T>, bool> filter = null) where T : SubParam
protected bool TryAddSubParam<T>(ContentXElement element, Func<ContentXElement, RagdollParams, T> constructor, out T subParam, IList<T> collection = null, Func<IList<T>, bool> filter = null) where T : SubParam
{
subParam = constructor(element, Ragdoll);
if (collection != null && filter != null)
@@ -846,7 +872,7 @@ namespace Barotrauma
public class DecorativeSpriteParams : SpriteParams
{
public DecorativeSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public DecorativeSpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
#if CLIENT
DecorativeSprite = new DecorativeSprite(element);
@@ -882,7 +908,7 @@ namespace Barotrauma
{
public DeformationParams Deformation { get; private set; }
public DeformSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public DeformSpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
Deformation = new DeformationParams(element, ragdoll);
SubParams.Add(Deformation);
@@ -891,46 +917,46 @@ namespace Barotrauma
public class SpriteParams : SubParam
{
[Serialize("0, 0, 0, 0", true), Editable]
[Serialize("0, 0, 0, 0", IsPropertySaveable.Yes), Editable]
public Rectangle SourceRect { get; set; }
[Serialize("0.5, 0.5", true, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)]
[Serialize("0.5, 0.5", IsPropertySaveable.Yes, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)]
public Vector2 Origin { get; set; }
[Serialize(0f, true, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)]
[Serialize(0f, IsPropertySaveable.Yes, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)]
public float Depth { get; set; }
[Serialize("", true), Editable()]
[Serialize("", IsPropertySaveable.Yes), Editable()]
public string Texture { get; set; }
[Serialize(false, true), Editable()]
[Serialize(false, IsPropertySaveable.Yes), Editable()]
public bool IgnoreTint { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true), Editable()]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()]
public Color Color { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Target color when the character is dead."), Editable()]
public Color DeadColor { get; set; }
[Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)]
[Serialize(0f, IsPropertySaveable.Yes, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)]
public float DeadColorTime { get; set; }
public override string Name => "Sprite";
public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
public SpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
public string GetTexturePath() => string.IsNullOrWhiteSpace(Texture) ? Ragdoll.Texture : Texture;
}
public class DeformationParams : SubParam
{
public DeformationParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public DeformationParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
#if CLIENT
Deformations = new Dictionary<SpriteDeformationParams, XElement>();
foreach (var deformationElement in element.GetChildElements("spritedeformation"))
{
string typeName = deformationElement.GetAttributeString("typename", null) ?? deformationElement.GetAttributeString("type", "");
string typeName = deformationElement.GetAttributeString("type", null) ?? deformationElement.GetAttributeString("typename", string.Empty);
SpriteDeformationParams deformation = null;
switch (typeName.ToLowerInvariant())
{
@@ -956,7 +982,7 @@ namespace Barotrauma
}
if (deformation != null)
{
deformation.TypeName = typeName;
deformation.Type = typeName;
}
Deformations.Add(deformation, deformationElement);
}
@@ -991,7 +1017,7 @@ namespace Barotrauma
public class ColliderParams : SubParam
{
private string name;
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public override string Name
{
get
@@ -1008,16 +1034,16 @@ namespace Barotrauma
}
}
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Radius { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Height { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
{
Name = name;
}
@@ -1029,16 +1055,16 @@ namespace Barotrauma
{
public override string Name => "Light Texture";
[Serialize("Content/Lights/pointlight_bright.png", true), Editable]
[Serialize("Content/Lights/pointlight_bright.png", IsPropertySaveable.Yes), Editable]
public string Texture { get; private set; }
[Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)]
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 Origin { get; set; }
[Serialize("1.0, 1.0", true), Editable(DecimalCount = 2)]
[Serialize("1.0, 1.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 Size { get; set; }
public LightTexture(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
public LightTexture(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
public LightTexture Texture { get; private set; }
@@ -1047,7 +1073,7 @@ namespace Barotrauma
public Lights.LightSourceParams LightSource { get; private set; }
#endif
public LightSourceParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public LightSourceParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
#if CLIENT
LightSource = new Lights.LightSourceParams(element);
@@ -1088,15 +1114,15 @@ namespace Barotrauma
{
public Attack Attack { get; private set; }
public AttackParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public AttackParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
Attack = new Attack(element, ragdoll.SpeciesName);
Attack = new Attack(element, ragdoll.SpeciesName.Value);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Attack.Deserialize(element ?? Element);
Attack.Deserialize(element ?? Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
return SerializableProperties != null;
}
@@ -1110,19 +1136,19 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
Attack.Deserialize(OriginalElement);
Attack.ReloadAfflictions(OriginalElement);
Attack.Deserialize(OriginalElement, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
Attack.ReloadAfflictions(OriginalElement, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
}
public bool AddNewAffliction()
{
Serialize();
var subElement = new XElement("affliction",
var subElement = CreateElement("affliction",
new XAttribute("identifier", "internaldamage"),
new XAttribute("strength", 0f),
new XAttribute("probability", 1.0f));
Element.Add(subElement);
Attack.ReloadAfflictions(Element);
Attack.ReloadAfflictions(Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
Serialize();
return true;
}
@@ -1131,7 +1157,7 @@ namespace Barotrauma
{
Serialize();
affliction.Remove();
Attack.ReloadAfflictions(Element);
Attack.ReloadAfflictions(Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
return Serialize();
}
}
@@ -1140,9 +1166,9 @@ namespace Barotrauma
{
public DamageModifier DamageModifier { get; private set; }
public DamageModifierParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
public DamageModifierParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
DamageModifier = new DamageModifier(element, ragdoll.SpeciesName);
DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
@@ -1170,24 +1196,27 @@ namespace Barotrauma
{
public override string Name => "Sound";
[Serialize("", true), Editable]
[Serialize("", IsPropertySaveable.Yes), Editable]
public string Tag { get; private set; }
public SoundParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
public SoundParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
public abstract class SubParam : ISerializableEntity
{
public virtual string Name { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public XElement Element { get; set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public ContentXElement Element { get; set; }
public XElement OriginalElement { get; protected set; }
public List<SubParam> SubParams { get; set; } = new List<SubParam>();
public RagdollParams Ragdoll { get; private set; }
public virtual string GenerateName() => Element.Name.ToString();
public SubParam(XElement element, RagdollParams ragdoll)
protected ContentXElement CreateElement(string name, params object[] attrs)
=> new XElement(name, attrs).FromPackage(Element.ContentPackage);
public SubParam(ContentXElement element, RagdollParams ragdoll)
{
Element = element;
OriginalElement = new XElement(element);
@@ -1226,7 +1255,7 @@ namespace Barotrauma
public virtual void Reset()
{
// Don't use recursion, because the reset method might be overriden
Deserialize(OriginalElement, false);
Deserialize(OriginalElement, recursive: false);
SubParams.ForEach(sp => sp.Reset());
}
@@ -1235,21 +1264,21 @@ namespace Barotrauma
public Dictionary<Affliction, SerializableEntityEditor> AfflictionEditors { get; private set; }
public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUI.LargeFont);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUIStyle.LargeFont);
if (this is DecorativeSpriteParams decSpriteParams)
{
new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUI.LargeFont);
new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUIStyle.LargeFont);
}
else if (this is DeformSpriteParams deformSpriteParams)
{
foreach (var deformation in deformSpriteParams.Deformation.Deformations.Keys)
{
new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUI.LargeFont);
new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUIStyle.LargeFont);
}
}
else if (this is AttackParams attackParams)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUI.LargeFont);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUIStyle.LargeFont);
if (AfflictionEditors == null)
{
AfflictionEditors = new Dictionary<Affliction, SerializableEntityEditor>();
@@ -1267,11 +1296,11 @@ namespace Barotrauma
}
else if (this is LightSourceParams lightParams)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUI.LargeFont);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUIStyle.LargeFont);
}
else if (this is DamageModifierParams damageModifierParams)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUI.LargeFont);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUIStyle.LargeFont);
}
if (recursive)
{
@@ -5,20 +5,17 @@ using System.Xml.Linq;
namespace Barotrauma
{
class SkillSettings : ISerializableEntity
class SkillSettings : Prefab, ISerializableEntity
{
public static SkillSettings Current
{
get;
private set;
}
public readonly static PrefabSelector<SkillSettings> Prefabs = new PrefabSelector<SkillSettings>();
public static SkillSettings Current => Prefabs.ActivePrefab;
[Serialize(4.0f, true)]
[Serialize(4.0f, IsPropertySaveable.Yes)]
public float SingleRoundSkillGainMultiplier { get; set; }
private float skillIncreasePerRepair;
[Serialize(5.0f, true)]
[Serialize(5.0f, IsPropertySaveable.Yes)]
public float SkillIncreasePerRepair
{
get { return skillIncreasePerRepair * GetCurrentSkillGainMultiplier(); }
@@ -26,7 +23,7 @@ namespace Barotrauma
}
private float skillIncreasePerSabotage;
[Serialize(3.0f, true)]
[Serialize(3.0f, IsPropertySaveable.Yes)]
public float SkillIncreasePerSabotage
{
get { return skillIncreasePerSabotage * GetCurrentSkillGainMultiplier(); }
@@ -34,7 +31,7 @@ namespace Barotrauma
}
private float skillIncreasePerCprRevive;
[Serialize(0.5f, true)]
[Serialize(0.5f, IsPropertySaveable.Yes)]
public float SkillIncreasePerCprRevive
{
get { return skillIncreasePerCprRevive * GetCurrentSkillGainMultiplier(); }
@@ -42,7 +39,7 @@ namespace Barotrauma
}
private float skillIncreasePerRepairedStructureDamage;
[Serialize(0.0025f, true)]
[Serialize(0.0025f, IsPropertySaveable.Yes)]
public float SkillIncreasePerRepairedStructureDamage
{
get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); }
@@ -50,7 +47,7 @@ namespace Barotrauma
}
private float skillIncreasePerSecondWhenSteering;
[Serialize(0.005f, true)]
[Serialize(0.005f, IsPropertySaveable.Yes)]
public float SkillIncreasePerSecondWhenSteering
{
get { return skillIncreasePerSecondWhenSteering * GetCurrentSkillGainMultiplier(); }
@@ -58,7 +55,7 @@ namespace Barotrauma
}
private float skillIncreasePerFabricatorRequiredSkill;
[Serialize(0.5f, true)]
[Serialize(0.5f, IsPropertySaveable.Yes)]
public float SkillIncreasePerFabricatorRequiredSkill
{
get { return skillIncreasePerFabricatorRequiredSkill * GetCurrentSkillGainMultiplier(); }
@@ -66,7 +63,7 @@ namespace Barotrauma
}
private float skillIncreasePerHostileDamage;
[Serialize(0.01f, true)]
[Serialize(0.01f, IsPropertySaveable.Yes)]
public float SkillIncreasePerHostileDamage
{
get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); }
@@ -74,7 +71,7 @@ namespace Barotrauma
}
private float skillIncreasePerSecondWhenOperatingTurret;
[Serialize(0.001f, true)]
[Serialize(0.001f, IsPropertySaveable.Yes)]
public float SkillIncreasePerSecondWhenOperatingTurret
{
get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); }
@@ -82,64 +79,40 @@ namespace Barotrauma
}
private float skillIncreasePerFriendlyHealed;
[Serialize(0.001f, true)]
[Serialize(0.001f, IsPropertySaveable.Yes)]
public float SkillIncreasePerFriendlyHealed
{
get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFriendlyHealed = value; }
}
[Serialize(1.1f, true)]
[Serialize(1.1f, IsPropertySaveable.Yes)]
public float AssistantSkillIncreaseMultiplier
{
get;
set;
}
[Serialize(200.0f, true)]
[Serialize(200.0f, IsPropertySaveable.Yes)]
public float MaximumSkillWithTalents
{
get;
set;
}
private SkillSettings(XElement element)
public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier())
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public string Name => "SkillSettings";
public Dictionary<string, SerializableProperty> SerializableProperties
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
set;
}
public static void Load(IEnumerable<ContentFile> files)
{
//reverse order to respect content package load order (last file overrides others)
foreach (ContentFile file in files.Reverse())
{
if (file.Type != ContentType.SkillSettings)
{
throw new ArgumentException();
}
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
Current = new SkillSettings(doc.Root);
break;
}
if (Current == null)
{
DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values.");
Current = new SkillSettings(null);
}
}
private float GetCurrentSkillGainMultiplier()
{
if (GameMain.GameSession?.GameMode is CampaignMode)
@@ -151,5 +124,7 @@ namespace Barotrauma
return SingleRoundSkillGainMultiplier;
}
}
public override void Dispose() { }
}
}
@@ -14,7 +14,7 @@ namespace Barotrauma.Abilities
public virtual bool AllowClientSimulation => true;
public AbilityCondition(CharacterTalent characterTalent, XElement conditionElement)
public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement)
{
this.characterTalent = characterTalent;
character = characterTalent.Character;
@@ -8,7 +8,7 @@ namespace Barotrauma.Abilities
class AbilityConditionAffliction : AbilityConditionData
{
private readonly string[] afflictions;
public AbilityConditionAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true);
}
@@ -1,5 +1,5 @@
using Barotrauma.Items.Components;
using System;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -25,10 +25,10 @@ namespace Barotrauma.Abilities
private readonly string[] tags;
private readonly WeaponType weapontype;
private readonly bool ignoreNonHarmfulAttacks;
public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty);
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false);
string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any");
@@ -54,7 +54,7 @@ namespace Barotrauma.Abilities
if (!string.IsNullOrEmpty(itemIdentifier))
{
if (item?.prefab.Identifier != itemIdentifier)
if (item?.Prefab.Identifier != itemIdentifier)
{
return false;
}
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using System;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -8,11 +9,11 @@ namespace Barotrauma.Abilities
class AbilityConditionAttackResult : AbilityConditionData
{
private readonly List<TargetType> targetTypes;
private readonly string[] afflictions;
public AbilityConditionAttackResult(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
private readonly Identifier[] afflictions;
public AbilityConditionAttackResult(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true));
afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true);
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>()));
afflictions = conditionElement.GetAttributeIdentifierArray("afflictions", Array.Empty<Identifier>());
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -10,9 +10,9 @@ namespace Barotrauma.Abilities
private List<PropertyConditional> conditionals = new List<PropertyConditional>();
public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true));
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true));
foreach (XElement subElement in conditionElement.Elements())
{
@@ -14,7 +14,7 @@ namespace Barotrauma.Abilities
///
/// These conditions will return an error if used outside their limited intended use.
/// </summary>
public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
public AbilityConditionData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
@@ -4,7 +4,7 @@ namespace Barotrauma.Abilities
{
class AbilityConditionEvasiveManeuvers : AbilityConditionData
{
public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
@@ -5,7 +5,7 @@ namespace Barotrauma.Abilities
class AbilityConditionGeneHarvester : AbilityConditionData
{
public AbilityConditionGeneHarvester(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
public AbilityConditionGeneHarvester(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
@@ -15,7 +15,7 @@ namespace Barotrauma.Abilities
private readonly bool hittingCountsAsAiming;
private readonly WeaponType weapontype;
public AbilityConditionIsAiming(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionIsAiming(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
hittingCountsAsAiming = conditionElement.GetAttributeBool("hittingcountsasaiming", false);
switch (conditionElement.GetAttributeString("weapontype", ""))
@@ -9,7 +9,7 @@ namespace Barotrauma.Abilities
private readonly string[] identifiers;
private readonly string[] tags;
public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
@@ -6,9 +6,9 @@ namespace Barotrauma.Abilities
{
private readonly SubmarineType? submarineType;
public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("submarinetype") != null)
if (conditionElement.GetAttribute("submarinetype") != null)
{
submarineType = conditionElement.GetAttributeEnum<SubmarineType>("submarinetype", SubmarineType.Player);
}
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -6,15 +7,15 @@ namespace Barotrauma.Abilities
class AbilityConditionLocation : AbilityConditionData
{
private readonly bool? hasOutpost;
private readonly string[] locationIdentifiers;
private readonly Identifier[] locationIdentifiers;
public AbilityConditionLocation(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("hasoutpost") != null)
if (conditionElement.GetAttribute("hasoutpost") != null)
{
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
}
locationIdentifiers = conditionElement.GetAttributeStringArray("locationtype", new string[0]);
locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>());
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -7,7 +7,7 @@ namespace Barotrauma.Abilities
class AbilityConditionMission : AbilityConditionData
{
private readonly MissionType missionType;
public AbilityConditionMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
string missionTypeString = conditionElement.GetAttributeString("missiontype", "None");
if (!Enum.TryParse(missionTypeString, out missionType))
@@ -1,4 +1,5 @@
using System.Xml.Linq;
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -7,9 +8,9 @@ namespace Barotrauma.Abilities
private readonly string[] allowedTypes;
private readonly string identifier;
public AbilityConditionReduceAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionReduceAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", new string[0], convertToLowerInvariant: true);
allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", Array.Empty<string>(), convertToLowerInvariant: true);
identifier = conditionElement.GetAttributeString("identifier", "");
}
@@ -6,19 +6,19 @@ namespace Barotrauma.Abilities
{
private readonly string skillIdentifier;
public AbilityConditionSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
}
private bool MatchesConditionSpecific(string skillIdentifier)
private bool MatchesConditionSpecific(Identifier skillIdentifier)
{
return this.skillIdentifier == skillIdentifier;
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier)
if (abilityObject is IAbilitySkillIdentifier { SkillIdentifier: Identifier skillIdentifier })
{
return MatchesConditionSpecific(skillIdentifier);
}
@@ -7,7 +7,7 @@ namespace Barotrauma.Abilities
{
private string effectIdentifier;
public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
effectIdentifier = conditionElement.GetAttributeString("effectidentifier", "").ToLowerInvariant();
}
@@ -6,7 +6,7 @@ namespace Barotrauma.Abilities
{
private readonly float vitalityPercentage;
public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f);
}
@@ -7,7 +7,7 @@ namespace Barotrauma.Abilities
{
float vitalityPercentage;
public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f);
}
@@ -7,7 +7,7 @@ namespace Barotrauma.Abilities
{
private readonly string jobIdentifier;
public AbilityConditionCoauthor(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionCoauthor(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
jobIdentifier = conditionElement.GetAttributeString("jobidentifier", string.Empty);
}

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