(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,149 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive }
abstract partial class AIController : ISteerable
{
public bool Enabled;
public readonly Character Character;
private AIState state;
protected void ResetAITarget()
{
_lastAiTarget = null;
_selectedAiTarget = null;
}
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
// Updated each time the value is updated (also when the value is the same).
protected AITarget _previousAiTarget;
protected AITarget _selectedAiTarget;
public AITarget SelectedAiTarget
{
get { return _selectedAiTarget; }
protected set
{
_previousAiTarget = _selectedAiTarget;
_selectedAiTarget = value;
if (_selectedAiTarget != _previousAiTarget)
{
if (_previousAiTarget != null)
{
_lastAiTarget = _previousAiTarget;
}
OnTargetChanged(_previousAiTarget, _selectedAiTarget);
}
}
}
protected SteeringManager steeringManager;
public SteeringManager SteeringManager
{
get { return steeringManager; }
}
public Vector2 Steering
{
get { return Character.AnimController.TargetMovement; }
set { Character.AnimController.TargetMovement = value; }
}
public Vector2 SimPosition
{
get { return Character.SimPosition; }
}
public Vector2 WorldPosition
{
get { return Character.WorldPosition; }
}
public Vector2 Velocity
{
get { return Character.AnimController.Collider.LinearVelocity; }
}
public virtual bool CanEnterSubmarine
{
get { return true; }
}
public virtual bool CanFlip
{
get { return true; }
}
public virtual AIObjectiveManager ObjectiveManager
{
get { return null; }
}
public AIState State
{
get { return state; }
set
{
if (state == value) { return; }
PreviousState = state;
OnStateChanged(state, value);
state = value;
}
}
public AIState PreviousState { get; protected set; }
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
public IEnumerable<Hull> VisibleHulls
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
return visibleHulls;
}
private set
{
visibleHulls = value;
}
}
public AIController (Character c)
{
Character = c;
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
Enabled = true;
}
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
public virtual void SelectTarget(AITarget target) { }
public virtual void Update(float deltaTime)
{
if (hullVisibilityTimer > 0)
{
hullVisibilityTimer--;
}
else
{
hullVisibilityTimer = hullVisibilityInterval;
VisibleHulls = Character.GetVisibleHulls();
}
}
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
}
}
@@ -0,0 +1,246 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
partial class AITarget
{
public static List<AITarget> List = new List<AITarget>();
private Entity entity;
public Entity Entity
{
get
{
if (entity != null && entity.Removed) { return null; }
return entity;
}
}
private float soundRange;
private float sightRange;
/// <summary>
/// How long does it take for the ai target to fade out if not kept alive.
/// </summary>
public float FadeOutTime { get; private set; } = 2;
public bool Static { get; private set; }
public bool StaticSound { get; private set; }
public bool StaticSight { get; private set; }
public float SoundRange
{
get { return soundRange; }
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace);
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
}
}
public float SightRange
{
get { return sightRange; }
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace);
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
}
}
private float sectorRad = MathHelper.TwoPi;
public float SectorDegrees
{
get { return MathHelper.ToDegrees(sectorRad); }
set { sectorRad = MathHelper.ToRadians(value); }
}
private Vector2 sectorDir;
public Vector2 SectorDir
{
get { return sectorDir; }
set
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
sectorDir = value;
}
}
public float SonarDisruption
{
get;
set;
}
public string SonarLabel;
public string SonarIconIdentifier;
public bool Enabled => SoundRange > 0 || SightRange > 0;
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
public TargetType Type { get; private set; }
public enum TargetType
{
Any,
HumanOnly,
EnemyOnly
}
public Vector2 WorldPosition
{
get
{
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
return Vector2.Zero;
}
return entity.WorldPosition;
}
}
public Vector2 SimPosition
{
get
{
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
return Vector2.Zero;
}
return entity.SimPosition;
}
}
public void Reset()
{
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = StaticSight ? MaxSightRange : MinSightRange;
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
}
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
SoundRange = element.GetAttributeFloat("soundrange", 0.0f);
MinSightRange = element.GetAttributeFloat("minsightrange", 0f);
MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f);
MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange);
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime);
Static = element.GetAttributeBool("static", Static);
StaticSight = element.GetAttributeBool("staticsight", StaticSight);
StaticSound = element.GetAttributeBool("staticsound", StaticSound);
if (Static)
{
StaticSound = true;
StaticSight = true;
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
string typeString = element.GetAttributeString("type", "Any");
if (Enum.TryParse(typeString, out TargetType t))
{
Type = t;
}
Reset();
}
public AITarget(Entity e)
{
entity = e;
List.Add(this);
}
public void Update(float deltaTime)
{
if (Enabled && !Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound)
{
DecreaseSoundRange(deltaTime);
}
}
}
public void IncreaseSoundRange(float deltaTime, float speed = 1)
{
SoundRange += speed * deltaTime * (MaxSoundRange / FadeOutTime);
}
public void IncreaseSightRange(float deltaTime, float speed = 1)
{
SightRange += speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public void DecreaseSoundRange(float deltaTime, float speed = 1)
{
SoundRange -= speed * deltaTime * (MaxSoundRange / FadeOutTime);
}
public void DecreaseSightRange(float deltaTime, float speed = 1)
{
SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) return true;
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
}
public void Remove()
{
List.Remove(this);
entity = null;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
interface ISteerable
{
Vector2 Steering
{
get;
set;
}
Vector2 Velocity
{
get;
}
Vector2 SimPosition
{
get;
}
Vector2 WorldPosition
{
get;
}
}
}
@@ -0,0 +1,617 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
class IndoorsSteeringManager : SteeringManager
{
private PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors, canBreakDoors;
private Character character;
private Vector2 currentTarget;
private float findPathTimer;
private float buttonPressCooldown;
const float ButtonPressInterval = 0.5f;
public SteeringPath CurrentPath
{
get { return currentPath; }
}
public PathFinder PathFinder
{
get { return pathFinder; }
}
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty
{
get;
private set;
}
/// <summary>
/// Returns true if the current or the next node is in ladders.
/// </summary>
public bool InLadders =>
currentPath != null &&
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null));
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsNextNodeLadder => GetNextLadder() != null;
public bool IsNextLadderSameAsCurrent
{
get
{
if (currentPath == null) { return false; }
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
}
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), indoorsSteering: true);
pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors;
this.canBreakDoors = canBreakDoors;
character = (host as AIController).Character;
findPathTimer = Rand.Range(0.0f, 1.0f);
}
public override void Update(float speed)
{
base.Update(speed);
buttonPressCooldown -= 1.0f / 60.0f;
findPathTimer -= 1.0f / 60.0f;
}
public void SetPath(SteeringPath path)
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
findPathTimer = 1.0f;
IsPathDirty = false;
}
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
}
/// <summary>
/// Seeks the ladder from the current and the next two nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null)
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
return node.Ladders;
}
return null;
}
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (findPathTimer > 0.0f) { return Vector2.Zero; }
currentTarget = target;
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
{
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
currentPos -= targetHull.Submarine.SimPosition;
}
}
pathFinder.InsideSubmarine = character.Submarine != null;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter);
bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
if (useNewPath)
{
currentPath = newPath;
}
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
}
Vector2 diff = DiffToCurrentNode();
var collider = character.AnimController.Collider;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
{
diff.Y = 0.0f;
}
//if (diff.LengthSquared() < 0.001f) { return -host.Steering; }
if (diff == Vector2.Zero) { return Vector2.Zero; }
return Vector2.Normalize(diff) * weight;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null);
private Vector2 DiffToCurrentNode()
{
if (currentPath == null || currentPath.Unreachable) return Vector2.Zero;
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
if (character != null && character.Submarine == null &&
CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
{
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
}
if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f)
{
CheckDoorsInPath();
}
Vector2 pos = host.SimPosition;
if (character != null && currentPath.CurrentNode != null)
{
if (CurrentPath.CurrentNode.Submarine != null)
{
if (character.Submarine == null)
{
pos -= CurrentPath.CurrentNode.Submarine.SimPosition;
}
else if (character.Submarine != currentPath.CurrentNode.Submarine)
{
pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
}
}
}
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
//only humanoids can climb ladders
if (!isDiving && character.AnimController is HumanoidAnimController && IsNextLadderSameAsCurrent)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
{
currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true);
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
{
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
Ladder nextLadder = GetNextLadder();
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent)
{
//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 heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
if (heightFromFloor <= 0.0f)
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
else if (!nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
{
nextLadder.Item.TryInteract(character, false, true);
}
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
if (nextLadder != null || isAboveFloor)
{
currentPath.SkipToNextNode();
}
}
else if (nextLadder != null)
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
currentPath.SkipToNextNode();
}
}
return diff;
}
else if (character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
}
}
else if (!IsNextLadderSameAsCurrent)
{
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is smaller than this, it fails to use the waypoint nodes, because they are always too high.
float minHeight = 1;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
float targetDistance = collider.radius * margin;
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh)
{
currentPath.SkipToNextNode();
}
}
if (currentPath.CurrentNode == null) return Vector2.Zero;
return currentPath.CurrentNode.SimPosition - pos;
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen) { return true; }
if (canBreakDoors) { return true; }
if (door.IsStuck) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (door.HasIntegratedButtons)
{
return door.CanBeOpenedWithoutTools(character);
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
}
}
private void CheckDoorsInPath()
{
for (int i = 0; i < 2; i++)
{
WayPoint currentWaypoint = null;
WayPoint nextWaypoint = null;
Door door = null;
bool shouldBeOpen = false;
if (currentPath.Nodes.Count == 1)
{
door = currentPath.Nodes.First().ConnectedDoor;
shouldBeOpen = door != null;
}
else
{
if (i == 0)
{
currentWaypoint = currentPath.CurrentNode;
nextWaypoint = currentPath.NextNode;
}
else
{
currentWaypoint = currentPath.PrevNode;
nextWaypoint = currentPath.CurrentNode;
}
if (currentWaypoint?.ConnectedDoor == null) { continue; }
if (nextWaypoint == null)
{
//the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node
shouldBeOpen = true;
}
else
{
door = currentWaypoint.ConnectedGap.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f;
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f;
}
}
}
if (door == null) { return; }
//toggle the door if it's the previous node and open, or if it's current node and closed
if (door.IsOpen != shouldBeOpen)
{
Controller closestButton = null;
float closestDist = 0;
bool canAccess = CanAccessDoor(door, button =>
{
if (currentWaypoint == null) { return true; }
// Check that the button is on the right side of the door.
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
if (closestButton == null || distance < closestDist)
{
closestButton = button;
closestDist = distance;
}
return true;
});
if (canAccess)
{
if (door.HasIntegratedButtons)
{
door.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
break;
}
else if (closestButton != null)
{
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2))
{
closestButton.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
// Can't reach the button closest to the character.
// It's possible that we could reach another buttons.
// If this becomes an issue, we could go through them here and check if any of them are reachable
// (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
if (body != null)
{
if (body.UserData is Item item)
{
var d = item.GetComponent<Door>();
if (d == null || d.IsOpen) { return; }
}
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
return;
}
}
}
else if (shouldBeOpen)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
#endif
currentPath.Unreachable = true;
return;
}
}
}
}
private float? GetNodePenalty(PathNode node, PathNode nextNode)
{
if (character == null) { return 0.0f; }
if (nextNode.Waypoint.isObstructed) { return null; }
float penalty = 0.0f;
if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f)
{
var door = nextNode.Waypoint.ConnectedDoor;
if (door == null)
{
penalty = 100.0f;
}
else
{
if (!CanAccessDoor(door, button =>
{
// Ignore buttons that are on the wrong side of the door
if (door.IsHorizontal)
{
if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
{
return false;
}
}
else
{
if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
{
return false;
}
}
return true;
}))
{
return null;
}
}
}
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null &&
nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater
{
return null;
}
}
if (node.Waypoint != null && node.Waypoint.CurrentHull != null)
{
var hull = node.Waypoint.CurrentHull;
if (hull.FireSources.Count > 0)
{
foreach (FireSource fs in hull.FireSources)
{
penalty += fs.Size.X * 10.0f;
}
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
penalty += 1000.0f;
}
}
return penalty;
}
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
{
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
var currentHull = character.CurrentHull;
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;
if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4)
{
Reset();
}
else
{
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
{
if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
{
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
return;
}
else if (stayStillInTightSpace)
{
Reset();
return;
}
}
if (leftDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = 0.0f;
}
else if (rightDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = MathHelper.Pi;
}
else
{
wander = true;
}
}
}
else
{
wander = true;
}
if (wander)
{
SteeringWander();
SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5);
}
if (!inWater)
{
//reset vertical steering to prevent dropping down from platforms etc
ResetY();
}
}
}
}
@@ -0,0 +1,307 @@
using FarseerPhysics;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class LatchOntoAI
{
const float RaycastInterval = 5.0f;
private float raycastTimer;
private Body attachTargetBody;
private Vector2 attachSurfaceNormal;
private Submarine attachTargetSubmarine;
private bool attachToSub;
private bool attachToWalls;
private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f;
private float damageOnDetach = 0.0f, detachStun = 0.0f;
private float deattachTimer;
private Vector2 wallAttachPos;
private float attachCooldown;
private Limb attachLimb;
private Vector2 localAttachPos;
private float attachLimbRotation;
private float jointDir;
private List<WeldJoint> attachJoints = new List<WeldJoint>();
public List<WeldJoint> AttachJoints
{
get { return attachJoints; }
}
public Vector2? WallAttachPos
{
get;
private set;
}
public bool IsAttached
{
get { return attachJoints.Count > 0; }
}
public bool IsAttachedToSub => IsAttached && (attachTargetBody?.UserData is Submarine || attachTargetBody?.UserData is Entity entity && entity.Submarine != null);
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
attachToWalls = element.GetAttributeBool("attachtowalls", false);
attachToSub = element.GetAttributeBool("attachtosub", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f));
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType))
{
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
}
if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb;
enemyAI.Character.OnDeath += OnCharacterDeath;
}
public void SetAttachTarget(Body attachTarget, Submarine attachTargetSub, Vector2 attachPos, Vector2 attachSurfaceNormal)
{
attachTargetBody = attachTarget;
attachTargetSubmarine = attachTargetSub;
this.attachSurfaceNormal = attachSurfaceNormal;
wallAttachPos = attachPos;
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
Character character = enemyAI.Character;
if (character.Submarine != null)
{
DeattachFromBody();
WallAttachPos = null;
return;
}
if (attachJoints.Count > 0)
{
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
{
attachJoints[0].LocalAnchorA =
new Vector2(-attachJoints[0].LocalAnchorA.X, attachJoints[0].LocalAnchorA.Y);
attachJoints[0].ReferenceAngle = -attachJoints[0].ReferenceAngle;
jointDir = attachLimb.Dir;
}
for (int i = 0; i < attachJoints.Count; i++)
{
//something went wrong, limb body is very far from the joint anchor -> deattach
if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f)
{
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
DeattachFromBody();
return;
}
}
}
attachCooldown -= deltaTime;
deattachTimer -= deltaTime;
Vector2 transformedAttachPos = wallAttachPos;
if (character.Submarine == null && attachTargetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(attachTargetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
WallAttachPos = transformedAttachPos;
}
switch (enemyAI.State)
{
case AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
{
if (!IsAttached)
{
raycastTimer -= deltaTime;
//check if there are any walls nearby the character could attach to
if (raycastTimer < 0.0f)
{
wallAttachPos = Vector2.Zero;
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
{
float closestDist = float.PositiveInfinity;
foreach (Voronoi2.VoronoiCell cell in cells)
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
attachSurfaceNormal = edge.GetNormal(cell);
attachTargetBody = cell.Body;
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
if (distSqr < closestDist)
{
wallAttachPos = potentialAttachPos;
closestDist = distSqr;
}
break;
}
}
}
}
raycastTimer = RaycastInterval;
}
}
}
else
{
wallAttachPos = Vector2.Zero;
}
if (wallAttachPos == Vector2.Zero)
{
DeattachFromBody();
}
else
{
float dist = Vector2.Distance(character.SimPosition, wallAttachPos);
if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f)
{
//close enough to a wall -> attach
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos);
enemyAI.SteeringManager.Reset();
}
else
{
//move closer to the wall
DeattachFromBody();
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos);
}
}
break;
case AIState.Attack:
if (enemyAI.AttackingLimb != null)
{
if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null)
{
// is not attached or is attached to something else
if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody)
{
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos);
}
}
}
}
break;
default:
WallAttachPos = null;
DeattachFromBody();
break;
}
if (IsAttached && attachTargetBody != null && deattachTimer < 0.0f)
{
Entity entity = attachTargetBody.UserData as Entity;
Submarine attachedSub = entity is Submarine sub ? sub : entity?.Submarine;
if (attachedSub != null)
{
float velocity = attachedSub.Velocity == Vector2.Zero ? 0.0f : attachedSub.Velocity.Length();
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
{
DeattachFromBody();
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = 5.0f;
}
}
deattachTimer = 5.0f;
}
}
private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos)
{
//already attached to something
if (attachJoints.Count > 0)
{
//already attached to the target body, no need to do anything
if (attachJoints[0].BodyB == targetBody) return;
DeattachFromBody();
}
jointDir = attachLimb.Dir;
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.character.AnimController.RagdollParams.LimbScale;
if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
//transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation));
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody,
transformedLocalAttachPos, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
};
GameMain.World.Add(limbJoint);
attachJoints.Add(limbJoint);
// Limb scale is already taken into account when creating the collider.
Vector2 colliderFront = collider.GetLocalFront();
if (jointDir < 0.0f) colliderFront.X = -colliderFront.X;
collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
//Length = 0.1f
};
GameMain.World.Add(colliderJoint);
attachJoints.Add(colliderJoint);
}
public void DeattachFromBody()
{
foreach (Joint joint in attachJoints)
{
GameMain.World.Remove(joint);
}
attachJoints.Clear();
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
{
DeattachFromBody();
character.OnDeath -= OnCharacterDeath;
}
}
}
@@ -0,0 +1,441 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
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 List<string> Flags;
//The line can only be selected when eventmanager intensity is between these values
//null = no restriction
public float? maxIntensity, minIntensity;
public readonly List<NPCConversation> Responses;
private readonly int speakerIndex;
private readonly List<string> allowedSpeakerTags;
public static void LoadAll(IEnumerable<ContentFile> files)
{
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());
}
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));
}
}
private static List<string> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<string>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) currentFlags.Add("SubmarineDeep");
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) currentFlags.Add("Initial");
if (speaker != null)
{
if (speaker.AnimController.InWater) currentFlags.Add("Underwater");
currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside");
if (Character.Controlled != null)
{
if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null)
{
currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf");
}
}
var afflictions = speaker.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength);
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
}
}
}
return currentFlags;
}
private static List<NPCConversation> previousConversations = new List<NPCConversation>();
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers)
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
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());
return lines;
}
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, List<string> 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();
if (availableConversations.Count > 0)
{
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true);
}
return lines;
}
private static void CreateConversation(
List<Character> availableSpeakers,
Dictionary<int, Character> assignedSpeakers,
NPCConversation baseConversation,
List<Pair<Character, string>> lineList,
List<NPCConversation> availableConversations,
bool ignoreFlags = false)
{
List<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
if (conversations.Count == 0) return;
int conversationIndex = Rand.Int(conversations.Count);
NPCConversation selectedConversation = conversations[conversationIndex];
if (string.IsNullOrEmpty(selectedConversation.Line)) return;
Character speaker = null;
//speaker already assigned for this line
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
{
//check if the character has all required flags to say the line
var characterFlags = GetCurrentFlags(assignedSpeakers[selectedConversation.speakerIndex]);
if (selectedConversation.Flags.All(flag => characterFlags.Contains(flag)))
{
speaker = assignedSpeakers[selectedConversation.speakerIndex];
}
}
if (speaker == null)
{
var allowedSpeakers = new List<Character>();
List<NPCConversation> potentialLines = new List<NPCConversation>(conversations);
//remove lines that are not appropriate for the intensity of the current situation
if (GameMain.GameSession?.EventManager != null)
{
potentialLines.RemoveAll(l =>
(l.minIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity < l.minIntensity) ||
(l.maxIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity > l.maxIntensity));
}
while (potentialLines.Count > 0)
{
//select a random line and attempt to find a speaker for it
// and if no valid speaker is found, choose another random line
selectedConversation = GetRandomConversation(potentialLines, baseConversation == null);
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) return;
//speaker already assigned for this line
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
{
speaker = assignedSpeakers[selectedConversation.speakerIndex];
break;
}
foreach (Character potentialSpeaker in availableSpeakers)
{
//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)) continue;
}
//check if the character has all required flags to say the line
if (!ignoreFlags)
{
var characterFlags = GetCurrentFlags(potentialSpeaker);
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) continue;
}
//check if the character has an appropriate personality
if (selectedConversation.allowedSpeakerTags.Count > 0)
{
if (potentialSpeaker.Info?.PersonalityTrait == null) continue;
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) continue;
}
else
{
if (potentialSpeaker.Info?.PersonalityTrait != null &&
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
{
continue;
}
}
allowedSpeakers.Add(potentialSpeaker);
}
if (allowedSpeakers.Count == 0)
{
potentialLines.Remove(selectedConversation);
}
else
{
break;
}
}
if (allowedSpeakers.Count == 0) return;
speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)];
availableSpeakers.Remove(speaker);
assignedSpeakers.Add(selectedConversation.speakerIndex, speaker);
}
if (baseConversation == null)
{
previousConversations.Insert(0, selectedConversation);
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
}
lineList.Add(new Pair<Character, string>(speaker, selectedConversation.Line));
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
}
private static NPCConversation GetRandomConversation(List<NPCConversation> conversations, bool avoidPreviouslyUsed)
{
if (!avoidPreviouslyUsed)
{
return conversations.Count == 0 ? null : conversations[Rand.Int(conversations.Count)];
}
List<float> probabilities = new List<float>();
foreach (NPCConversation conversation in conversations)
{
probabilities.Add(GetConversationProbability(conversation));
}
return ToolBox.SelectWeightedRandom(conversations, probabilities, Rand.RandSync.Unsynced);
}
private static float GetConversationProbability(NPCConversation conversation)
{
int index = previousConversations.IndexOf(conversation);
if (index < 0) return 10.0f;
return 1.0f - 1.0f / (index + 1);
}
#if DEBUG
public static void WriteToCSV()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (string key in allConversations.Keys)
{
foreach (string lang in allConversations[key].Conversations.Keys)
{
if (lang != TextManager.Language) { continue; }
foreach (var current in allConversations[key].Conversations[lang])
{
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
}
}
}
StreamWriter file = new StreamWriter(@"NPCConversations.csv");
file.WriteLine(sb.ToString());
file.Close();
}
private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex)
{
sb.Append(conv.speakerIndex); // Speaker index
sb.Append('*');
sb.Append(depthIndex); // Depth index
sb.Append('*');
sb.Append(conv.Line); // Original
sb.Append('*');
// Translated
sb.Append('*');
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('*');
sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits
sb.Append('*');
sb.Append(conv.minIntensity); // Minimum intensity
sb.Append('*');
sb.Append(conv.maxIntensity); // Maximum intensity
sb.Append('*');
// Comments
sb.AppendLine();
}
private static void WriteSubConversations(System.Text.StringBuilder sb, List<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)
{
WriteSubConversations(sb, responses[i].Responses, depthIndex + 1);
}
}
}
private static void WriteEmptyRow(System.Text.StringBuilder sb)
{
for (int i = 0; i < 8; i++)
{
sb.Append('*');
}
sb.AppendLine();
}
#endif
}
}
@@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AIObjective
{
public virtual float Devotion => AIObjectiveManager.baseDevotion;
public abstract string DebugTag { get; }
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
/// <summary>
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
/// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues
/// </summary>
public virtual bool AllowMultipleInstances => false;
/// <summary>
/// Run the main objective with all subobjectives concurrently?
/// If false, the main objective will continue only when all the subobjectives have been removed (done).
/// </summary>
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
protected float CumulatedDevotion
{
get { return _cumulatedDevotion; }
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
}
protected virtual float MaxDevotion => 10;
/// <summary>
/// Final priority value after all calculations.
/// </summary>
public float Priority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
public string Option { get; private set; }
private bool _abandon;
public bool Abandon
{
get { return _abandon; }
set
{
_abandon = value;
if (_abandon)
{
OnAbandon();
}
}
}
public virtual bool CanBeCompleted => !Abandon;
/// <summary>
/// When true, the objective is never completed, unless CanBeCompleted returns false.
/// </summary>
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
{
all.Clear();
if (includingSelf)
{
all.Add(this);
}
foreach (var subObjective in subObjectives)
{
all.AddRange(subObjective.GetSubObjectivesRecursive(true));
}
return all;
}
public event Action Completed;
public event Action Abandoned;
public event Action Selected;
public event Action Deselected;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
public AIObjective GetActiveObjective()
{
var subObjective = CurrentSubObjective;
return subObjective == null ? this : subObjective.GetActiveObjective();
}
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
{
this.objectiveManager = objectiveManager;
this.character = character;
Option = option ?? string.Empty;
PriorityModifier = priorityModifier;
}
/// <summary>
/// Makes the character act according to the objective, or according to any subobjectives that need to be completed before this one
/// </summary>
public void TryComplete(float deltaTime)
{
if (isCompleted) { return; }
//if (Abandon && !IsLoop && subObjectives.None()) { return; }
if (CheckState()) { return; }
// Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks.
for (int i = 0; i < subObjectives.Count; i++)
{
subObjectives[i].TryComplete(deltaTime);
if (!ConcurrentObjectives) { return; }
}
Act(deltaTime);
}
// TODO: check turret aioperate
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
subObjectives.RemoveAll(o => o.GetType() == type);
if (addFirst)
{
subObjectives.Insert(0, objective);
}
else
{
subObjectives.Add(objective);
}
}
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
{
if (objective != null)
{
if (subObjectives.Contains(objective))
{
subObjectives.Remove(objective);
}
objective = null;
}
}
public void SortSubObjectives()
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
subObjectives.ForEach(so => so.GetPriority());
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
if (ConcurrentObjectives)
{
subObjectives.ForEach(so => so.SortSubObjectives());
}
else
{
subObjectives.First().SortSubObjectives();
}
}
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
/// </summary>
public virtual float GetPriority()
{
Priority = CumulatedDevotion * PriorityModifier;
return Priority;
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
{
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
}
}
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
else if (objectiveManager.WaitTimer <= 0)
{
UpdateDevotion(deltaTime);
}
subObjectives.ForEach(so => so.Update(deltaTime));
}
/// <summary>
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
/// </summary>
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
{
foreach (T1 key in collection)
{
if (dictionary.TryGetValue(key, out T2 objective))
{
if (!subObjectives.Contains(objective))
{
dictionary.Remove(key);
}
}
}
}
/// <summary>
/// Checks if the objective already is created and added in subobjectives. If not, creates it.
/// Handles objectives that cannot be completed. If the objective has been removed form the subobjectives, a null value is assigned to the reference.
/// Returns true if the objective was created and successfully added.
/// </summary>
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective
{
if (objective != null)
{
// Sub objective already found, no need to do anything if it remains in the subobjectives
// If the sub objective is removed -> it's either completed or impossible to complete.
if (!subObjectives.Contains(objective))
{
objective = null;
}
return false;
}
else
{
objective = constructor();
if (!subObjectives.Contains(objective))
{
if (objective.AllowMultipleInstances)
{
subObjectives.Add(objective);
}
else
{
AddSubObjective(objective);
}
if (onCompleted != null)
{
objective.Completed += onCompleted;
}
if (onAbandon != null)
{
objective.Abandoned += onAbandon;
}
return true;
}
#if DEBUG
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
#endif
return false;
}
}
public virtual void OnSelected()
{
Reset();
Selected?.Invoke();
}
public virtual void OnDeselected()
{
CumulatedDevotion = 0;
Deselected?.Invoke();
}
protected virtual void OnCompleted()
{
Completed?.Invoke();
}
protected virtual void OnAbandon()
{
Abandoned?.Invoke();
}
public virtual void Reset()
{
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
CumulatedDevotion = 0;
}
protected abstract void Act(float deltaTime);
private bool isCompleted;
private bool hasBeenChecked;
public bool IsCompleted
{
get
{
if (!hasBeenChecked)
{
CheckState();
}
return isCompleted;
}
protected set
{
isCompleted = value;
}
}
protected abstract bool Check();
private bool CheckState()
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None())
{
if (Check())
{
isCompleted = true;
OnCompleted();
}
}
return isCompleted;
}
private void CheckSubObjectives()
{
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
subObjective.CheckState();
if (subObjective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.", Color.LightGreen);
#endif
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
#endif
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
Abandon = true;
}
}
}
}
}
}
@@ -0,0 +1,83 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
public override string DebugTag => "charge batteries";
public override bool UnequipItems => true;
private IEnumerable<PowerContainer> batteryList;
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
var item = battery.Item;
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.ConditionPercentage <= 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { 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;
}
protected override float TargetEvaluation()
{
if (Option == "charge")
{
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio)));
}
else
{
return Targets.Max(t => MathHelper.Lerp(0, 100, t.RechargeRatio));
}
}
protected override IEnumerable<PowerContainer> GetList()
{
if (batteryList == null)
{
if (character == null || character.Submarine == null)
{
return new PowerContainer[0];
}
batteryList = character.Submarine.GetItems(true).Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
}
return batteryList;
}
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
}
else
{
return battery.RechargeRatio <= 0;
}
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
completionCondition = () => IsReady(battery)
};
protected override void OnObjectiveCompleted(AIObjective objective, PowerContainer target)
=> HumanAIController.RemoveTargets<AIObjectiveChargeBatteries, PowerContainer>(character, target);
}
}
@@ -0,0 +1,683 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveCombat : AIObjective
{
public override string DebugTag => "combat";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
const float seekWeaponsInterval = 1;
private float ignoreWeaponTimer;
const float ignoredWeaponsClearTime = 10;
const float coolDown = 10.0f;
// Won't take the offensive with weapons that have lower priority than this
const float goodWeaponPriority = 30;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
private Item _weapon;
private Item Weapon
{
get { return _weapon; }
set
{
_weapon = value;
_weaponComponent = null;
RemoveSubObjective(ref seekAmmunition);
}
}
private ItemComponent _weaponComponent;
private ItemComponent WeaponComponent
{
get
{
if (Weapon == null) { return null; }
if (_weaponComponent == null)
{
_weaponComponent =
Weapon.GetComponent<RangedWeapon>() as ItemComponent ??
Weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
Weapon.GetComponent<RepairTool>() as ItemComponent;
}
return _weaponComponent;
}
}
public override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private AIObjectiveContainItem seekAmmunition;
private AIObjectiveGoTo retreatObjective;
private AIObjectiveGoTo followTargetObjective;
private Hull retreatTarget;
private float coolDownTimer;
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
private float aimTimer;
public enum CombatMode
{
Defensive,
Offensive,
Retreat
}
public CombatMode Mode { get; private set; }
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
Enemy = enemy;
coolDownTimer = coolDown;
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
if (findSafety != null)
{
findSafety.Priority = 0;
HumanAIController.UnreachableHulls.Clear();
}
Mode = mode;
initialMode = Mode;
if (Enemy == null)
{
Mode = CombatMode.Retreat;
}
}
public override float GetPriority()
{
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
return Priority;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
seekWeaponsTimer -= deltaTime;
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
}
protected override bool Check()
{
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
{
if (objectiveManager.CurrentOrder == this && Enemy != null && Enemy.IsDead)
{
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
if (Weapon != null)
{
Unequip();
}
}
return completed;
}
protected override void Act(float deltaTime)
{
if (initialMode != CombatMode.Offensive)
{
coolDownTimer -= deltaTime;
}
if (seekAmmunition == null)
{
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
{
Move();
}
}
}
private void Move()
{
switch (Mode)
{
case CombatMode.Offensive:
Engage();
break;
case CombatMode.Defensive:
case CombatMode.Retreat:
Retreat();
break;
default:
throw new NotImplementedException();
}
}
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
private bool TryArm()
{
if (character.LockHands || Enemy == null)
{
Weapon = null;
return false;
}
if (seekWeaponsTimer < 0)
{
seekWeaponsTimer = seekWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons().ToList();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
if (Weapon == null)
{
// No weapons
break;
}
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
if (initialMode == CombatMode.Offensive)
{
// In the offensive mode, let's ignore weapons that cannot be used in the offensive mode
if (WeaponComponent.CombatPriority < goodWeaponPriority)
{
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
}
if (IsLoaded(WeaponComponent))
{
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
{
// All good, reloading successful
break;
}
else
{
// No ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
}
if (Weapon == null)
{
// No weapon found with the conditions above. Try again, now let's try to seek ammunition too
Weapon = GetWeapon(out _weaponComponent);
if (Weapon != null)
{
if (!CheckWeapon(seekAmmo: true))
{
if (seekAmmunition != null)
{
// No loaded weapon, but we are trying to seek ammunition.
return false;
}
else
{
Weapon = null;
}
}
}
}
}
else
{
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
}
}
if (Weapon == null)
{
Mode = CombatMode.Retreat;
}
else
{
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
}
return Weapon != null;
bool CheckWeapon(bool seekAmmo)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
{
return false;
}
}
return true;
};
}
private void OperateWeapon(float deltaTime)
{
switch (Mode)
{
case CombatMode.Offensive:
case CombatMode.Defensive:
if (Equip())
{
Attack(deltaTime);
}
break;
case CombatMode.Retreat:
break;
default:
throw new NotImplementedException();
}
}
private Item GetWeapon(out ItemComponent weaponComponent)
{
GetAllWeapons();
return GetWeapon(weapons, out weaponComponent);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault();
if (weaponComponent == null) { return null; }
if (weaponComponent.CombatPriority < 1) { return null; }
return weaponComponent.Item;
}
private float CalculateWeaponPriority(ItemComponent weapon)
{
float priority = weapon.CombatPriority;
// Halve the priority for weapons that don't have proper ammunition loaded.
if (!weapon.HasRequiredContainedItems(character, addMessage: false))
{
priority /= 2;
}
return priority;
}
private HashSet<ItemComponent> GetAllWeapons()
{
weapons.Clear();
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
if (ignoredWeapons.Contains(item)) { continue; }
SeekWeapons(item, weapons);
if (item.OwnInventory != null)
{
item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons));
}
}
return weapons;
}
private void SeekWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
{
if (component is RangedWeapon rw)
{
weaponList.Add(rw);
}
else if (component is MeleeWeapon mw)
{
weaponList.Add(mw);
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
foreach (var statusEffects in effects.Values)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.Afflictions.Any())
{
weaponList.Add(component);
}
}
}
}
}
}
}
private void Unequip()
{
if (!character.LockHands && character.SelectedItems.Contains(Weapon))
{
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
{
Weapon.Drop(character);
}
}
}
private bool Equip()
{
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
return false;
}
if (!character.HasEquippedItem(Weapon))
{
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.5f, 1f);
}
else
{
Weapon = null;
Mode = CombatMode.Retreat;
return false;
}
}
return true;
}
private void Retreat()
{
RemoveSubObjective(ref followTargetObjective);
RemoveSubObjective(ref seekAmmunition);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
retreatObjective = null;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
private void Engage()
{
if (character.LockHands || Enemy == null)
{
Mode = CombatMode.Retreat;
SteeringManager.Reset();
return;
}
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunition);
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
followTargetObjective = null;
}
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
{
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName
},
onAbandon: () =>
{
Mode = CombatMode.Defensive;
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
});
if (followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
}
}
/// <summary>
/// Seeks for more ammunition. Creates a new subobjective.
/// </summary>
private void SeekAmmunition(string[] ammunitionIdentifiers)
{
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekAmmunition,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
{
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
checkInventory = false
},
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
onAbandon: () =>
{
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunition);
ignoredWeapons.Add(Weapon);
Weapon = null;
});
}
/// <summary>
/// Reloads the ammunition found in the inventory.
/// If seekAmmo is true, tries to get find the ammo elsewhere.
/// </summary>
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
var containedItems = Weapon.ContainedItems;
// Drop empty ammo
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0)
{
containedItem.Drop(character);
}
}
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
ammunitionIdentifiers = requiredItem.Identifiers;
}
// No ammo
if (ammunition == null)
{
if (ammunitionIdentifiers != null)
{
// Try reload ammunition from inventory
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(ammunition))
{
return false;
}
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
{
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
return true;
}
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
return false;
}
private void Attack(float deltaTime)
{
float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position);
character.CursorPosition = Enemy.Position;
float engageDistance = 500;
if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; }
if (!character.CanSeeCharacter(Enemy)) { return; }
if (Weapon.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
}
bool isFacing = character.AnimController.Dir > 0 && Enemy.WorldPosition.X > character.WorldPosition.X || character.AnimController.Dir < 0 && Enemy.WorldPosition.X < character.WorldPosition.X;
if (!isFacing)
{
aimTimer = Rand.Range(1f, 1.5f);
}
if (aimTimer > 0)
{
aimTimer -= deltaTime;
return;
}
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
}
}
else
{
if (WeaponComponent is RepairTool repairTool)
{
if (squaredDistance > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null)
{
Character target = null;
if (pickedBody.UserData is Character c)
{
target = c;
}
else if (pickedBody.UserData is Limb limb)
{
target = limb.character;
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = reloadTime * Rand.Range(1f, 1.5f);
}
}
}
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
// AttackContext currentContext = character.GetAttackContext();
// foreach (Limb limb in Enemy.AnimController.Limbs)
// {
// if (limb.attack == null) continue;
// if (!limb.attack.IsValidContext(currentContext)) { continue; }
// if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
// enemyStrength += limb.attack.GetTotalDamage(false);
// }
// return enemyStrength;
//}
}
}
@@ -0,0 +1,181 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveContainItem: AIObjective
{
public override string DebugTag => "contain item";
public Func<Item, float> GetItemPriority;
public int targetItemCount = 1;
public string[] ignoredContainerIdentifiers;
public bool checkInventory = true;
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
public readonly Item item;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
private readonly HashSet<Item> containedItems = new HashSet<Item>();
public bool AllowToFindDivingGear { get; set; } = true;
public float ConditionLevel { get; set; }
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.container = container;
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
}
protected override bool Check()
{
if (IsCompleted) { return true; }
if (item != null)
{
return container.Inventory.Items.Contains(item);
}
else
{
int containedItemCount = 0;
foreach (Item i in container.Inventory.Items)
{
if (i != null && CheckItem(i))
{
containedItemCount++;
}
}
return containedItemCount >= targetItemCount;
}
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
protected override void Act(float deltaTime)
{
if (container == null)
{
Abandon = true;
return;
}
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (itemToContain != null)
{
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
{
if (RemoveEmpty)
{
foreach (var emptyItem in container.Inventory.Items)
{
if (emptyItem == null) { continue; }
if (emptyItem.Condition <= 0)
{
emptyItem.Drop(character);
}
}
}
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(itemToContain))
{
Abandon = true;
}
else
{
character.Inventory.RemoveItem(itemToContain);
if (container.Inventory.TryPutItem(itemToContain, null))
{
IsCompleted = true;
}
else
{
itemToContain.Drop(character);
Abandon = true;
}
}
}
else
{
if (container.Combine(itemToContain, character))
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = this.AllowToFindDivingGear
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective.TargetItem != null)
{
containedItems.Add(getItemObjective.TargetItem);
}
else
{
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
RemoveSubObjective(ref getItemObjective);
});
}
}
}
}
@@ -0,0 +1,134 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveDecontainItem : AIObjective
{
public override string DebugTag => "decontain item";
public Func<Item, float> GetItemPriority;
//can either be a tag or an identifier
private readonly string[] itemIdentifiers;
private readonly ItemContainer sourceContainer;
private ItemContainer targetContainer;
private readonly Item targetItem;
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem containObjective;
public bool Equip { get; set; }
/// <summary>
/// If true drops the item when containing the item fails.
/// In both cases abandons the objective.
/// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped.
/// </summary>
public bool DropIfFailsToContain { get; set; } = true;
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
protected override bool Check() => IsCompleted;
protected override void Act(float deltaTime)
{
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
if (itemToDecontain == null)
{
Abandon = true;
return;
}
if (targetContainer == null)
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (itemToDecontain.Container != sourceContainer.Item)
{
IsCompleted = true;
return;
}
}
else
{
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
IsCompleted = true;
return;
}
}
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager)
{
// If the container changes, the item is no longer where it was
abortCondition = () => itemToDecontain.Container != sourceContainer.Item,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = sourceContainer.Item.Name
},
onAbandon: () => Abandon = true);
return;
}
}
if (targetContainer != null)
{
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
Equip = this.Equip,
RemoveEmpty = false,
GetItemPriority = this.GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
},
onCompleted: () => IsCompleted = true,
onAbandon: () =>
{
if (DropIfFailsToContain)
{
itemToDecontain.Drop(character);
}
Abandon = true;
});
}
else
{
itemToDecontain.Drop(character);
IsCompleted = true;
}
}
}
}
@@ -0,0 +1,146 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
public override string DebugTag => "extinguish fire";
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetHull = targetHull;
}
public override float GetPriority()
{
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check() => targetHull.FireSources.None();
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
{
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
});
}
else
{
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
if (extinguisher == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
#endif
Abandon = true;
return;
}
foreach (FireSource fs in targetHull.FireSources)
{
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
bool move = !inRange || !HumanAIController.VisibleHulls.Contains(fs.Hull);
if (inRange || useExtinquisherTimer > 0.0f)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f)
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (extinguisherItem.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
sinTime += deltaTime * 10;
}
character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true);
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f);
}
if (!character.CanSeeTarget(fs))
{
move = true;
}
}
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
else
{
character.AIController.SteeringManager.Reset();
}
break;
}
}
}
}
}
@@ -0,0 +1,39 @@
using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using System;
namespace Barotrauma
{
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : Targets.Sum(t => GetFireSeverity(t));
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override AIObjective ObjectiveConstructor(Hull target)
=> new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Hull target)
=> HumanAIController.RemoveTargets<AIObjectiveExtinguishFires, Hull>(character, target);
public static bool IsValidTarget(Hull hull, Character character)
{
if (hull == null) { return false; }
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (hull.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,47 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
{
public override string DebugTag => "fight intruders";
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
// TODO: sorting criteria
return 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == character) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (target.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,130 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private readonly string gearTag;
private readonly string fallbackTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
public static float lowOxygenThreshold = 10;
protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource");
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needDivingSuit ? "divingsuit" : "divingmask";
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true);
if (item == null && fallbackTag != gearTag)
{
item = character.Inventory.FindItemByTag(fallbackTag, true);
}
if (item == null || !character.HasEquippedItem(item))
{
TryAddSubObjective(ref getDivingGear, () =>
{
if (item == null)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false };
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getDivingGear));
}
else
{
var containedItems = item.ContainedItems;
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
{
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
if (oxygenTank != null)
{
var container = item.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(oxygenTank))
{
Abandon = true;
}
character.Inventory.RemoveItem(oxygenTank);
if (!container.Inventory.TryPutItem(oxygenTank, null))
{
oxygenTank.Drop(character);
Abandon = true;
}
}
else
{
container.Combine(oxygenTank, character);
}
}
else
{
// Seek oxygen that has min 10% condition left
TryAddSubObjective(ref getOxygen, () =>
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = lowOxygenThreshold
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = 0
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
}
}
}
}
}
}
@@ -0,0 +1,293 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveFindSafety : AIObjective
{
public override string DebugTag => "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
// TODO: expose?
const float priorityIncrease = 100;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
private float currenthullSafety;
private float searchHullTimer;
private AIObjectiveGoTo goToObjective;
private AIObjectiveFindDivingGear divingGearObjective;
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Check() => false;
public override bool CanBeCompleted => true;
private bool resetPriority;
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (resetPriority)
{
Priority = 0;
resetPriority = false;
return;
}
if (character.CurrentHull == null)
{
currenthullSafety = 0;
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
return;
}
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
{
Priority = 100;
}
currenthullSafety = HumanAIController.CurrentHullSafety;
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
}
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
}
}
private Hull currentSafeHull;
private Hull previousSafeHull;
protected override void Act(float deltaTime)
{
var currentHull = character.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
}
else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
}
if (searchHullTimer > 0.0f)
{
searchHullTimer -= deltaTime;
}
else
{
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull();
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
}
if (currentSafeHull != null && currentSafeHull != currentHull)
{
if (goToObjective?.Target != currentSafeHull)
{
RemoveSubObjective(ref goToObjective);
}
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
}
RemoveSubObjective(ref goToObjective);
// If diving gear objective failed, let's reset it here.
RemoveSubObjective(ref divingGearObjective);
},
onAbandon: () =>
{
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
{
if (goToObjective.Target is Hull hull)
{
HumanAIController.UnreachableHulls.Add(hull);
}
}
RemoveSubObjective(ref goToObjective);
});
}
else
{
RemoveSubObjective(ref goToObjective);
}
}
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
if (currentHull != null)
{
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
if (escapeVel != Vector2.Zero)
{
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
return;
}
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
{
Hull bestHull = null;
float bestValue = 0;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
if (character.CurrentHull != null && character.Submarine != null)
{
// Inside
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
// Don't allow to go outside if not already outside.
var path = character.CurrentHull != null ?
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) :
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable && character.CurrentHull != null)
{
HumanAIController.UnreachableHulls.Add(hull);
continue;
}
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
{
hullSafety /= 10;
}
}
else
{
// Outside
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
{
hullSafety = 100;
}
else
{
// TODO: could also target gaps that get us inside?
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull && item.HasTag("airlock"))
{
hullSafety = 100;
break;
}
}
}
// TODO: could we get a closest door to the outside and target the flowing hull if no airlock is found?
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
if (hullSafety > bestValue)
{
bestHull = hull;
bestValue = hullSafety;
}
}
return bestHull;
}
}
}
@@ -0,0 +1,148 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFixLeak : AIObjective
{
public override string DebugTag => "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public Gap Leak { get; private set; }
private AIObjectiveGetItem getWeldingTool;
private AIObjectiveContainItem refuelObjective;
private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective;
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier)
{
Leak = leak;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
public override float GetPriority()
{
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
}
else
{
var containedItems = weldingTool.ContainedItems;
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
if (subObjectives.Any()) { return; }
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
Abandon = true;
return;
}
Vector2 toLeak = Leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (Check()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName
},
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2)
{
// Too far
Abandon = true;
}
else
{
// We are close, try again.
RemoveSubObjective(ref gotoObjective);
}
},
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
}
}
}
@@ -0,0 +1,78 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
public static float GetLeakSeverity(Gap leak)
{
if (leak == null) { return 0; }
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, leak.Size));
float severity = sizeFactor * leak.Open;
if (!leak.IsRoomToRoom)
{
severity *= 10;
// If there is a leak in the outer walls, the severity cannot be lower than 10, no matter how small the leak
return MathHelper.Clamp(severity, 10, 100);
}
else
{
return MathHelper.Min(severity, 100);
}
}
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
{
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
else
{
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
}
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
}
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
=> new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
public static bool IsValidTarget(Gap gap, Character character)
{
if (gap == null) { return false; }
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null) { return false; }
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,296 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveGetItem : AIObjective
{
public override string DebugTag => "get item";
private readonly bool equip;
public HashSet<Item> ignoredItems = new HashSet<Item>();
public Func<Item, float> GetItemPriority;
public Func<Item, bool> ItemFilter;
public float TargetCondition { get; set; } = 1;
//can be either tags or identifiers
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
private Item targetItem, moveToTarget, rootContainer;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private bool checkInventory;
public bool AllowToFindDivingGear { get; set; } = true;
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
this.targetItem = targetItem;
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.checkInventory = checkInventory;
}
private bool CheckInventory()
{
if (itemIdentifiers == null) { return false; }
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (item != null)
{
targetItem = item;
rootContainer = item.GetRootContainer();
moveToTarget = rootContainer ?? item;
}
return item != null;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (itemIdentifiers != null && !isDoneSeeking)
{
if (checkInventory)
{
if (CheckInventory())
{
isDoneSeeking = true;
}
}
if (!isDoneSeeking)
{
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
}
}
if (targetItem == null || targetItem.Removed)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
return;
}
if (character.IsItemTakenBySomeoneElse(targetItem))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else.", Color.Yellow);
#endif
// Try again
Reset();
return;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
{
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target not pickable. Aborting.", Color.Yellow);
#endif
Abandon = true;
return;
}
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) &&
character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
{
targetItem.Equip(character);
IsCompleted = true;
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
Abandon = true;
}
}
else
{
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
{
IsCompleted = true;
}
else
{
Abandon = true;
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
}
}
}
else
{
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = moveToTarget.Name
};
},
onAbandon: () =>
{
ignoredItems.Add(targetItem);
Reset();
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
private void FindTargetItem()
{
if (itemIdentifiers == null)
{
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
#endif
Abandon = true;
}
return;
}
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
if (item.Submarine == null) { continue; }
if (item.CurrentHull == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
{
itemPriority = GetItemPriority(item);
}
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
//ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
this.rootContainer = rootContainer;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
{
isDoneSeeking = true;
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
protected override bool Check()
{
if (IsCompleted) { return true; }
if (targetItem != null)
{
return character.HasItem(targetItem, equip);
}
else if (itemIdentifiers != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
return !equip || character.HasEquippedItem(matchingItem);
}
return false;
}
return false;
}
private bool CheckItem(Item item)
{
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
}
public override void Reset()
{
base.Reset();
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
rootContainer = null;
isDoneSeeking = false;
currSearchIndex = 0;
}
}
}
@@ -0,0 +1,347 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
public override string DebugTag => "go to";
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
/// <summary>
/// Doesn't allow the objective to complete if this condition is false
/// </summary>
public Func<bool> requiredCondition;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public bool followControlledCharacter;
public bool mimic;
private float _closeEnough = 50;
/// <summary>
/// Display units
/// </summary>
public float CloseEnough
{
get { return _closeEnough; }
set
{
_closeEnough = Math.Max(_closeEnough, value);
}
}
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public string DialogueIdentifier { get; set; }
public string TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
public override float GetPriority()
{
if (followControlledCharacter && Character.Controlled == null)
{
Priority = 0;
}
if (Target is Entity e && e.Removed)
{
Priority = 0;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
{
Priority = 0;
}
return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base (character, objectiveManager, priorityModifier)
{
this.Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
CloseEnough = closeEnough;
if (Target is Item i)
{
CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
}
}
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true);
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
}
}
protected override void Act(float deltaTime)
{
if (followControlledCharacter)
{
if (Character.Controlled == null)
{
Abandon = true;
SteeringManager.Reset();
return;
}
Target = Character.Controlled;
}
if (Target == character)
{
// Wait
character.AIController.SteeringManager.Reset();
return;
}
waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
if (Target is Entity e)
{
if (e.Removed)
{
Abandon = true;
SteeringManager.Reset();
return;
}
else
{
character.AIController.SelectTarget(e.AiTarget);
}
}
Hull targetHull = GetTargetHull();
if (!followControlledCharacter)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
{
Abandon = true;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
{
Abandon = true;
}
else if (waitUntilPathUnreachable < 0)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
{
if (repeat)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
{
Abandon = true;
}
}
}
if (Abandon)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
{
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingSuit = targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit);
if (!needsDivingGear && mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
{
needsDivingGear = true;
}
}
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
OnCompleted();
return;
}
if (SteeringManager == PathSteering)
{
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = node => node.Waypoint.CurrentHull != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, n =>
{
if (n.Waypoint.isObstructed) { return false; }
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
}, endNodeFilter, nodeFilter);
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
private Hull GetTargetHull()
{
if (Target is Hull h)
{
return h;
}
else if (Target is Item i)
{
return i.CurrentHull;
}
else if (Target is Character c)
{
return c.CurrentHull;
}
else if (Target is Gap g)
{
return g.FlowTargetHull;
}
else if (Target is WayPoint wp)
{
return wp.CurrentHull;
}
else if (Target is FireSource fs)
{
return fs.Hull;
}
return null;
}
private bool IsCloseEnough
{
get
{
if (character.IsClimbing && SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished)
{
// Still in ladders and the path is not finished -> don't release
return false;
}
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (closeEnough)
{
closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull;
}
return closeEnough;
}
}
protected override bool Check()
{
if (IsCompleted) { return true; }
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
if (Target == null)
{
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition())
{
Abandon = true;
return false;
}
if (repeat)
{
return false;
}
else
{
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (Target is Item item)
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; }
}
else
{
IsCompleted = true;
}
}
}
}
return IsCompleted;
}
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
base.OnCompleted();
}
}
}
@@ -0,0 +1,269 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveIdle : AIObjective
{
public override string DebugTag => "idle";
public override bool UnequipItems => true;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
private readonly float standStillMin = 2;
private readonly float standStillMax = 10;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
private Hull currentTarget;
private float newTargetTimer;
private bool searchingNewHull;
private float standStillTimer;
private float walkDuration;
private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20);
public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
}
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void CalculatePriority()
{
Random = Rand.Range(0.5f, 1.5f);
randomTimer = randomUpdateInterval;
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
float initiative = character.GetSkillLevel("initiative");
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
}
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
{
if (randomTimer > 0)
{
randomTimer -= deltaTime;
}
else
{
CalculatePriority();
}
}
}
protected override void Act(float deltaTime)
{
if (PathSteering == null) { return; }
//don't keep dragging others when idling
if (character.SelectedCharacter != null)
{
character.DeselectCharacter();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
{
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
//standStillTimer = 0.0f;
}
else if (character.IsClimbing)
{
if (currentTarget == null)
{
newTargetTimer = 0;
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
// Don't allow new targets when climbing straight up or down
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
}
else if (targetHulls.Count > 0)
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
return;
}
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
searchingNewHull = false;
}
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
}
newTargetTimer -= deltaTime;
//wander randomly
// - if reached the end of the path
// - if the target is unreachable
// - if the path requires going outside
if (!character.IsClimbing)
{
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
Wander(deltaTime);
return;
}
}
if (currentTarget != null)
{
if (SteeringManager == PathSteering)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget));
}
}
else
{
Wander(deltaTime);
}
}
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
if (!character.AnimController.InWater)
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
PathSteering.Wander(deltaTime);
}
private void FindTargetHulls()
{
targetHulls.Clear();
hullWeights.Clear();
foreach (var hull in Hull.hullList)
{
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != character.TeamID) { continue; }
// If the character is inside, only take connected hulls into account.
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (IsForbidden(hull)) { continue; }
// Ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
{
continue;
}
}
if (!targetHulls.Contains(hull))
{
targetHulls.Add(hull);
float weight = hull.Volume;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
weight *= distanceFactor;
hullWeights.Add(weight);
}
}
}
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName;
if (hullName == null) { return false; }
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
abstract class AIObjectiveLoop<T> : AIObjective
{
public HashSet<T> Targets { get; private set; } = new HashSet<T>();
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
protected float targetUpdateTimer;
private float syncTimer;
private readonly float syncTime = 1;
// By default, doesn't clear the list automatically
protected virtual float IgnoreListClearInterval => 0;
public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
public bool AddTarget(T target)
{
if (character.IsDead) { return false; }
if (ReportedTargets.Contains(target))
{
return false;
}
if (Filter(target))
{
ReportedTargets.Add(target);
return true;
}
return false;
}
public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (IgnoreListClearInterval > 0)
{
if (ignoreListTimer > IgnoreListClearInterval)
{
Reset();
}
else
{
ignoreListTimer += deltaTime;
}
}
if (targetUpdateTimer < 0)
{
UpdateTargets();
}
else
{
targetUpdateTimer -= deltaTime;
}
if (syncTimer < 0)
{
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
{
var target = objective.Key;
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
}
else
{
syncTimer -= deltaTime;
}
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
{
CreateObjectives();
}
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1);
public override void Reset()
{
base.Reset();
ignoreList.Clear();
ignoreListTimer = 0;
UpdateTargets();
}
public override float GetPriority()
{
if (character.LockHands || character.Submarine == null || Targets.None())
{
Priority = 0;
}
else
{
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
{
targetValue = 100 - targetValue;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
{
Priority = 0;
}
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
}
return Priority;
}
protected void UpdateTargets()
{
SetTargetUpdateTimer();
Targets.Clear();
FindTargets();
CreateObjectives();
}
protected virtual void FindTargets()
{
foreach (T target in GetList())
{
// The bots always find targets when the objective is an order.
if (objectiveManager.CurrentOrder != this)
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
if (!ignoreList.Contains(target))
{
Targets.Add(target);
}
}
}
protected virtual void CreateObjectives()
{
foreach (T target in Targets)
{
if (ignoreList.Contains(target)) { continue; }
if (!Objectives.TryGetValue(target, out AIObjective objective))
{
objective = ObjectiveConstructor(target);
Objectives.Add(target, objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(target);
OnObjectiveCompleted(objective, target);
};
objective.Abandoned += () =>
{
Objectives.Remove(target);
ignoreList.Add(target);
targetUpdateTimer = 0;
};
}
}
}
protected abstract void OnObjectiveCompleted(AIObjective objective, T target);
/// <summary>
/// List of all possible items of the specified type. Used for filtering the removed objectives.
/// </summary>
protected abstract IEnumerable<T> GetList();
protected abstract float TargetEvaluation();
protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target);
}
}
@@ -0,0 +1,340 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class AIObjectiveManager
{
// TODO: expose
public const float OrderPriority = 70;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 3;
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
private readonly Character character;
private float _waitTimer;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat.
/// </summary>
public float WaitTimer
{
get { return _waitTimer; }
set
{
_waitTimer = IsAllowedToWait() ? value : 0;
}
}
public AIObjective CurrentOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public AIObjectiveManager(Character character)
{
this.character = character;
CreateAutonomousObjectives();
}
public void AddObjective<T>(T objective) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
#endif
return;
}
// Can't use the generic type, because it's possible that the user of this method uses the base type AIObjective.
// We need to get the highest type.
var type = objective.GetType();
if (objective.AllowMultipleInstances)
{
if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
{
Objectives.Remove(existingObjective);
}
}
else
{
Objectives.RemoveAll(o => o.GetType() == type);
}
Objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void CreateAutonomousObjectives()
{
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
DelayedObjectives.Clear();
Objectives.Clear();
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
int objectiveCount = Objectives.Count;
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
{
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
// TODO: Similar code is used in CrewManager:815-> DRY
var matchingItems = orderPrefab.ItemIdentifiers.Any() ?
Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType));
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
var item = matchingItems.GetRandom();
var order = new Order(
orderPrefab,
item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType),
orderGiver: character);
if (order == null) { continue; }
var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier);
if (objective != null)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
public void AddObjective<T>(T objective, float delay, Action callback = null) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
#endif
return;
}
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
{
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
}, delay);
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T;
private AIObjective GetCurrentObjective()
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
{
CurrentObjective = CurrentOrder;
}
else
{
CurrentObjective = firstObjective;
}
if (previousObjective != CurrentObjective)
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority();
}
return CurrentObjective;
}
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
}
public void UpdateObjectives(float deltaTime)
{
CurrentOrder?.Update(deltaTime);
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
return;
}
for (int i = 0; i < Objectives.Count; i++)
{
var objective = Objectives[i];
if (objective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen);
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
#endif
Objectives.Remove(objective);
}
else if (objective != CurrentOrder)
{
objective.Update(deltaTime);
}
}
GetCurrentObjective();
}
public void SortObjectives()
{
if (Objectives.Any())
{
Objectives.ForEach(o => o.GetPriority());
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
public void DoCurrentObjective(float deltaTime)
{
if (WaitTimer <= 0)
{
CurrentObjective?.TryComplete(deltaTime);
}
else
{
character.AIController.SteeringManager.Reset();
}
}
public void SetOrder(AIObjective objective)
{
CurrentOrder = objective;
}
public void SetOrder(Order order, string option, Character orderGiver)
{
CurrentOrder = CreateObjective(order, option, orderGiver);
if (CurrentOrder == null)
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else
{
CurrentOrder.Reset();
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
{
if (order == null) { return null; }
AIObjective newObjective;
switch (order.Identifier.ToLowerInvariant())
{
case "follow":
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
mimic = true,
DialogueIdentifier = "dialogcannotreachplace"
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.CurrentHull == null
};
break;
case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier);
break;
case "chargebatteries":
newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier);
break;
case "rescue":
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break;
case "repairsystems":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier)
{
RequireAdequateSkills = option == "jobspecific"
};
break;
case "pumpwater":
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
break;
case "extinguishfires":
newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier);
break;
case "fightintruders":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
default:
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
}
return newObjective;
}
private bool IsAllowedToWait()
{
if (CurrentOrder != null) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
if (character.IsClimbing) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (humanAI.UnsafeHulls.Contains(character.CurrentHull)) { return false; }
}
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
return true;
}
}
}
@@ -0,0 +1,224 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string DebugTag => "operate item";
public override bool UnequipItems => true;
private ItemComponent component, controller;
private Entity operateTarget;
private bool requireEquip;
private bool useController;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
public Entity OperateTarget => operateTarget;
public ItemComponent Component => component;
public ItemComponent GetTarget() => useController ? controller : component;
public Func<bool> completionCondition;
public override float GetPriority()
{
if (component.Item.ConditionPercentage <= 0)
{
Priority = 0;
}
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.None() || IsOperatedByAnother(GetTarget()))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
: base (character, objectiveManager, priorityModifier, option)
{
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
this.requireEquip = requireEquip;
this.operateTarget = operateTarget;
this.useController = useController;
if (useController)
{
//try finding the controller with the simpler non-recursive method first
controller =
component.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
component.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
}
private bool IsOperatedByAnother(ItemComponent target)
{
foreach (var c in Character.CharacterList)
{
if (c == character) { continue; }
if (!HumanAIController.IsFriendly(c)) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.AIController is HumanAIController humanAi)
{
// If the other character is ordered to operate the item, let him do it
if (humanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
return true;
}
else
{
if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
ItemComponent target = GetTarget();
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
Abandon = true;
return;
}
// Don't allow to operate an item that someone with a better skills already operates, unless this is an order
if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(target))
{
// Don't abandon
return;
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(target.Item);
if (character.SelectedConstruction != target.Item)
{
target.Item.TryInteract(character, false, true);
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
{
if (component.Item.GetComponent<Pickable>() == null)
{
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
Abandon = true;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
{
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getItemObjective));
}
else
{
if (requireEquip && !character.HasEquippedItem(component.Item))
{
//the item has to be equipped before using it if it's holdable
var holdable = component.Item.GetComponent<Holdable>();
if (holdable == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
#endif
return;
}
for (int i = 0; i < character.Inventory.Capacity; i++)
{
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || !holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
{
continue;
}
//equip slot already taken
if (character.Inventory.Items[i] != null)
{
//try to put the item in an Any slot, and drop it if that fails
if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) ||
!character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any }))
{
character.Inventory.Items[i].Drop(character);
}
}
if (character.Inventory.TryPutItem(component.Item, i, true, false, character))
{
component.Item.Equip(character);
break;
}
}
return;
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
}
}
}
}
protected override bool Check() => IsCompleted && !IsLoop;
}
}
@@ -0,0 +1,85 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool UnequipItems => true;
private IEnumerable<Pump> pumpList;
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option) { }
protected override void FindTargets()
{
if (Option == null) { return; }
base.FindTargets();
}
protected override bool Filter(Pump pump)
{
if (pump == null) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(pump)) { return false; }
return true;
}
protected override IEnumerable<Pump> GetList()
{
if (pumpList == null)
{
if (character == null || character.Submarine == null) { return new Pump[0]; }
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
}
return pumpList;
}
protected override float TargetEvaluation()
{
if (Option == "stoppumping")
{
return Targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100)));
}
else
{
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(-t.FlowPercentage / 100)));
}
}
private bool IsReady(Pump pump)
{
if (Option == "stoppumping")
{
return !pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0);
}
else
{
return !pump.Item.InWater || pump.IsActive && pump.FlowPercentage <= -99.9f;
}
}
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
{
IsLoop = false,
completionCondition = () => IsReady(pump)
};
protected override void OnObjectiveCompleted(AIObjective objective, Pump target)
=> HumanAIController.RemoveTargets<AIObjectivePumpWater, Pump>(character, target);
}
}
@@ -0,0 +1,248 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem refuelObjective;
private float previousCondition = -1;
private RepairTool repairTool;
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
Item = item;
}
public override float GetPriority()
{
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
{
Priority = 0;
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check()
{
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return IsCompleted;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
foreach (Repairable repairable in Item.Repairables)
{
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
}
}
return;
}
}
if (repairTool == null)
{
FindRepairTool();
}
if (repairTool != null)
{
var containedItems = repairTool.Item.ContainedItems;
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
{
item = requiredItem;
fuel = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (fuel != null) { break; }
}
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
OperateRepairTool(deltaTime);
}
foreach (Repairable repairable in Item.Repairables)
{
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
{
// Someone else is repairing the target. Abandon the objective if the other is better at this than us.
Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!Abandon)
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, true, true))
{
Abandon = true;
}
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
}
if (Abandon)
{
if (IsRepairing)
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
repairable.StopRepairing(character);
}
else if (repairable.CurrentFixer != character)
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
break;
}
}
else
{
RemoveSubObjective(ref refuelObjective);
// If cannot reach the item, approach it.
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
previousCondition = -1;
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
endNodeFilter = node => node.Waypoint.Ladders == null
};
if (repairTool != null)
{
objective.CloseEnough = repairTool.Range * 0.75f;
}
return objective;
},
onAbandon: () =>
{
Abandon = true;
if (IsRepairing)
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
});
}
}
private void FindRepairTool()
{
foreach (Repairable repairable in Item.Repairables)
{
foreach (var kvp in repairable.requiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
foreach (var item in character.Inventory.Items)
{
if (requiredItem.MatchesItem(item))
{
repairTool = item.GetComponent<RepairTool>();
}
}
}
}
}
}
private void OperateRepairTool(float deltaTime)
{
character.CursorPosition = Item.Position;
if (repairTool.Item.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
}
Vector2 fromToolToTarget = Item.Position - repairTool.Item.Position;
if (fromToolToTarget.LengthSquared() < MathUtils.Pow(repairTool.Range / 2, 2))
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - Item.SimPosition) / 2);
}
else
{
character.AIController.SteeringManager.Reset();
}
if (VectorExtensions.Angle(VectorExtensions.Forward(repairTool.Item.body.TransformedRotation), fromToolToTarget) < MathHelper.PiOver4)
{
repairTool.Use(deltaTime, character);
}
}
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "repair items";
/// <summary>
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
/// </summary>
public bool RequireAdequateSkills;
public override bool AllowMultipleInstances => true;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override void CreateObjectives()
{
foreach (var item in Targets)
{
foreach (Repairable repairable in item.Repairables)
{
if (!Objectives.TryGetValue(item, out AIObjective objective))
{
objective = ObjectiveConstructor(item);
Objectives.Add(item, objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(item);
OnObjectiveCompleted(objective, item);
};
objective.Abandoned += () =>
{
Objectives.Remove(item);
ignoreList.Add(item);
targetUpdateTimer = 0;
};
}
break;
}
}
}
protected override bool Filter(Item item)
{
if (!IsValidTarget(item, character)) { return false; }
if (item.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (!Objectives.ContainsKey(item))
{
if (item != character.SelectedConstruction)
{
float condition = item.ConditionPercentage;
if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; }
}
}
if (RequireAdequateSkills)
{
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
}
return true;
}
protected override float TargetEvaluation()
{
if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100))
{
// Don't stop fixing until done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
int items = Targets.Count;
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
else
{
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
}
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.Repairables.None()) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,303 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRescue : AIObjective
{
public override string DebugTag => "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 100.0f;
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
Abandon = true;
return;
}
this.targetCharacter = targetCharacter;
}
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return;
}
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
{
var otherCharacter = character.SelectedBy;
if (otherCharacter != null)
{
// Someone else is rescuing/holding the target.
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
}
}
if (targetCharacter != character)
{
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (character.SelectedCharacter != targetCharacter)
{
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);
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (safeHull == null)
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != safeHull)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
}
}
}
}
if (subObjectives.Any()) { return; }
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
// We can start applying treatment
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
{
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.SelectCharacter(targetCharacter);
}
GiveTreatment(deltaTime);
}
}
private readonly List<string> suitableItemIdentifiers = new List<string>();
private readonly List<string> itemNameList = new List<string>();
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController.SteeringManager.Reset();
}
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
return;
}
treatmentTimer = TreatmentDelay;
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
if (matchingItem == null) { continue; }
ApplyTreatment(affliction, matchingItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
}
}
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, 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; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
{
itemNameList.Add(itemPrefab.Name);
}
}
}
if (itemNameList.Count > 0)
{
string itemListStr = "";
if (itemNameList.Count == 1)
{
itemListStr = itemNameList[0];
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
{
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.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
}
if (character != targetCharacter)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
bool remove = false;
foreach (ItemComponent ic in item.Components)
{
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
if (ic.DeleteOnUse)
{
remove = true;
}
}
if (remove)
{
Entity.Spawner?.AddToRemoveQueue(item);
}
}
protected override bool Check()
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return false;
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
}
return isCompleted;
}
public override float GetPriority()
{
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Priority = 0;
}
else
{
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
{
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 100;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
{
return vitalityThreshold;
}
else
{
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
}
}
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRescueAll>(), onlyBots: true);
int targetCount = Targets.Count;
bool anyRescuers = otherRescuers > 0;
float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
}
else
{
float multiplier = 1;
if (anyRescuers)
{
float mySkill = character.GetSkillLevel("medical");
int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true);
if (targetCount / (float)betterRescuers <= 1)
{
// Enough rescuers
return 100;
}
else
{
bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor");
if (foundOtherMedics)
{
if (character.Info.Job.Prefab.Identifier != "medicaldoctor")
{
// Double the vitality factor -> less likely to take action
multiplier = 2;
}
}
}
}
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
}
}
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
return Math.Clamp(vitality, 0, 100);
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
{
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
}
else
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
return false;
}
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
}
}
@@ -0,0 +1,314 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
public enum OrderCategory
{
Emergency,
Movement,
Power,
Maintenance,
Operate,
Undefined
}
class Order
{
public static Dictionary<string, Order> Prefabs { get; private set; }
public static Dictionary<OrderCategory, Tuple<Sprite, Color>> OrderCategoryIcons { get; private set; }
public static List<Order> PrefabList { get; private set; }
public static Order GetPrefab(string identifier)
{
if (!Prefabs.TryGetValue(identifier, out Order order))
{
DebugConsole.ThrowError($"Cannot find an order with the identifier '{identifier}'!");
}
return order;
}
public Order Prefab
{
get;
private set;
}
public readonly string Name;
public readonly Sprite SymbolSprite;
public readonly Type ItemComponentType;
public readonly string[] ItemIdentifiers;
public readonly string Identifier;
private Color? color;
public Color Color
{
get
{
if (color.HasValue)
{
return color.Value;
}
else if (OrderCategoryIcons.TryGetValue(Category, out Tuple<Sprite, Color> sprite))
{
return sprite.Item2;
}
else
{
return Color.White;
}
}
private set
{
color = value;
}
}
//if true, the order is issued to all available characters
public bool TargetAllCharacters;
public readonly float FadeOutTime;
public Entity TargetEntity;
public ItemComponent TargetItemComponent;
public readonly bool UseController;
public Controller ConnectedController;
public Character OrderGiver;
public readonly OrderCategory Category;
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
public readonly Dictionary<string, Sprite> OptionSprites;
public readonly float Weight;
static Order()
{
Prefabs = new Dictionary<string, Order>();
OrderCategoryIcons = new Dictionary<OrderCategory, Tuple<Sprite, Color>>();
foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Orders))
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
bool allowOverriding = false;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = element.Name.ToString();
if (name.Equals("order", StringComparison.OrdinalIgnoreCase))
{
string identifier = element.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Error in file {file.Path}: The order element '{name}' does not have an identifier! All orders must have a unique identifier.");
continue;
}
if (Prefabs.TryGetValue(identifier, out Order duplicate))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file.Path}'", Color.Yellow);
Prefabs.Remove(identifier);
}
else
{
DebugConsole.ThrowError($"Error in file {file.Path}: Duplicate element with the idenfitier '{identifier}' found in '{file.Path}'! All orders must have a unique identifier. Use <override></override> tags to override an order with the same identifier.");
continue;
}
}
var newOrder = new Order(element);
newOrder.Prefab = newOrder;
Prefabs.Add(identifier, newOrder);
}
else if (name.Equals("ordercategory", StringComparison.OrdinalIgnoreCase))
{
var category = (OrderCategory)Enum.Parse(typeof(OrderCategory), element.GetAttributeString("category", "undefined"), true);
if (OrderCategoryIcons.ContainsKey(category))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing icon for the '{category}' order category with another one defined in '{file}'", Color.Yellow);
OrderCategoryIcons.Remove(category);
}
else
{
DebugConsole.ThrowError($"Error in file {file}: Duplicate element for the '{category}' order category found in '{file}'! All order categories must be unique. Use <override></override> tags to override an order category.");
continue;
}
}
var spriteElement = element.GetChildElement("sprite");
if (spriteElement != null)
{
var sprite = new Sprite(spriteElement, lazyLoad: true);
var color = element.GetAttributeColor("color", Color.White);
OrderCategoryIcons.Add(category, new Tuple<Sprite, Color>(sprite, color));
}
}
}
}
PrefabList = new List<Order>(Prefabs.Values);
}
/// <summary>
/// Constructor for order prefabs
/// </summary>
private Order(XElement orderElement)
{
Identifier = orderElement.GetAttributeString("identifier", "");
Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found";
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType))
{
try
{
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemType, true, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e);
}
}
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), orderElement.GetAttributeString("category", "undefined"), true);
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
{
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
}
else
{
string[] splitOptionNames = translatedOptionNames.Split(',', '');
OptionNames = new string[Options.Length];
for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++)
{
OptionNames[i] = splitOptionNames[i].Trim();
}
}
if (OptionNames.Length != Options.Length)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames = Options;
}
var spriteElement = orderElement.GetChildElement("sprite");
if (spriteElement != null)
{
SymbolSprite = new Sprite(spriteElement, lazyLoad: true);
}
OptionSprites = new Dictionary<string, Sprite>();
if (Options != null && Options.Length > 0)
{
var optionSpriteElements = orderElement.GetChildElement("optionsprites")?.GetChildElements("sprite");
if (optionSpriteElements != null && optionSpriteElements.Any())
{
for (int i = 0; i < Options.Length; i++)
{
if (i >= optionSpriteElements.Count()) { break; };
var sprite = new Sprite(optionSpriteElements.ElementAt(i), lazyLoad: true);
OptionSprites.Add(Options[i], sprite);
}
}
}
}
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
{
Prefab = prefab;
Name = prefab.Name;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
Weight = prefab.Weight;
Category = prefab.Category;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController)
{
//try finding the controller with the simpler non-recursive method first
ConnectedController =
targetItem.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetItem.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
}
public bool HasAppropriateJob(Character character)
{
if (character.Info == null || character.Info.Job == null) { return false; }
if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; }
if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
(AppropriateJobs == null || AppropriateJobs.Length == 0))
{
return true;
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "")
{
orderOption ??= "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (targetCharacterName == null) { targetCharacterName = ""; }
if (targetRoomName == null) { targetRoomName = ""; }
string msg = TextManager.GetWithVariables(messageTag, new string[2] { "[name]", "[roomname]" }, new string[2] { targetCharacterName, targetRoomName }, new bool[2] { false, true }, true);
if (msg == null) { return ""; }
return msg;
}
}
}
@@ -0,0 +1,443 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class PathNode
{
private WayPoint wayPoint;
private int wayPointID;
public int state;
public PathNode Parent;
private Vector2 position;
public float F,G,H;
public List<PathNode> connections;
public List<float> distances;
public WayPoint Waypoint
{
get { return wayPoint; }
}
public Vector2 Position
{
get { return position; }
}
public PathNode(WayPoint wayPoint)
{
this.wayPoint = wayPoint;
this.position = wayPoint.SimPosition;
wayPointID = wayPoint.ID;
connections = new List<PathNode>();
}
public static List<PathNode> GenerateNodes(List<WayPoint> wayPoints)
{
var nodes = new Dictionary<int, PathNode>();
foreach (WayPoint wayPoint in wayPoints)
{
if (wayPoint == null) continue;
if (nodes.ContainsKey(wayPoint.ID))
{
#if DEBUG
DebugConsole.ThrowError("Error in PathFinder.GenerateNodes (duplicate ID \"" + wayPoint.ID + "\")");
#endif
continue;
}
nodes.Add(wayPoint.ID, new PathNode(wayPoint));
}
foreach (KeyValuePair<int,PathNode> node in nodes)
{
foreach (MapEntity linked in node.Value.wayPoint.linkedTo)
{
PathNode connectedNode = null;
nodes.TryGetValue(linked.ID, out connectedNode);
if (connectedNode == null) continue;
node.Value.connections.Add(connectedNode);
}
}
var nodeList = nodes.Values.ToList();
nodeList.RemoveAll(n => n.connections.Count == 0);
foreach (PathNode node in nodeList)
{
node.distances = new List<float>();
for (int i = 0; i< node.connections.Count; i++)
{
node.distances.Add(Vector2.Distance(node.position, node.connections[i].position));
}
}
return nodeList;
}
}
class PathFinder
{
public delegate float? GetNodePenaltyHandler(PathNode node, PathNode prevNode);
public GetNodePenaltyHandler GetNodePenalty;
private List<PathNode> nodes;
public bool InsideSubmarine { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
{
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering));
foreach (WayPoint wp in wayPoints)
{
wp.linkedTo.CollectionChanged += WaypointLinksChanged;
}
InsideSubmarine = indoorsSteering;
}
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (Submarine.Unloading) return;
var waypoints = sender as IEnumerable<MapEntity>;
foreach (MapEntity me in waypoints)
{
WayPoint wp = me as WayPoint;
if (me == null) continue;
var node = nodes.Find(n => n.Waypoint == wp);
if (node == null) return;
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
for (int i = node.connections.Count - 1; i >= 0; i--)
{
//remove connection if the waypoint isn't connected anymore
if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null)
{
node.connections.RemoveAt(i);
node.distances.RemoveAt(i);
}
}
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
for (int i = 0; i < wp.linkedTo.Count; i++)
{
WayPoint connected = wp.linkedTo[i] as WayPoint;
if (connected == null) continue;
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) continue;
var matchingNode = nodes.Find(n => n.Waypoint == connected);
if (matchingNode == null)
{
#if DEBUG
DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder");
#endif
return;
}
node.connections.Add(matchingNode);
node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position));
}
}
}
}
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
float closestDist = 0.0f;
PathNode startNode = null;
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
}
float xDiff = Math.Abs(start.X - nodePos.X);
float yDiff = Math.Abs(start.Y - nodePos.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
{
yDiff += 10.0f;
}
float dist = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//prefer nodes that are closer to the end position
dist += (Math.Abs(end.X - nodePos.X) + Math.Abs(end.Y - nodePos.Y)) / 2.0f;
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine)
{
dist *= 10.0f;
}
if (dist < closestDist || startNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(
start, nodePos, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
}
}
closestDist = dist;
startNode = node;
}
}
if (startNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
closestDist = 0.0f;
PathNode endNode = null;
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
}
float dist = Vector2.DistanceSquared(end, nodePos);
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f;
}
if (dist < closestDist || endNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(end, nodePos, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
}
}
closestDist = dist;
endNode = node;
}
}
if (endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter);
return path;
}
public SteeringPath FindPath(WayPoint start, WayPoint end)
{
PathNode startNode=null, endNode=null;
foreach (PathNode node in nodes)
{
if (node.Waypoint == start)
{
startNode = node;
if (endNode != null) break;
}
if (node.Waypoint == end)
{
endNode = node;
if (startNode != null) break;
}
}
if (startNode == null || endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
#endif
return new SteeringPath(true);
}
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null)
{
if (start == end)
{
var path1 = new SteeringPath();
path1.AddNode(start.Waypoint);
return path1;
}
foreach (PathNode node in nodes)
{
node.Parent = null;
node.state = 0;
node.F = 0.0f;
node.G = 0.0f;
node.H = 0.0f;
}
start.state = 1;
while (true)
{
PathNode currNode = null;
float dist = float.MaxValue;
foreach (PathNode node in nodes)
{
if (filter != null && !filter(node)) { continue; }
if (node.state != 1) { continue; }
if (node.F < dist)
{
dist = node.F;
currNode = node;
}
}
if (currNode == null || currNode == end) { break; }
currNode.state = 2;
for (int i = 0; i < currNode.connections.Count; i++)
{
PathNode nextNode = currNode.connections[i];
//a node that hasn't been searched yet
if (nextNode.state == 0)
{
nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
float penalty = 0.0f;
if (GetNodePenalty != null)
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null)
{
nextNode.state = -1;
continue;
}
penalty = nodePenalty.Value;
}
nextNode.G = currNode.G + currNode.distances[i] + penalty;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
//node that has been searched
else if (nextNode.state == 1 || nextNode.state == -1)
{
float tempG = currNode.G + currNode.distances[i];
if (GetNodePenalty != null)
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null) { continue; }
tempG += nodePenalty.Value;
}
//only use if this new route is better than the
//route the node was a part of
if (tempG < nextNode.G)
{
nextNode.G = tempG;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
}
}
}
if (end.state == 0 || end.Parent == null)
{
#if DEBUG
DebugConsole.NewMessage("Path not found", Color.Yellow);
#endif
return new SteeringPath(true);
}
SteeringPath path = new SteeringPath();
List<WayPoint> finalPath = new List<WayPoint>();
PathNode pathNode = end;
while (pathNode != start && pathNode != null)
{
finalPath.Add(pathNode.Waypoint);
//(there was one bug report that seems to have been caused by this loop never terminating:
//couldn't reproduce or figure out what caused it, but here's a workaround that prevents the game from crashing in case it happens again)
//should be fixed now, was most likely caused by the parent fields of the nodes not being cleared before starting the pathfinding
if (finalPath.Count > nodes.Count)
{
#if DEBUG
DebugConsole.ThrowError("Pathfinding error: constructing final path failed");
#endif
return new SteeringPath(true);
}
path.Cost += pathNode.F;
pathNode = pathNode.Parent;
}
finalPath.Add(start.Waypoint);
finalPath.Reverse();
foreach (WayPoint wayPoint in finalPath)
{
path.AddNode(wayPoint);
}
return path;
}
}
}
@@ -0,0 +1,181 @@
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using FarseerPhysics;
using Barotrauma.Extensions;
namespace Barotrauma
{
class SteeringManager
{
protected const float CircleDistance = 2.5f;
protected const float CircleRadius = 0.3f;
protected const float RayCastInterval = 0.5f;
protected ISteerable host;
protected Vector2 steering;
private float lastRayCastTime;
private bool avoidRayCastHit;
public Vector2 AvoidDir { get; private set; }
public Vector2 AvoidRayCastHitPosition { get; private set; }
public Vector2 AvoidLookAheadPos { get; private set; }
private float wanderAngle;
public float WanderAngle
{
get { return wanderAngle; }
set { wanderAngle = value; }
}
public SteeringManager(ISteerable host)
{
this.host = host;
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
}
public void SteeringSeek(Vector2 targetSimPos, float weight = 1)
{
steering += DoSteeringSeek(targetSimPos, weight);
}
public void SteeringWander(float weight = 1)
{
steering += DoSteeringWander(weight);
}
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
{
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight);
}
public void SteeringManual(float deltaTime, Vector2 velocity)
{
steering += velocity;
}
public void Reset()
{
steering = Vector2.Zero;
}
public void ResetX()
{
steering.X = 0.0f;
}
public void ResetY()
{
steering.Y = 0.0f;
}
public virtual void Update(float speed)
{
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
{
steering = Vector2.Zero;
host.Steering = Vector2.Zero;
return;
}
if (steering.LengthSquared() > speed * speed)
{
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
host.Steering = steering;
}
protected virtual Vector2 DoSteeringSeek(Vector2 target, float weight)
{
Vector2 targetVel = target - host.SimPosition;
if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero;
targetVel = Vector2.Normalize(targetVel) * weight;
Vector2 newSteering = targetVel - host.Steering;
if (newSteering == Vector2.Zero) return Vector2.Zero;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > Math.Abs(weight))
{
newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
}
return newSteering;
}
protected virtual Vector2 DoSteeringWander(float weight)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Vector2.UnitY : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2(
(float)Math.Cos(wanderAngle),
(float)Math.Sin(wanderAngle));
displacement = displacement * CircleRadius;
float angleChange = 1.5f;
wanderAngle += Rand.Range(0.0f, 1.0f) * angleChange - angleChange * 0.5f;
Vector2 newSteering = circleCenter + displacement;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > weight)
{
newSteering = Vector2.Normalize(newSteering) * weight;
}
return newSteering;
}
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float weight, Vector2? heading = null)
{
if (steering == Vector2.Zero || host.Steering == Vector2.Zero)
{
return Vector2.Zero;
}
float maxDistance = lookAheadDistance;
if (Timing.TotalTime >= lastRayCastTime + RayCastInterval)
{
avoidRayCastHit = false;
AvoidLookAheadPos = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
lastRayCastTime = (float)Timing.TotalTime;
Body closestBody = Submarine.CheckVisibility(host.SimPosition, AvoidLookAheadPos);
if (closestBody != null)
{
avoidRayCastHit = true;
AvoidRayCastHitPosition = Submarine.LastPickedPosition;
AvoidDir = Submarine.LastPickedNormal;
//add a bit of randomness
AvoidDir = MathUtils.RotatePoint(AvoidDir, Rand.Range(-0.15f, 0.15f));
//wait a bit longer for the next raycast
lastRayCastTime += RayCastInterval;
}
}
if (AvoidDir.LengthSquared() < 0.0001f) { return Vector2.Zero; }
//if raycast hit nothing, lerp avoid dir to zero
if (!avoidRayCastHit)
{
AvoidDir -= Vector2.Normalize(AvoidDir) * deltaTime * 0.5f;
}
Vector2 diff = AvoidRayCastHitPosition - host.SimPosition;
float dist = diff.Length();
//> 0 when heading in the same direction as the obstacle, < 0 when away from it
float dot = MathHelper.Clamp(Vector2.Dot(diff / dist, host.Steering), 0.0f, 1.0f);
if (dot < 0) { return Vector2.Zero; }
return AvoidDir * dot * weight * MathHelper.Clamp(1.0f - dist / lookAheadDistance, 0.0f, 1.0f);
}
}
}
@@ -0,0 +1,104 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
class SteeringPath
{
private List<WayPoint> nodes;
int currentIndex;
public bool Unreachable
{
get;
set;
}
public SteeringPath(bool unreachable = false)
{
nodes = new List<WayPoint>();
Unreachable = unreachable;
}
public void AddNode(WayPoint node)
{
if (node == null) return;
nodes.Add(node);
if (node.CurrentHull == null) HasOutdoorsNodes = true;
}
public bool HasOutdoorsNodes
{
get;
private set;
}
public int CurrentIndex
{
get { return currentIndex; }
}
public float Cost
{
get;
set;
}
public WayPoint PrevNode
{
get
{
if (currentIndex-1 < 0 || currentIndex-1 > nodes.Count - 1) return null;
return nodes[currentIndex-1];
}
}
public WayPoint CurrentNode
{
get
{
if (currentIndex < 0 || currentIndex > nodes.Count - 1) return null;
return nodes[currentIndex];
}
}
public List<WayPoint> Nodes
{
get { return nodes; }
}
public WayPoint NextNode
{
get
{
if (currentIndex+1 < 0 || currentIndex+1 > nodes.Count - 1) return null;
return nodes[currentIndex+1];
}
}
public bool Finished
{
get { return currentIndex >= nodes.Count; }
}
public void SkipToNextNode()
{
currentIndex++;
}
public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f)
{
if (nodes.Count == 0 || currentIndex>nodes.Count-1) return null;
if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) currentIndex++;
return CurrentNode;
}
public void ClearPath()
{
nodes.Clear();
}
}
}
@@ -0,0 +1,121 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma
{
class SwarmBehavior
{
private readonly float minDistFromClosest;
private readonly float maxDistFromCenter;
private readonly float cohesion;
public List<AICharacter> Members { get; private set; } = new List<AICharacter>();
public HashSet<AICharacter> ActiveMembers { get; private set; } = new HashSet<AICharacter>();
private EnemyAIController ai;
public bool IsActive { get; set; }
public bool IsEnoughMembers => ActiveMembers.Count > 1;
public SwarmBehavior(XElement element, EnemyAIController ai)
{
this.ai = ai;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f));
cohesion = element.GetAttributeFloat("cohesion", 1) / 10;
}
public static void CreateSwarm(IEnumerable<AICharacter> swarm)
{
var aiControllers = new List<EnemyAIController>();
foreach (AICharacter character in swarm)
{
if (character.AIController is EnemyAIController enemyAI && enemyAI.SwarmBehavior != null)
{
aiControllers.Add(enemyAI);
}
}
var filteredMembers = aiControllers.Select(m => m.Character as AICharacter).Where(m => m != null);
foreach (EnemyAIController ai in aiControllers)
{
ai.SwarmBehavior.Members = filteredMembers.ToList();
}
}
public void Refresh()
{
Members.RemoveAll(m => m.IsDead || m.Removed || m.AIController is EnemyAIController ai && ai.State == AIState.Flee);
foreach (var member in Members)
{
if (!member.AIController.Enabled && member.IsRemotePlayer || Character.Controlled == member || !((EnemyAIController)member.AIController).SwarmBehavior.IsActive)
{
ActiveMembers.Remove(member);
}
else
{
ActiveMembers.Add(member);
}
}
}
public void UpdateSteering(float deltaTime)
{
if (!IsActive) { return; }
if (!IsEnoughMembers) { return; }
//calculate the "center of mass" of the swarm and the distance to the closest character in the swarm
float closestDistSqr = float.MaxValue;
Vector2 center = Vector2.Zero;
AICharacter closest = null;
foreach (AICharacter member in Members)
{
center += member.SimPosition;
if (member == ai.Character) { continue; }
float distSqr = Vector2.DistanceSquared(member.SimPosition, ai.Character.SimPosition);
if (distSqr < closestDistSqr)
{
closestDistSqr = distSqr;
closest = member;
}
}
center /= Members.Count;
if (closest == null) { return; }
//steer away from the closest if too close
float closestDist = (float)Math.Sqrt(closestDistSqr);
if (closestDist < minDistFromClosest)
{
Vector2 diff = closest.SimPosition - ai.SimPosition;
if (diff.LengthSquared() < 0.0001f)
{
diff = Vector2.UnitX;
}
ai.SteeringManager.SteeringManual(deltaTime, -diff);
}
//steer closer to the center of mass if too far
else if (Vector2.DistanceSquared(center, ai.SimPosition) > maxDistFromCenter * maxDistFromCenter)
{
float distFromCenter = Vector2.Distance(center, ai.SimPosition);
ai.SteeringManager.SteeringSeek(center, (distFromCenter - maxDistFromCenter) / 10.0f);
}
//keep the characters moving in roughly the same direction
if (cohesion > 0.0f)
{
Vector2 avgVel = Vector2.Zero;
foreach (AICharacter member in Members)
{
avgVel += member.AnimController.TargetMovement;
}
avgVel /= Members.Count;
ai.SteeringManager.SteeringManual(deltaTime, avgVel * cohesion);
}
}
}
}
@@ -0,0 +1,129 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
partial class AICharacter : Character
{
//characters that are further than this from the camera (and all clients)
//have all their limb physics bodies disabled
const float EnableSimplePhysicsDist = 6000.0f;
const float DisableSimplePhysicsDist = EnableSimplePhysicsDist * 0.9f;
const float EnableSimplePhysicsDistSqr = EnableSimplePhysicsDist * EnableSimplePhysicsDist;
const float DisableSimplePhysicsDistSqr = DisableSimplePhysicsDist * DisableSimplePhysicsDist;
private AIController aiController;
public override AIController AIController
{
get { return aiController; }
}
public AICharacter(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(speciesName, position, seed, characterInfo, isNetworkPlayer, ragdoll)
{
InitProjSpecific();
}
partial void InitProjSpecific();
public void SetAI(AIController aiController)
{
if (AIController != null)
{
OnAttacked -= AIController.OnAttacked;
}
this.aiController = aiController;
if (aiController != null)
{
OnAttacked += aiController.OnAttacked;
}
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
if (!Enabled) return;
if (!IsRemotePlayer)
{
float characterDist = float.MaxValue;
#if CLIENT
characterDist = Vector2.DistanceSquared(cam.GetPosition(), WorldPosition);
#elif SERVER
if (GameMain.Server != null)
{
characterDist = GetClosestDistance();
}
#endif
if (characterDist > EnableSimplePhysicsDistSqr)
{
AnimController.SimplePhysicsEnabled = true;
}
else if (characterDist < DisableSimplePhysicsDistSqr)
{
AnimController.SimplePhysicsEnabled = false;
}
}
if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return;
if (!aiController.Enabled) return;
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return;
if (Controlled == this) return;
if (!IsRemotePlayer)
{
aiController.Update(deltaTime);
}
}
#if SERVER
// Gets the closest distance, either an active player character or spectator
private float GetClosestDistance()
{
float minDist = float.MaxValue;
for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++)
{
var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos;
if (spectatePos != null)
{
float dist = Vector2.DistanceSquared(spectatePos.Value, WorldPosition);
if (dist < minDist)
{
minDist = dist;
}
if (dist < DisableSimplePhysicsDistSqr)
{
return dist;
}
}
}
foreach (Character c in CharacterList)
{
if (c != this && c.IsRemotePlayer)
{
float dist = Vector2.DistanceSquared(c.WorldPosition, WorldPosition);
if (dist < minDist)
{
minDist = dist;
}
if (dist < DisableSimplePhysicsDistSqr)
{
return dist;
}
}
}
return minDist;
}
#endif
}
}
@@ -0,0 +1,31 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
class AIChatMessage
{
public readonly string Message;
/// <summary>
/// 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 ChatMessageType? MessageType;
public float SendDelay;
public double SendTime;
public AIChatMessage(string message, ChatMessageType? type, string identifier = "", float delay = 0.0f)
{
Message = message;
MessageType = type;
Identifier = identifier;
SendDelay = delay;
}
}
}
@@ -0,0 +1,224 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System;
namespace Barotrauma
{
abstract class AnimController : Ragdoll
{
public abstract GroundedMovementParams WalkParams { get; set; }
public abstract GroundedMovementParams RunParams { get; set; }
public abstract SwimParams SwimSlowParams { get; set; }
public abstract SwimParams SwimFastParams { get; set; }
public AnimationParams CurrentAnimationParams
{
get
{
if (ForceSelectAnimationType == AnimationType.NotDefined)
{
return (InWater || !CanWalk) ? (AnimationParams)CurrentSwimParams : CurrentGroundedParams;
}
else
{
return GetAnimationParamsFromType(ForceSelectAnimationType);
}
}
}
public AnimationType ForceSelectAnimationType { get; set; }
public GroundedMovementParams CurrentGroundedParams
{
get
{
if (ForceSelectAnimationType != AnimationType.NotDefined)
{
return GetAnimationParamsFromType(ForceSelectAnimationType) as GroundedMovementParams;
}
if (!CanWalk)
{
//DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!");
return null;
}
else
{
return IsMovingFast ? RunParams : WalkParams;
}
}
}
public SwimParams CurrentSwimParams
{
get
{
if (ForceSelectAnimationType != AnimationType.NotDefined)
{
return GetAnimationParamsFromType(ForceSelectAnimationType) as SwimParams;
}
else
{
return IsMovingFast? SwimFastParams : SwimSlowParams;
}
}
}
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
// TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f;
/// <summary>
/// Note: Presupposes that the slow speed is lower than the high speed. Otherwise will give invalid results.
/// </summary>
public bool IsMovingFast
{
get
{
if (InWater || !CanWalk)
{
return TargetMovement.Length() > (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
}
else
{
return Math.Abs(TargetMovement.X) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f;
}
}
}
/// <summary>
/// Note: creates a new list every time, because the params might have changed. If there is a need to access the property frequently, change the implementation to an array, where the slot is updated when the param is updated(?)
/// Currently it's not simple to implement, since the properties are not implemented here, but in the derived classes. Would require to change the params virtual and to call the base property getter/setter or something.
/// </summary>
public List<AnimationParams> AllAnimParams
{
get
{
if (CanWalk)
{
return new List<AnimationParams> { WalkParams, RunParams, SwimSlowParams, SwimFastParams };
}
else
{
return new List<AnimationParams> { SwimSlowParams, SwimFastParams };
}
}
}
public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR };
public Animation Anim;
public Vector2 AimSourcePos => ConvertUnits.ToDisplayUnits(AimSourceSimPos);
public virtual Vector2 AimSourceSimPos => Collider.SimPosition;
protected float? GetValidOrNull(AnimationParams p, float? v)
{
if (p == null) { return null; }
if (v == null) { return null; }
if (!MathUtils.IsValid(v.Value)) { return null; }
return v.Value;
}
protected Vector2? GetValidOrNull(AnimationParams p, Vector2 v)
{
if (p == null) { return null; }
return v;
}
public override float? HeadPosition => GetValidOrNull(CurrentGroundedParams, CurrentGroundedParams?.HeadPosition * RagdollParams.JointScale);
public override float? TorsoPosition => GetValidOrNull(CurrentGroundedParams, CurrentGroundedParams?.TorsoPosition * RagdollParams.JointScale);
public override float? HeadAngle => GetValidOrNull(CurrentAnimationParams, CurrentAnimationParams?.HeadAngleInRadians);
public override float? TorsoAngle => GetValidOrNull(CurrentAnimationParams, CurrentAnimationParams?.TorsoAngleInRadians);
public virtual Vector2? StepSize => GetValidOrNull(CurrentGroundedParams, CurrentGroundedParams.StepSize * RagdollParams.JointScale);
public bool AnimationTestPose { get; set; }
public float WalkPos { get; protected set; }
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public virtual void UpdateAnim(float deltaTime) { }
public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) { }
public virtual void DragCharacter(Character target, float deltaTime) { }
public virtual void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { }
public float GetSpeed(AnimationType type)
{
GroundedMovementParams movementParams;
switch (type)
{
case AnimationType.Walk:
if (!CanWalk)
{
DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!");
return 0;
}
movementParams = WalkParams;
break;
case AnimationType.Run:
if (!CanWalk)
{
DebugConsole.ThrowError($"{character.SpeciesName} cannot run!");
return 0;
}
movementParams = RunParams;
break;
case AnimationType.SwimSlow:
return SwimSlowParams.MovementSpeed;
case AnimationType.SwimFast:
return SwimFastParams.MovementSpeed;
default:
throw new NotImplementedException(type.ToString());
}
return IsMovingBackwards ? movementParams.MovementSpeed * movementParams.BackwardsMovementMultiplier : movementParams.MovementSpeed;
}
public float GetCurrentSpeed(bool useMaxSpeed)
{
AnimationType animType;
if (InWater || !CanWalk)
{
if (useMaxSpeed)
{
animType = AnimationType.SwimFast;
}
else
{
animType = AnimationType.SwimSlow;
}
}
else
{
if (useMaxSpeed)
{
animType = AnimationType.Run;
}
else
{
animType = AnimationType.Walk;
}
}
return GetSpeed(animType);
}
public AnimationParams GetAnimationParamsFromType(AnimationType type)
{
switch (type)
{
case AnimationType.Walk:
return WalkParams;
case AnimationType.Run:
return RunParams;
case AnimationType.SwimSlow:
return SwimSlowParams;
case AnimationType.SwimFast:
return SwimFastParams;
case AnimationType.NotDefined:
return null;
default:
throw new NotImplementedException(type.ToString());
}
}
}
}
@@ -0,0 +1,858 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class FishAnimController : AnimController
{
public override RagdollParams RagdollParams
{
get { return FishRagdollParams; }
protected set { FishRagdollParams = value as FishRagdollParams; }
}
private FishRagdollParams _ragdollParams;
public FishRagdollParams FishRagdollParams
{
get
{
if (_ragdollParams == null)
{
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
}
return _ragdollParams;
}
protected set
{
_ragdollParams = value;
}
}
private FishWalkParams _fishWalkParams;
public FishWalkParams FishWalkParams
{
get
{
if (_fishWalkParams == null)
{
_fishWalkParams = FishWalkParams.GetDefaultAnimParams(character);
}
return _fishWalkParams;
}
set { _fishWalkParams = value; }
}
private FishRunParams _fishRunParams;
public FishRunParams FishRunParams
{
get
{
if (_fishRunParams == null)
{
_fishRunParams = FishRunParams.GetDefaultAnimParams(character);
}
return _fishRunParams;
}
set { _fishRunParams = value; }
}
private FishSwimSlowParams _fishSwimSlowParams;
public FishSwimSlowParams FishSwimSlowParams
{
get
{
if (_fishSwimSlowParams == null)
{
_fishSwimSlowParams = FishSwimSlowParams.GetDefaultAnimParams(character);
}
return _fishSwimSlowParams;
}
set { _fishSwimSlowParams = value; }
}
private FishSwimFastParams _fishSwimFastParams;
public FishSwimFastParams FishSwimFastParams
{
get
{
if (_fishSwimFastParams == null)
{
_fishSwimFastParams = FishSwimFastParams.GetDefaultAnimParams(character);
}
return _fishSwimFastParams;
}
set { _fishSwimFastParams = value; }
}
public IFishAnimation CurrentFishAnimation => CurrentAnimationParams as IFishAnimation;
public new FishGroundedParams CurrentGroundedParams => base.CurrentGroundedParams as FishGroundedParams;
public new FishSwimParams CurrentSwimParams => base.CurrentSwimParams as FishSwimParams;
public float? TailAngle => GetValidOrNull(CurrentAnimationParams, CurrentFishAnimation?.TailAngleInRadians);
public float FootTorque => CurrentFishAnimation.FootTorque;
public float HeadTorque => CurrentFishAnimation.HeadTorque;
public float TorsoTorque => CurrentFishAnimation.TorsoTorque;
public float TailTorque => CurrentFishAnimation.TailTorque;
public float HeadMoveForce => CurrentGroundedParams.HeadMoveForce;
public float TorsoMoveForce => CurrentGroundedParams.TorsoMoveForce;
public float FootMoveForce => CurrentGroundedParams.FootMoveForce;
public override GroundedMovementParams WalkParams
{
get { return FishWalkParams; }
set { FishWalkParams = value as FishWalkParams; }
}
public override GroundedMovementParams RunParams
{
get { return FishRunParams; }
set { FishRunParams = value as FishRunParams; }
}
public override SwimParams SwimSlowParams
{
get { return FishSwimSlowParams; }
set { FishSwimSlowParams = value as FishSwimSlowParams; }
}
public override SwimParams SwimFastParams
{
get { return FishSwimFastParams; }
set { FishSwimFastParams = value as FishSwimFastParams; }
}
private float flipTimer, flipCooldown;
public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public override void UpdateAnim(float deltaTime)
{
if (Frozen) return;
if (MainLimb == null) { return; }
var mainLimb = MainLimb;
levitatingCollider = true;
if (!character.CanMove)
{
levitatingCollider = false;
Collider.FarseerBody.FixedRotation = false;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.Enabled = false;
Collider.LinearVelocity = mainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
}
if (character.IsDead && deathAnimTimer < deathAnimDuration)
{
deathAnimTimer += deltaTime;
UpdateDying(deltaTime);
}
else if (!InWater && !CanWalk && character.AllowInput)
{
//cannot walk but on dry land -> wiggle around
UpdateDying(deltaTime);
}
return;
}
else
{
deathAnimTimer = 0.0f;
}
//re-enable collider
if (!Collider.Enabled)
{
var lowestLimb = FindLowestLimb();
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
0.0f);
Collider.Enabled = true;
}
ResetPullJoints();
if (strongestImpact > 0.0f)
{
character.Stun = MathHelper.Clamp(strongestImpact * 0.5f, character.Stun, 5.0f);
strongestImpact = 0.0f;
}
if (inWater && !forceStanding)
{
Collider.FarseerBody.FixedRotation = false;
UpdateSineAnim(deltaTime);
}
else if (RagdollParams.CanWalk && (currentHull != null || forceStanding))
{
if (CurrentGroundedParams != null)
{
//rotate collider back upright
float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians;
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f)
{
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f;
Collider.FarseerBody.FixedRotation = false;
}
else
{
Collider.FarseerBody.FixedRotation = true;
}
}
UpdateWalkAnim(deltaTime);
}
//don't flip or drag when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
{
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
if (targetMovement.X > 0.1f && targetMovement.X > Math.Abs(targetMovement.Y) * 0.2f)
{
TargetDir = Direction.Right;
}
else if (targetMovement.X < -0.1f && targetMovement.X < -Math.Abs(targetMovement.Y) * 0.2f)
{
TargetDir = Direction.Left;
}
}
else
{
Limb refLimb = GetLimb(LimbType.Head);
float refAngle;
if (refLimb == null)
{
refAngle = CurrentAnimationParams.TorsoAngleInRadians;
refLimb = GetLimb(LimbType.Torso);
}
else
{
refAngle = CurrentAnimationParams.HeadAngleInRadians;
}
float rotation = refLimb.Rotation;
if (!float.IsNaN(refAngle)) { rotation -= refAngle * Dir; }
rotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(rotation));
if (rotation < 0.0f) rotation += 360;
if (rotation > 20 && rotation < 160)
{
TargetDir = Direction.Left;
}
else if (rotation > 200 && rotation < 340)
{
TargetDir = Direction.Right;
}
}
}
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
{
flipTimer += deltaTime;
if ((flipTimer > 0.5f && flipCooldown <= 0.0f) || character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror();
}
flipTimer = 0.0f;
flipCooldown = 1.0f;
}
}
else
{
flipTimer = 0.0f;
}
}
private bool CanDrag(Character target)
{
return Mass / target.Mass > 0.1f;
}
private float eatTimer = 0.0f;
public override void DragCharacter(Character target, float deltaTime)
{
if (target == null) { return; }
Limb mouthLimb = GetLimb(LimbType.Head);
if (mouthLimb == null) { return; }
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
//stop dragging if there's something between the pull limb and the target
Vector2 sourceSimPos = mouthLimb.SimPosition;
Vector2 targetSimPos = target.SimPosition;
if (character.Submarine != null && character.SelectedCharacter.Submarine == null)
{
targetSimPos -= character.Submarine.SimPosition;
}
else if (character.Submarine == null && character.SelectedCharacter.Submarine != null)
{
sourceSimPos -= character.SelectedCharacter.Submarine.SimPosition;
}
var body = Submarine.CheckVisibility(sourceSimPos, targetSimPos, ignoreSubs: true);
if (body != null)
{
character.DeselectCharacter();
return;
}
}
float dmg = character.Params.EatingSpeed;
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
eatTimer += deltaTime * eatSpeed;
Vector2 mouthPos = GetMouthPosition().Value;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
if (limbDiff.LengthSquared() < extent * extent)
{
//pull the target character to the position of the mouth
//(+ make the force fluctuate to waggle the character a bit)
float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40);
if (dragForce > 0.1f)
{
target.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
target.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, dragForce * 2);
target.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
}
//pull the character's mouth to the target character (again with a fluctuating force)
float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f));
mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
if (Rand.Value() < particleFrequency / 6)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false);
}
if (Rand.Value() < particleFrequency)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false);
}
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
{
bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered;
//keep severing joints until there is only one limb left
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
{
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
character.SelectedCharacter = null;
}
else //sever a random joint
{
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
}
}
}
else
{
character.SelectedCharacter = null;
}
}
public bool reverse;
void UpdateSineAnim(float deltaTime)
{
if (CurrentSwimParams == null) { return; }
movement = TargetMovement;
if (movement.LengthSquared() > 0.00001f)
{
float t = 0.5f;
if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2)
{
// Reduce the linear movement speed when not facing the movement direction
t /= 5;
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
var mainLimb = MainLimb;
mainLimb.PullJointEnabled = true;
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (movement.LengthSquared() < 0.00001f)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
return;
}
Vector2 transformedMovement = reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
mainLimbAngle = TorsoAngle.Value;
}
else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
mainLimbAngle = HeadAngle.Value;
}
mainLimbAngle *= Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
}
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
movementAngle -= MathHelper.TwoPi;
}
if (CurrentSwimParams.RotateTowardsMovement)
{
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
}
if (TailAngle.HasValue)
{
Limb tail = GetLimb(LimbType.Tail);
if (tail != null)
{
float? mainLimbTargetAngle = null;
if (mainLimb.type == LimbType.Torso)
{
mainLimbTargetAngle = TorsoAngle;
}
else if (mainLimb.type == LimbType.Head)
{
mainLimbTargetAngle = HeadAngle;
}
float torque = TailTorque;
float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier;
if (mainLimbTargetAngle.HasValue && maxMultiplier > 1)
{
float diff = Math.Abs(mainLimb.Rotation - tail.Rotation);
float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value);
torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset));
}
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque);
}
}
}
else
{
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
if (reverse)
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
}
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque);
}
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque);
}
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque);
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque);
}
if (TailAngle.HasValue)
{
Limb tail = GetLimb(LimbType.Tail);
tail?.body.SmoothRotate(TailAngle.Value * Dir, TailTorque);
}
}
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude);
if (waveLength > 0 && waveAmplitude > 0)
{
WalkPos -= transformedMovement.Length() / Math.Abs(waveLength);
WalkPos = MathUtils.WrapAngleTwoPi(WalkPos);
}
foreach (var limb in Limbs)
{
switch (limb.type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude);
}
break;
}
}
for (int i = 0; i < Limbs.Length; i++)
{
if (Limbs[i].SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
if (CurrentSwimParams.UseSineMovement)
{
mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Vector2.Lerp(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f);
}
floorY = Limbs[0].SimPosition.Y;
}
void UpdateWalkAnim(float deltaTime)
{
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
Collider.LinearVelocity = new Vector2(
movement.X,
Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y);
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
Vector2 colliderBottom = GetColliderBottom();
float movementAngle = 0.0f;
var mainLimb = MainLimb;
float mainLimbAngle = (mainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
}
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
movementAngle -= MathHelper.TwoPi;
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
if (TorsoAngle.HasValue)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
if (TorsoPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift);
if (torso != mainLimb)
{
pos.X = torso.SimPosition.X;
}
torso.MoveToPos(pos, TorsoMoveForce);
torso.PullJointEnabled = true;
torso.PullJointWorldAnchorB = pos;
}
}
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
if (HeadAngle.HasValue)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
if (HeadPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
if (head != mainLimb)
{
pos.X = head.SimPosition.X;
}
head.MoveToPos(pos, HeadMoveForce);
head.PullJointEnabled = true;
head.PullJointWorldAnchorB = pos;
}
}
if (TailAngle.HasValue)
{
var tail = GetLimb(LimbType.Tail);
if (tail != null)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque);
}
}
float prevWalkPos = WalkPos;
WalkPos -= mainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f);
Vector2 transformedStepSize = Vector2.Zero;
if (Math.Abs(TargetMovement.X) > 0.01f)
{
transformedStepSize = new Vector2(
(float)Math.Cos(WalkPos) * StepSize.Value.X * 3.0f,
(float)Math.Sin(WalkPos) * StepSize.Value.Y * 2.0f);
}
foreach (Limb limb in Limbs)
{
switch (limb.type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
Vector2 footPos = new Vector2(limb.SimPosition.X, colliderBottom.Y);
if (limb.RefJointIndex > -1)
{
if (LimbJoints.Length <= limb.RefJointIndex)
{
DebugConsole.ThrowError($"Reference joint index {limb.RefJointIndex} is out of array. This is probably due to a missing joint. If you just deleted a joint, don't do that without first removing the reference joint indices from the limbs. If this is not the case, please ensure that you have defined the index to the right joint.");
}
else
{
footPos.X = LimbJoints[limb.RefJointIndex].WorldAnchorA.X;
}
}
footPos.X += limb.StepOffset.X * Dir;
footPos.Y += limb.StepOffset.Y;
bool playFootstepSound = false;
if (limb.type == LimbType.LeftFoot)
{
if (Math.Sign(Math.Sin(prevWalkPos)) > 0 && Math.Sign(transformedStepSize.Y) < 0)
{
playFootstepSound = true;
}
limb.DebugRefPos = footPos + Vector2.UnitX * movement.X * 0.1f;
limb.DebugTargetPos = footPos + new Vector2(
transformedStepSize.X + movement.X * 0.1f,
(transformedStepSize.Y > 0.0f) ? transformedStepSize.Y : 0.0f);
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
}
else if (limb.type == LimbType.RightFoot)
{
if (Math.Sign(Math.Sin(prevWalkPos)) < 0 && Math.Sign(transformedStepSize.Y) > 0)
{
playFootstepSound = true;
}
limb.DebugRefPos = footPos + Vector2.UnitX * movement.X * 0.1f;
limb.DebugTargetPos = footPos + new Vector2(
-transformedStepSize.X + movement.X * 0.1f,
(-transformedStepSize.Y > 0.0f) ? -transformedStepSize.Y : 0.0f);
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
}
if (playFootstepSound)
{
#if CLIENT
PlayImpactSound(limb);
#endif
}
if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb,
movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.Params.ID] * Dir,
mainLimb, FootTorque);
}
break;
case LimbType.LeftLeg:
case LimbType.RightLeg:
if (Math.Abs(CurrentGroundedParams.LegTorque) > 0.001f) limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir);
break;
}
}
}
void UpdateDying(float deltaTime)
{
if (deathAnimDuration <= 0.0f) { return; }
float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f;
float animStrength = (1.0f - deathAnimTimer / deathAnimDuration);
Limb head = GetLimb(LimbType.Head);
if (head != null && head.IsSevered) { return; }
Limb tail = GetLimb(LimbType.Tail);
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength);
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength);
WalkPos += deltaTime * 10.0f * animStrength;
Vector2 centerOfMass = GetCenterOfMass();
foreach (Limb limb in Limbs)
{
#if CLIENT
if (limb.LightSource != null)
{
limb.LightSource.Color = Color.Lerp(limb.InitialLightSourceColor, Color.TransparentBlack, deathAnimTimer / deathAnimDuration);
if (limb.InitialLightSpriteAlpha.HasValue)
{
limb.LightSource.OverrideLightSpriteAlpha = MathHelper.Lerp(limb.InitialLightSpriteAlpha.Value, 0.0f, deathAnimTimer / deathAnimDuration);
}
}
#endif
if (limb.type == LimbType.Head || limb.type == LimbType.Tail || limb.IsSevered || !limb.body.Enabled) continue;
if (limb.Mass <= 0.0f)
{
string errorMsg = "Creature death animation error: invalid limb mass on character \"" + character.SpeciesName + "\" (type: " + limb.type + ", mass: " + limb.Mass + ")";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidMass" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
deathAnimTimer = deathAnimDuration;
return;
}
Vector2 diff = (centerOfMass - limb.SimPosition);
if (!MathUtils.IsValid(diff))
{
string errorMsg = "Creature death animation error: invalid diff (center of mass: " + centerOfMass + ", limb position: " + limb.SimPosition + ")";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidDiff" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
deathAnimTimer = deathAnimDuration;
return;
}
limb.body.ApplyForce(diff * (float)(Math.Sin(WalkPos) * Math.Sqrt(limb.Mass)) * 30.0f * animStrength, maxVelocity: 10.0f);
}
}
private void SmoothRotateWithoutWrapping(Limb limb, float angle, Limb referenceLimb, float torque)
{
//make sure the angle "has the same number of revolutions" as the reference limb
//(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints)
while (referenceLimb.Rotation - angle > MathHelper.TwoPi)
{
angle += MathHelper.TwoPi;
}
while (referenceLimb.Rotation - angle < -MathHelper.TwoPi)
{
angle -= MathHelper.TwoPi;
}
limb?.body.SmoothRotate(angle, torque, wrapAngle: false);
}
public override void Flip()
{
base.Flip();
foreach (Limb l in Limbs)
{
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally aligned limbs need to be flipped 180 degrees
l.body.SetTransform(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir);
}
//no need to do anything when flipping vertically oriented limbs
//the sprite gets flipped horizontally, which does the job
}
}
public void Mirror(bool lerp = true)
{
Vector2 centerOfMass = GetCenterOfMass();
foreach (Limb l in Limbs)
{
TrySetLimbPosition(l,
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
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))
{
float diff = character.SelectedCharacter.SimPosition.X - centerOfMass.X;
if (diff < 100.0f)
{
character.SelectedCharacter.AnimController.SetPosition(
new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp: true);
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,566 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
public enum HitDetection
{
Distance,
Contact
}
public enum AttackContext
{
NotDefined,
Water,
Ground,
Inside,
Outside
}
public enum AttackTarget
{
Any,
Character,
Structure // Including hulls etc. Evaluated as anything but a character.
}
public enum AIBehaviorAfterAttack
{
FallBack,
FallBackUntilCanAttack,
PursueIfCanAttack,
Pursue,
FollowThrough,
FollowThroughUntilCanAttack
}
struct AttackResult
{
public readonly float Damage;
public readonly List<Affliction> Afflictions;
public readonly Limb HitLimb;
public readonly List<DamageModifier> AppliedDamageModifiers;
public AttackResult(List<Affliction> afflictions, Limb hitLimb, List<DamageModifier> appliedDamageModifiers = null)
{
HitLimb = hitLimb;
Afflictions = new List<Affliction>();
foreach (Affliction affliction in afflictions)
{
Afflictions.Add(affliction.Prefab.Instantiate(affliction.Strength, affliction.Source));
}
AppliedDamageModifiers = appliedDamageModifiers;
Damage = Afflictions.Sum(a => a.GetVitalityDecrease(null));
}
public AttackResult(float damage, List<DamageModifier> appliedDamageModifiers = null)
{
Damage = damage;
HitLimb = null;
Afflictions = null;
AppliedDamageModifiers = appliedDamageModifiers;
}
}
partial class Attack : ISerializableEntity
{
[Serialize(AttackContext.NotDefined, true, 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]
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]
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]
public HitDetection HitDetectionType { get; private set; }
[Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable]
public AIBehaviorAfterAttack AfterAttack { get; set; }
[Serialize(false, true, 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]
public bool Retreat { get; private set; }
[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)]
public float Range { get; set; }
[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)]
public float DamageRange { get; set; }
[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)]
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)]
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)]
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)]
public float CoolDownRandomFactor { get; private set; } = 0;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float StructureDamage { get; set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
[Serialize(0.0f, false)]
public float Stun { get; private set; }
[Serialize(false, true, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; private set; }
[Serialize("", true), Editable]
public string ApplyForceOnLimbs
{
get
{
return string.Join(", ", ForceOnLimbIndices);
}
set
{
ForceOnLimbIndices.Clear();
if (string.IsNullOrEmpty(value)) { return; }
foreach (string limbIndexStr in value.Split(','))
{
if (int.TryParse(limbIndexStr.Trim(), out int limbIndex))
{
ForceOnLimbIndices.Add(limbIndex);
}
}
}
}
[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)]
public float Force { get; private set; }
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Torque { get; private set; }
[Serialize(false, true), 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)]
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)."), 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)]
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)."), Editable]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float SeverLimbsProbability { get; set; }
// TODO: disabled because not synced
//[Serialize(0.0f, true), 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)]
public float Priority { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
{
get { return statusEffects; }
}
public string Name => "Attack";
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
//the indices of the limbs Force is applied on
//(if none, force is applied only to the limb the attack is attached to)
public readonly List<int> ForceOnLimbIndices = new List<int>();
public readonly Dictionary<Affliction, XElement> Afflictions = new Dictionary<Affliction, XElement>();
/// <summary>
/// Only affects ai decision making. All the conditionals has to be met in order to select the attack. TODO: allow to define conditionals using any (implemented in StatusEffect -> move from there to PropertyConditional?)
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public void SetUser(Character user)
{
if (statusEffects == null) { return; }
foreach (StatusEffect statusEffect in statusEffects)
{
statusEffect.SetUser(user);
}
}
public List<Affliction> GetMultipliedAfflictions(float multiplier)
{
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions.Keys)
{
multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source));
}
return multipliedAfflictions;
}
public float GetStructureDamage(float deltaTime)
{
return (Duration == 0.0f) ? StructureDamage : StructureDamage * deltaTime;
}
public float GetItemDamage(float deltaTime)
{
return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime;
}
public float GetTotalDamage(bool includeStructureDamage = false)
{
float totalDamage = includeStructureDamage ? StructureDamage : 0.0f;
foreach (Affliction affliction in Afflictions.Keys)
{
totalDamage += affliction.GetVitalityDecrease(null);
}
return totalDamage;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float range = 0.0f)
{
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null);
Range = range;
DamageRange = range;
StructureDamage = structureDamage;
}
public Attack(XElement element, string parentDebugName)
{
Deserialize(element);
if (element.Attribute("damage") != null ||
element.Attribute("bluntdamage") != null ||
element.Attribute("burndamage") != null ||
element.Attribute("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\" />).");
}
InitProjSpecific(element);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
case "affliction":
AfflictionPrefab afflictionPrefab;
if (subElement.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
continue;
}
}
else
{
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
continue;
}
}
//float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
//var affliction = afflictionPrefab.Instantiate(afflictionStrength);
//Afflictions.Add(affliction, subElement);
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
Conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
}
}
partial void InitProjSpecific(XElement element = null);
public void ReloadAfflictions(XElement element)
{
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)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
affliction = afflictionPrefab.Instantiate(afflictionStrength);
}
else
{
affliction = new Affliction(null, 0);
}
affliction.Deserialize(subElement);
// add the affliction anyway, so that it can be shown in the editor.
Afflictions.Add(affliction, subElement);
}
}
public void Serialize(XElement element)
{
SerializableProperty.SerializeProperties(this, element, true);
foreach (var affliction in Afflictions)
{
if (affliction.Value != null)
{
affliction.Key.Serialize(affliction.Value);
}
}
}
public void Deserialize(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ReloadAfflictions(element);
}
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true)
{
Character targetCharacter = target as Character;
if (OnlyHumans)
{
if (targetCharacter != null && !targetCharacter.IsHuman)
{
return new AttackResult();
}
}
SetUser(attacker);
DamageParticles(deltaTime, worldPosition);
var attackResult = target.AddDamage(attacker, worldPosition, this, deltaTime, playSound);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
if (targetCharacter != null && targetCharacter.IsDead)
{
effectType = ActionType.OnEating;
}
foreach (StatusEffect effect in statusEffects)
{
// 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
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker, worldPosition);
}
if (targetCharacter != null)
{
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
}
if (target is Entity entity)
{
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(worldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, entity, targets);
}
}
}
return attackResult;
}
public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true)
{
if (targetLimb == null) return new AttackResult();
if (OnlyHumans)
{
if (targetLimb.character != null && !targetLimb.character.IsHuman)
{
return new AttackResult();
}
}
SetUser(attacker);
DamageParticles(deltaTime, worldPosition);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
{
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
}
return attackResult;
}
public float AttackTimer { get; private set; }
public float CoolDownTimer { get; set; }
public float SecondaryCoolDownTimer { get; set; }
public bool IsRunning { get; private set; }
public void UpdateCoolDown(float deltaTime)
{
CoolDownTimer -= deltaTime;
SecondaryCoolDownTimer -= deltaTime;
if (CoolDownTimer < 0) { CoolDownTimer = 0; }
if (SecondaryCoolDownTimer < 0) { SecondaryCoolDownTimer = 0; }
}
public void UpdateAttackTimer(float deltaTime)
{
IsRunning = true;
AttackTimer += deltaTime;
if (AttackTimer >= Duration)
{
ResetAttackTimer();
SetCoolDown();
}
}
public void ResetAttackTimer()
{
AttackTimer = 0;
IsRunning = false;
}
public void SetCoolDown()
{
float randomFraction = CoolDown * CoolDownRandomFactor;
CoolDownTimer = CoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
}
public void ResetCoolDown()
{
CoolDownTimer = 0;
SecondaryCoolDownTimer = 0;
}
partial void DamageParticles(float deltaTime, Vector2 worldPosition);
public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.NotDefined;
public bool IsValidContext(IEnumerable<AttackContext> contexts)
{
foreach (var context in contexts)
{
switch (context)
{
case AttackContext.Ground:
if (Context == AttackContext.Water)
{
return false;
}
break;
case AttackContext.Water:
if (Context == AttackContext.Ground)
{
return false;
}
break;
case AttackContext.Inside:
if (Context == AttackContext.Outside)
{
return false;
}
break;
case AttackContext.Outside:
if (Context == AttackContext.Inside)
{
return false;
}
break;
default:
continue;
}
}
return true;
}
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(Entity target)
{
switch (TargetType)
{
case AttackTarget.Character:
return target is Character;
case AttackTarget.Structure:
return !(target is Character);
case AttackTarget.Any:
default:
return true;
}
}
}
}
@@ -0,0 +1,33 @@
using System;
namespace Barotrauma
{
enum CauseOfDeathType
{
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected
}
class CauseOfDeath
{
public readonly CauseOfDeathType Type;
public readonly AfflictionPrefab Affliction;
public readonly Character Killer;
public readonly Entity DamageSource;
public CauseOfDeath(CauseOfDeathType type, AfflictionPrefab affliction, Character killer, Entity damageSource)
{
if (type == CauseOfDeathType.Affliction && affliction == null)
{
string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
type = CauseOfDeathType.Unknown;
}
Type = type;
Affliction = affliction;
Killer = killer;
DamageSource = damageSource;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class CharacterStateInfo : PosInfo
{
public readonly Direction Direction;
public readonly Character SelectedCharacter;
public readonly Item SelectedItem;
public readonly AnimController.Animation Animation;
public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, animation)
{
}
public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, animation)
{
}
protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, AnimController.Animation animation = AnimController.Animation.None)
: base(pos, rotation, velocity, angularVelocity, ID, time)
{
Direction = dir;
SelectedCharacter = selectedCharacter;
SelectedItem = selectedItem;
Animation = animation;
}
}
partial class Character
{
[Flags]
private enum InputNetFlags : ushort
{
None = 0x0,
Left = 0x1,
Right = 0x2,
Up = 0x4,
Down = 0x8,
FacingLeft = 0x10,
Run = 0x20,
Crouch = 0x40,
Select = 0x80,
Use = 0x100,
Aim = 0x200,
Attack = 0x400,
Ragdoll = 0x800,
Health = 0x1000,
Grab = 0x2000,
Deselect = 0x4000, // 16384
Shoot = 0x8000, // 32768
MaxVal = 0xFFFF // 65535
//MaxVal = 0x7FFF // 32767
//MaxVal = 0x3FFF // 16383
}
private InputNetFlags dequeuedInput = 0;
private InputNetFlags prevDequeuedInput = 0;
public UInt16 LastNetworkUpdateID = 0;
/// <summary>
/// ID of the last inputs the server has processed
/// </summary>
public UInt16 LastProcessedID;
private struct NetInputMem
{
public InputNetFlags states; //keys pressed/other boolean states at this step
public UInt16 intAim; //aim angle, represented as an unsigned short where 0=0º, 65535=just a bit under 360º
public UInt16 interact; //id of the entity being interacted with
public UInt16 networkUpdateID;
}
private List<NetInputMem> memInput = new List<NetInputMem>();
private List<CharacterStateInfo> memState = new List<CharacterStateInfo>();
private List<CharacterStateInfo> memLocalState = new List<CharacterStateInfo>();
public float healthUpdateTimer;
private float healthUpdateInterval;
public float HealthUpdateInterval
{
get { return healthUpdateInterval; }
set
{
healthUpdateInterval = MathHelper.Clamp(value, 0.0f, IsDead ? NetConfig.MaxHealthUpdateIntervalDead : NetConfig.MaxHealthUpdateInterval);
healthUpdateTimer = Math.Min(healthUpdateTimer, healthUpdateInterval);
}
}
public bool isSynced = false;
public List<CharacterStateInfo> MemState
{
get { return memState; }
}
public List<CharacterStateInfo> MemLocalState
{
get { return memLocalState; }
}
public void ResetNetState()
{
memInput.Clear();
memState.Clear();
memLocalState.Clear();
LastNetworkUpdateID = 0;
LastProcessedID = 0;
}
partial void UpdateNetInput();
}
}
@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class CharacterPrefab : IPrefab, IDisposable
{
public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
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 ContentPackage ContentPackage { get; private set; }
public XDocument XDocument { get; private set; }
public static IEnumerable<string> ConfigFilePaths => Prefabs.Select(p => p.FilePath);
public static IEnumerable<XDocument> ConfigFiles => Prefabs.Select(p => p.XDocument);
public const string HumanSpeciesName = "human";
public static string HumanConfigFile => FindBySpeciesName(HumanSpeciesName).FilePath;
/// <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)
{
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());
}
public static CharacterPrefab Find(Predicate<CharacterPrefab> predicate)
{
return Prefabs.Find(predicate);
}
public static void RemoveByFile(string file)
{
Prefabs.RemoveByFile(file);
}
public static bool LoadFromFile(ContentFile file, bool forceOverride=false)
{
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)
{
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.IsOverride() ? doc.Root.FirstElement() : doc.Root;
var name = mainElement.GetAttributeString("name", null);
if (name != null)
{
DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
name = mainElement.GetAttributeString("speciesname", string.Empty);
}
if (string.IsNullOrWhiteSpace(name))
{
DebugConsole.ThrowError($"No species name defined for: {filePath}");
return false;
}
var 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 void LoadAll()
{
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character))
{
LoadFromFile(file);
}
}
}
}
@@ -0,0 +1,208 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class Affliction : ISerializableEntity
{
public readonly AfflictionPrefab Prefab;
public string Name => ToString();
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
[Serialize(0f, true), Editable]
public float Strength { get; set; }
[Serialize("", true), Editable]
public string Identifier { get; private set; }
[Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
public float Probability { get; private set; } = 1.0f;
public float DamagePerSecond;
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
public float StrengthDiminishMultiplier = 1.0f;
public Affliction MultiplierSource;
/// <summary>
/// Which character gave this affliction
/// </summary>
public Character Source;
public Affliction(AfflictionPrefab prefab, float strength)
{
Prefab = prefab;
Strength = strength;
Identifier = prefab?.Identifier;
}
public void Serialize(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
}
public void Deserialize(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public Affliction CreateMultiplied(float multiplier)
{
return Prefab.Instantiate(Strength * multiplier, Source);
}
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) return 0.0f;
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
if (currentEffect.MultiplyByMaxVitality) currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
return currVitalityDecrease;
}
public float GetScreenDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinScreenDistortStrength,
currentEffect.MaxScreenDistortStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetRadialDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinRadialDistortStrength,
currentEffect.MaxRadialDistortStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetChromaticAberrationStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinChromaticAberrationStrength,
currentEffect.MaxChromaticAberrationStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetScreenBlurStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinScreenBlurStrength,
currentEffect.MaxScreenBlurStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public void CalculateDamagePerSecond(float currentVitalityDecrease)
{
DamagePerSecond = Math.Max(DamagePerSecond, currentVitalityDecrease - PreviousVitalityDecrease);
if (DamagePerSecondTimer >= 1.0f)
{
DamagePerSecond = currentVitalityDecrease - PreviousVitalityDecrease;
PreviousVitalityDecrease = currentVitalityDecrease;
DamagePerSecondTimer = 0.0f;
}
}
public float GetResistance(string afflictionId)
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) return 0.0f;
if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinResistance,
currentEffect.MaxResistance,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetSpeedMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) return 1.0f;
return MathHelper.Lerp(
currentEffect.MinSpeedMultiplier,
currentEffect.MaxSpeedMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return;
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
{
Strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier;
}
else // Reduce strengthening of afflictions if resistant
{
Strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
}
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
statusEffect.SetUser(Source);
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
}
}
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
class AfflictionBleeding : Affliction
{
public AfflictionBleeding(AfflictionPrefab prefab, float strength) :
base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
}
}
}
@@ -0,0 +1,301 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class AfflictionHusk : Affliction
{
public enum InfectionState
{
Dormant, Transition, Active
}
private bool subscribedToDeathEvent;
private InfectionState state;
private List<Limb> huskAppendage;
public InfectionState State
{
get { return state; }
}
public AfflictionHusk(AfflictionPrefab prefab, float strength) :
base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
float prevStrength = Strength;
base.Update(characterHealth, targetLimb, deltaTime);
if (!subscribedToDeathEvent)
{
characterHealth.Character.OnDeath += CharacterDead;
subscribedToDeathEvent = true;
}
if (characterHealth.Character == Character.Controlled) UpdateMessages(prevStrength, characterHealth.Character);
if (Strength < Prefab.MaxStrength * 0.5f)
{
UpdateDormantState(deltaTime, characterHealth.Character);
}
else if (Strength < Prefab.MaxStrength)
{
characterHealth.Character.SpeechImpediment = 100.0f;
UpdateTransitionState(deltaTime, characterHealth.Character);
}
else
{
characterHealth.Character.SpeechImpediment = 100.0f;
UpdateActiveState(deltaTime, characterHealth.Character);
}
}
partial void UpdateMessages(float prevStrength, Character character);
private void UpdateDormantState(float deltaTime, Character character)
{
if (state != InfectionState.Dormant)
{
DeactivateHusk(character);
}
state = InfectionState.Dormant;
}
private void UpdateTransitionState(float deltaTime, Character character)
{
if (state != InfectionState.Transition)
{
DeactivateHusk(character);
}
state = InfectionState.Transition;
}
private void UpdateActiveState(float deltaTime, Character character)
{
if (state != InfectionState.Active)
{
ActivateHusk(character);
state = InfectionState.Active;
}
foreach (Limb limb in character.AnimController.Limbs)
{
character.LastDamageSource = null;
character.DamageLimb(
limb.WorldPosition, limb,
new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(0.5f * deltaTime / character.AnimController.Limbs.Length) },
0.0f, false, 0.0f);
}
}
public void ActivateHusk(Character character)
{
if (huskAppendage == null)
{
huskAppendage = AttachHuskAppendage(character, Prefab.Identifier);
if (huskAppendage != null)
{
character.NeedsAir = false;
character.SetStun(0.5f);
}
#if CLIENT
character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = true;
#endif
}
}
private void DeactivateHusk(Character character)
{
character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
if (huskAppendage != null)
{
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
huskAppendage = null;
#if CLIENT
character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = false;
#endif
}
}
public void Remove(Character character)
{
DeactivateHusk(character);
if (character != null) character.OnDeath -= CharacterDead;
subscribedToDeathEvent = false;
}
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < Prefab.MaxStrength * 0.5f || character.Removed) { return; }
//don't turn the character into a husk if any of its limbs are severed
if (character.AnimController?.LimbJoints != null)
{
foreach (var limbJoint in character.AnimController.LimbJoints)
{
if (limbJoint.IsSevered) return;
}
}
//create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it
CoroutineManager.StartCoroutine(CreateAIHusk(character));
}
private IEnumerable<object> CreateAIHusk(Character character)
{
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
string speciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.");
yield return CoroutineStatus.Success;
}
var husk = Character.Create(speciesName, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams);
foreach (Limb limb in husk.AnimController.Limbs)
{
if (limb.type == LimbType.None)
{
limb.body.SetTransform(character.SimPosition, 0.0f);
continue;
}
var matchingLimb = character.AnimController.GetLimb(limb.type);
if (matchingLimb?.body != null)
{
limb.body.SetTransform(matchingLimb.SimPosition, matchingLimb.Rotation);
limb.body.LinearVelocity = matchingLimb.LinearVelocity;
limb.body.AngularVelocity = matchingLimb.body.AngularVelocity;
}
}
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
{
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
}
for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null) continue;
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
}
yield return CoroutineStatus.Success;
}
public static List<Limb> AttachHuskAppendage(Character character, string afflictionIdentifier, XElement appendageDefinition = null, Ragdoll ragdoll = null)
{
var appendage = new List<Limb>();
if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction))
{
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);
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (huskPrefab?.XDocument == 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 element = appendageDefinition;
if (element == null)
{
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
}
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);
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
if (doc == null) { return appendage; }
if (ragdoll == null)
{
ragdoll = character.AnimController;
}
if (ragdoll.Dir < 1.0f)
{
ragdoll.Flip();
}
var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
foreach (var jointElement in doc.Root.Elements("joint"))
{
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement))
{
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is.");
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams)
{
// Ensure that we have a valid id for the new limb
ID = ragdoll.Limbs.Length
};
jointParams.Limb2 = appendageLimbParams.ID;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
else
{
DebugConsole.ThrowError("Attachment limb not found!");
}
}
}
return appendage;
}
public static string GetHuskedSpeciesName(string speciesName, AfflictionPrefabHusk prefab)
{
return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName);
}
public static string GetNonHuskedSpeciesName(string huskedSpeciesName, AfflictionPrefabHusk prefab)
{
string nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag);
return huskedSpeciesName.ToLowerInvariant().Remove(nonTag);
}
}
}
@@ -0,0 +1,623 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using System.Linq;
using System.Security.Cryptography;
namespace Barotrauma
{
static class CPRSettings
{
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; }
private static string insufficientSkillAfflictionIdentifier { get; set; }
public static AfflictionPrefab InsufficientSkillAffliction
{
get
{
return
AfflictionPrefab.Prefabs.ContainsKey(insufficientSkillAfflictionIdentifier) ?
AfflictionPrefab.Prefabs[insufficientSkillAfflictionIdentifier] :
AfflictionPrefab.InternalDamage;
}
}
public static void Load(XElement element, string filePath)
{
ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f);
ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f);
ReviveChanceMin = MathHelper.Clamp(element.GetAttributeFloat("revivechancemin", 0.05f), 0.0f, 1.0f);
ReviveChanceMax = MathHelper.Clamp(element.GetAttributeFloat("revivechancemax", 0.9f), ReviveChanceMin, 1.0f);
StabilizationPerSkill = Math.Max(element.GetAttributeFloat("stabilizationperskill", 0.01f), 0.0f);
StabilizationMin = MathHelper.Max(element.GetAttributeFloat("stabilizationmin", 0.05f), 0.0f);
StabilizationMax = MathHelper.Max(element.GetAttributeFloat("stabilizationmax", 2.0f), StabilizationMin);
DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f);
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;
}
}
class AfflictionPrefabHusk : AfflictionPrefab
{
public AfflictionPrefabHusk(XElement element, string filePath, Type type = null) : base(element, filePath, type)
{
HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant();
if (HuskedSpeciesName == null)
{
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
HuskedSpeciesName = "[speciesname]husk";
}
TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true);
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
TargetSpecies = new string[] { "human" };
}
var attachElement = element.GetChildElement("attachlimb");
if (attachElement != null)
{
AttachLimbId = attachElement.GetAttributeInt("id", -1);
AttachLimbName = attachElement.GetAttributeString("name", null);
AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None;
}
else
{
AttachLimbId = -1;
AttachLimbName = null;
AttachLimbType = LimbType.None;
}
}
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
public readonly string HuskedSpeciesName;
public readonly string[] TargetSpecies;
public const string Tag = "[speciesname]";
}
class AfflictionPrefab : IPrefab, IDisposable
{
public class Effect
{
//this effect is applied when the strength is within this range
public float MinStrength, MaxStrength;
public readonly float MinVitalityDecrease = 0.0f;
public readonly float MaxVitalityDecrease = 0.0f;
//how much the strength of the affliction changes per second
public readonly float StrengthChange = 0.0f;
public readonly bool MultiplyByMaxVitality;
public float MinScreenBlurStrength, MaxScreenBlurStrength;
public float MinScreenDistortStrength, MaxScreenDistortStrength;
public float MinRadialDistortStrength, MaxRadialDistortStrength;
public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength;
public float MinSpeedMultiplier, MaxSpeedMultiplier;
public float MinBuffMultiplier, MaxBuffMultiplier;
public float MinResistance, MaxResistance;
public string ResistanceFor;
public string DialogFlag;
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public Effect(XElement element, string parentDebugName)
{
MinStrength = element.GetAttributeFloat("minstrength", 0);
MaxStrength = element.GetAttributeFloat("maxstrength", 0);
MultiplyByMaxVitality = element.GetAttributeBool("multiplybymaxvitality", false);
MinVitalityDecrease = element.GetAttributeFloat("minvitalitydecrease", 0.0f);
MaxVitalityDecrease = element.GetAttributeFloat("maxvitalitydecrease", 0.0f);
MaxVitalityDecrease = Math.Max(MinVitalityDecrease, MaxVitalityDecrease);
MinScreenDistortStrength = element.GetAttributeFloat("minscreendistort", 0.0f);
MaxScreenDistortStrength = element.GetAttributeFloat("maxscreendistort", 0.0f);
MaxScreenDistortStrength = Math.Max(MinScreenDistortStrength, MaxScreenDistortStrength);
MinRadialDistortStrength = element.GetAttributeFloat("minradialdistort", 0.0f);
MaxRadialDistortStrength = element.GetAttributeFloat("maxradialdistort", 0.0f);
MaxRadialDistortStrength = Math.Max(MinRadialDistortStrength, MaxRadialDistortStrength);
MinChromaticAberrationStrength = element.GetAttributeFloat("minchromaticaberration", 0.0f);
MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f);
MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength);
MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f);
MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f);
MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength);
ResistanceFor = element.GetAttributeString("resistancefor", "");
MinResistance = element.GetAttributeFloat("minresistance", 0.0f);
MaxResistance = element.GetAttributeFloat("maxresistance", 0.0f);
MaxResistance = Math.Max(MinResistance, MaxResistance);
MinSpeedMultiplier = element.GetAttributeFloat("minspeedmultiplier", 1.0f);
MaxSpeedMultiplier = element.GetAttributeFloat("maxspeedmultiplier", 1.0f);
MaxSpeedMultiplier = Math.Max(MinSpeedMultiplier, MaxSpeedMultiplier);
MinBuffMultiplier = element.GetAttributeFloat("minbuffmultiplier", 1.0f);
MaxBuffMultiplier = element.GetAttributeFloat("maxbuffmultiplier", 1.0f);
MaxBuffMultiplier = Math.Max(MinBuffMultiplier, MaxBuffMultiplier);
DialogFlag = element.GetAttributeString("dialogflag", "");
StrengthChange = element.GetAttributeFloat("strengthchange", 0.0f);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
}
}
}
}
public static AfflictionPrefab InternalDamage;
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 readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
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;
// Arbitrary string that is used to identify the type of the affliction.
public readonly string AfflictionType;
//Does the affliction affect a specific limb or the whole character
public readonly bool LimbSpecific;
//If not a limb-specific affliction, which limb is the indicator shown on in the health menu
//(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 bool IsBuff;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
//how high the strength has to be for the affliction to take affect
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
public readonly float ShowIconThreshold = 0.05f;
public readonly float MaxStrength = 100.0f;
//how high the strength has to be for the affliction icon to be shown with a health scanner
public readonly float ShowInHealthScannerThreshold = 0.05f;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
public float BurnOverlayAlpha;
public float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character
public readonly string AchievementOnRemoved;
public readonly Sprite Icon;
public readonly Color[] IconColors;
private List<Effect> effects = new List<Effect>();
private readonly string typeName;
private readonly ConstructorInfo constructor;
public IEnumerable<KeyValuePair<string, float>> TreatmentSuitability
{
get
{
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
if (suitability > 0.0f)
{
yield return new KeyValuePair<string, float>(itemPrefab.Identifier, suitability);
}
}
}
}
public static void LoadAll(IEnumerable<ContentFile> files)
{
CPRSettings.Unload();
InternalDamage = null;
Bleeding = null;
Burn = null;
OxygenLow = null;
Bloodloss = null;
Pressure = null;
Stun = null;
#if CLIENT
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
CharacterHealth.DamageOverlayFile = string.Empty;
#endif
var prevPrefabs = Prefabs.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."); }
}
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.");
}
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":
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 "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;
}
if (prefab != null)
{
Prefabs.Add(prefab, isOverride);
}
}
using MD5 md5 = MD5.Create();
foreach (AfflictionPrefab prefab in Prefabs)
{
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Afflictions: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
collision.UIntIdentifier++;
}
}
}
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", "");
Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", "");
IsBuff = element.GetAttributeBool("isbuff", false);
LimbSpecific = element.GetAttributeBool("limbspecific", false);
if (!LimbSpecific)
{
string indicatorLimbName = element.GetAttributeString("indicatorlimb", "Torso");
if (!Enum.TryParse(indicatorLimbName, out IndicatorLimb))
{
DebugConsole.ThrowError("Error in affliction prefab " + Name + " - limb type \"" + indicatorLimbName + "\" not found.");
}
}
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f));
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
IconColors = element.GetAttributeColorArray("iconcolors", null);
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
break;
case "effect":
effects.Add(new Effect(subElement, Name));
break;
}
}
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) });
}
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
}
public Affliction Instantiate(float strength, Character source = null)
{
object instance = null;
try
{
instance = constructor.Invoke(new object[] { this, strength });
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
Affliction affliction = instance as Affliction;
affliction.Source = source;
return affliction;
}
public Effect GetActiveEffect(float currentStrength)
{
foreach (Effect effect in effects)
{
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) return effect;
}
//if above the strength range of all effects, use the highest strength effect
Effect strongestEffect = null;
float largestStrength = currentStrength;
foreach (Effect effect in effects)
{
if (currentStrength > effect.MaxStrength &&
(strongestEffect == null || effect.MaxStrength > largestStrength))
{
strongestEffect = effect;
largestStrength = effect.MaxStrength;
}
}
return strongestEffect;
}
public float GetTreatmentSuitability(Item item)
{
if (item == null)
{
return 0.0f;
}
return Math.Max(item.Prefab.GetTreatmentSuitability(Identifier), item.Prefab.GetTreatmentSuitability(AfflictionType));
}
}
}
@@ -0,0 +1,27 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Barotrauma.Extensions;
using System.Xml.Linq;
using System;
namespace Barotrauma
{
partial class AfflictionPsychosis : Affliction
{
public AfflictionPsychosis(AfflictionPrefab prefab, float strength) : base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
UpdateProjSpecific(characterHealth, targetLimb, deltaTime);
}
partial void UpdateProjSpecific(CharacterHealth characterHealth, Limb targetLimb, float deltaTime);
}
}
@@ -0,0 +1,71 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
class AfflictionSpaceHerpes : Affliction
{
private float invertControlsCooldown = 60.0f;
private float stunCoolDown = 60.0f;
private float invertControlsTimer;
private float invertControlsToggleTimer;
public AfflictionSpaceHerpes(AfflictionPrefab prefab, float strength) : base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
invertControlsCooldown -= deltaTime;
if (invertControlsCooldown <= 0.0f)
{
//invert controls every 126-234 seconds when strength is close to 0
//every 56-104 seconds when strength is close to 100
invertControlsCooldown = (180.0f - Strength) * Rand.Range(0.7f, 1.3f);
invertControlsTimer = MathHelper.Lerp(10.0f, 60.0f, Strength / 100.0f) * Rand.Range(0.7f, 1.3f);
}
else if (invertControlsTimer > 0.0f)
{
//randomly toggle inverted controls on/off every 5 seconds
invertControlsToggleTimer -= deltaTime;
if (invertControlsToggleTimer <= 0.0f)
{
invertControlsToggleTimer = 5.0f;
if (Rand.Range(0.0f, 1.0f) < 0.5f)
{
characterHealth.ReduceAffliction(null, "invertcontrols", 100);
}
else
{
var invertControlsAffliction = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == "invertcontrols");
characterHealth.ApplyAffliction(null, new Affliction(invertControlsAffliction, 5.0f));
}
}
invertControlsTimer -= deltaTime;
}
if (Strength > 50.0f)
{
stunCoolDown -= deltaTime;
if (stunCoolDown <= 0.0f)
{
//stun every 126-234 seconds when strength is close to 0
//stun 56-104 seconds when strength is close to 100
stunCoolDown = (180.0f - Strength) * Rand.Range(0.7f, 1.3f);
float stunDuration = MathHelper.Lerp(3.0f, 10.0f, Strength / 100.0f) * Rand.Range(0.7f, 1.3f);
characterHealth.Character.SetStun(stunDuration);
}
}
}
}
}
@@ -0,0 +1,53 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class BuffDurationIncrease : Affliction
{
public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
var afflictions = characterHealth.GetAllAfflictions();
if (Strength <= 0)
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) continue;
affliction.MultiplierSource = null;
affliction.StrengthDiminishMultiplier = 1f;
}
}
else
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource == this) continue;
float multiplier = GetDiminishMultiplier();
if (affliction.StrengthDiminishMultiplier < multiplier) continue;
affliction.MultiplierSource = this;
affliction.StrengthDiminishMultiplier = multiplier;
}
}
}
private float GetDiminishMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
return MathHelper.Lerp(
currentEffect.MinBuffMultiplier,
currentEffect.MaxBuffMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
}
}
@@ -0,0 +1,883 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
namespace Barotrauma
{
partial class CharacterHealth
{
class LimbHealth
{
public Sprite IndicatorSprite;
public Sprite HighlightSprite;
public Rectangle HighlightArea;
public readonly string 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>();
private readonly CharacterHealth characterHealth;
public float TotalDamage
{
get { return Afflictions.Sum(a => a.GetVitalityDecrease(characterHealth)); }
}
public LimbHealth() { }
public LimbHealth(XElement element, CharacterHealth characterHealth)
{
Name = TextManager.Get("HealthLimbName." + element.GetAttributeString("name", ""));
this.characterHealth = characterHealth;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
IndicatorSprite = new Sprite(subElement);
HighlightArea = subElement.GetAttributeRect("highlightarea", new Rectangle(0, 0, (int)IndicatorSprite.size.X, (int)IndicatorSprite.size.Y));
break;
case "highlightsprite":
HighlightSprite = new Sprite(subElement);
break;
case "vitalitymultiplier":
if (subElement.Attribute("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))
{
VitalityMultipliers.Add(afflictionIdentifier.ToLowerInvariant(), multiplier);
}
else
{
VitalityTypeMultipliers.Add(afflictionType.ToLowerInvariant(), multiplier);
}
break;
}
}
}
public List<Affliction> GetActiveAfflictions(AfflictionPrefab prefab)
{
return Afflictions.FindAll(a => a.Prefab == prefab);
}
public List<Affliction> GetActiveAfflictions(string afflictionType)
{
return Afflictions.FindAll(a => a.Prefab.AfflictionType == afflictionType);
}
}
public const float InsufficientOxygenThreshold = 30.0f;
public const float LowOxygenThreshold = 50.0f;
protected float minVitality;
protected float maxVitality
{
get => Character.Params.Health.Vitality;
set => Character.Params.Health.Vitality = value;
}
public bool Unkillable;
public bool DoesBleed
{
get => Character.Params.Health.DoesBleed;
private set => Character.Params.Health.DoesBleed = value;
}
public bool UseHealthWindow
{
get => Character.Params.Health.UseHealthWindow;
set => Character.Params.Health.UseHealthWindow = value;
}
public float CrushDepth
{
get => Character.Params.Health.CrushDepth;
private set => Character.Params.Health.CrushDepth = value;
}
private List<LimbHealth> limbHealths = new List<LimbHealth>();
//non-limb-specific afflictions
private List<Affliction> afflictions = new List<Affliction>();
private HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
private Affliction bloodlossAffliction;
private Affliction oxygenLowAffliction;
private Affliction pressureAffliction;
private Affliction stunAffliction;
public bool IsUnconscious
{
get { return Vitality <= 0.0f; }
}
public float PressureKillDelay { get; private set; } = 5.0f;
public float Vitality { get; private set; }
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
public float MaxVitality
{
get
{
if (Character?.Info?.Job?.Prefab != null)
{
return maxVitality + Character.Info.Job.Prefab.VitalityModifier;
}
return maxVitality;
}
}
public float MinVitality
{
get
{
if (Character?.Info?.Job?.Prefab != null)
{
return -MaxVitality;
}
return minVitality;
}
}
public float OxygenAmount
{
get
{
if (!Character.NeedsAir || Unkillable) return 100.0f;
return -oxygenLowAffliction.Strength + 100;
}
set
{
if (!Character.NeedsAir || Unkillable) return;
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
}
}
public float BloodlossAmount
{
get { return bloodlossAffliction.Strength; }
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public float StunTimer
{
get { return stunAffliction.Strength; }
set { stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength); }
}
public Affliction PressureAffliction
{
get { return pressureAffliction; }
}
public Character Character { get; private set; }
public CharacterHealth(Character character)
{
this.Character = character;
Vitality = 100.0f;
DoesBleed = true;
UseHealthWindow = false;
InitIrremovableAfflictions();
limbHealths.Add(new LimbHealth());
InitProjSpecific(null, character);
}
public CharacterHealth(XElement element, Character character)
{
this.Character = character;
InitIrremovableAfflictions();
Vitality = maxVitality;
minVitality = character.IsHuman ? -100.0f : 0.0f;
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
}
if (limbHealths.Count == 0)
{
limbHealths.Add(new LimbHealth());
}
InitProjSpecific(element, character);
}
private void InitIrremovableAfflictions()
{
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
foreach (Affliction affliction in irremovableAfflictions)
{
afflictions.Add(affliction);
}
}
partial void InitProjSpecific(XElement element, Character character);
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter = null)
{
return limbHealthFilter == null
? afflictions.Union(limbHealths.SelectMany(lh => lh.Afflictions))
: afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter)));
}
private LimbHealth GetMatchingLimbHealth(Limb limb) => limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb));
/// <summary>
/// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb.
/// </summary>
private IEnumerable<Affliction> GetMatchingAfflictions(LimbHealth limb, Func<Affliction, bool> predicate)
=> limb.Afflictions.Where(predicate).Union(afflictions.Where(a => predicate(a) && GetMatchingLimbHealth(a) == limb));
public IEnumerable<Affliction> GetAfflictionsByType(string afflictionType, bool allowLimbAfflictions = true)
{
if (allowLimbAfflictions)
{
return GetAllAfflictions(a => a.Prefab.AfflictionType == afflictionType);
}
else
{
return afflictions.Where(a => a.Prefab.AfflictionType == afflictionType);
}
}
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
if (!allowLimbAfflictions) return null;
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
}
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public IEnumerable<Affliction> GetAfflictionsByType(string afflictionType, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return null;
}
return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType);
}
public Affliction GetAffliction(string identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return null;
}
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
return null;
}
public Limb GetAfflictionLimb(Affliction affliction)
{
for (int i = 0; i < limbHealths.Count; i++)
{
if (!limbHealths[i].Afflictions.Contains(affliction)) continue;
return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i);
}
return null;
}
/// <summary>
/// Get the total strength of the afflictions of a specific type attached to a specific limb
/// </summary>
/// <param name="afflictionType">Type of the affliction</param>
/// <param name="limb">The limb the affliction is attached to</param>
/// <param name="requireLimbSpecific">Does the affliction have to be attached to only the specific limb.
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific)
{
if (requireLimbSpecific && limbHealths.Count == 1) return 0.0f;
float strength = 0.0f;
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
}
return strength;
}
public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true)
{
float strength = 0.0f;
foreach (Affliction affliction in afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
}
if (!allowLimbAfflictions) return strength;
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
}
}
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction)
{
if (Unkillable) { return; }
if (affliction.Prefab.LimbSpecific)
{
if (targetLimb == null)
{
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
foreach (LimbHealth limbHealth in limbHealths)
{
AddLimbAffliction(limbHealth, affliction);
}
}
else
{
AddLimbAffliction(targetLimb, affliction);
}
}
else
{
AddAffliction(affliction);
}
}
public float GetResistance(string resistanceId)
{
float resistance = 0.0f;
for (int i = 0; i < afflictions.Count; i++)
{
if (!afflictions[i].Prefab.IsBuff) continue;
float temp = afflictions[i].GetResistance(resistanceId);
if (temp > resistance) resistance = temp;
}
return resistance;
}
private List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAffliction(Limb targetLimb, string affliction, float amount)
{
matchingAfflictions.Clear();
if (targetLimb != null)
{
matchingAfflictions.AddRange(limbHealths[targetLimb.HealthIndex].Afflictions);
}
else
{
foreach (LimbHealth limbHealth in limbHealths)
{
matchingAfflictions.AddRange(limbHealth.Afflictions);
}
}
matchingAfflictions.RemoveAll(a =>
!a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase));
if (matchingAfflictions.Count == 0) return;
float reduceAmount = amount / matchingAfflictions.Count;
for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
{
var matchingAffliction = matchingAfflictions[i];
if (matchingAffliction.Strength < reduceAmount)
{
float surplus = reduceAmount - matchingAffliction.Strength;
amount -= matchingAffliction.Strength;
matchingAffliction.Strength = 0.0f;
matchingAfflictions.RemoveAt(i);
if (i == 0) i = matchingAfflictions.Count;
if (i > 0) reduceAmount += surplus / i;
SteamAchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
}
else
{
matchingAffliction.Strength -= reduceAmount;
amount -= reduceAmount;
}
}
CalculateVitality();
}
public void ApplyDamage(Limb hitLimb, AttackResult attackResult)
{
if (Unkillable) { return; }
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
return;
}
foreach (Affliction newAffliction in attackResult.Afflictions)
{
if (newAffliction.Prefab.LimbSpecific)
{
AddLimbAffliction(hitLimb, newAffliction);
}
else
{
AddAffliction(newAffliction);
}
}
}
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
if (Unkillable) { return; }
foreach (LimbHealth limbHealth in limbHealths)
{
limbHealth.Afflictions.RemoveAll(a =>
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType ||
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType);
if (damageAmount > 0.0f) limbHealth.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount));
if (bleedingDamageAmount > 0.0f && DoesBleed) limbHealth.Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount));
if (burnDamageAmount > 0.0f) limbHealth.Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount));
}
CalculateVitality();
if (Vitality <= MinVitality) { Kill(); }
}
public void RemoveAllAfflictions()
{
foreach (LimbHealth limbHealth in limbHealths)
{
limbHealth.Afflictions.Clear();
}
afflictions.RemoveAll(a => !irremovableAfflictions.Contains(a));
foreach (Affliction affliction in irremovableAfflictions)
{
affliction.Strength = 0.0f;
}
CalculateVitality();
}
private void AddLimbAffliction(Limb limb, Affliction newAffliction)
{
if (!newAffliction.Prefab.LimbSpecific || limb == null) return;
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return;
}
AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction);
}
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (newAffliction.Prefab == affliction.Prefab)
{
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
return;
}
}
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))),
newAffliction.Source);
limbHealth.Afflictions.Add(copyAffliction);
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
#if CLIENT
selectedLimbIndex = -1;
#endif
}
private void AddAffliction(Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (newAffliction.Prefab.AfflictionType == "huskinfection")
{
var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk;
if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
return;
}
}
foreach (Affliction affliction in afflictions)
{
if (newAffliction.Prefab == affliction.Prefab)
{
float newStrength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
affliction.Strength = newStrength;
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
return;
}
}
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
afflictions.Add(newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))),
source: newAffliction.Source));
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
}
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
{
UpdateOxygen(deltaTime);
for (int i = 0; i < limbHealths.Count; i++)
{
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
{
if (limbHealths[i].Afflictions[j].Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(limbHealths[i].Afflictions[j], Character);
limbHealths[i].Afflictions.RemoveAt(j);
}
}
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
{
var affliction = limbHealths[i].Afflictions[j];
Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i);
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding)
{
UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
}
for (int i = afflictions.Count - 1; i >= 0; i--)
{
var affliction = afflictions[i];
if (irremovableAfflictions.Contains(affliction)) continue;
if (affliction.Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
afflictions.RemoveAt(i);
}
}
for (int i = 0; i < afflictions.Count; i++)
{
var affliction = afflictions[i];
affliction.Update(this, null, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
UpdateLimbAfflictionOverlays();
CalculateVitality();
if (Vitality <= MinVitality) Kill();
}
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsAir) return;
float prevOxygen = OxygenAmount;
if (IsUnconscious)
{
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
OxygenAmount = MathHelper.Clamp(OxygenAmount - 1.0f * deltaTime, -100.0f, 100.0f);
}
else
{
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? -5.0f : 10.0f), -100.0f, 100.0f);
}
UpdateOxygenProjSpecific(prevOxygen);
}
partial void UpdateOxygenProjSpecific(float prevOxygen);
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
public void SetVitality(float newVitality)
{
maxVitality = newVitality;
CalculateVitality();
}
public void CalculateVitality()
{
Vitality = MaxVitality;
if (Unkillable) { return; }
float damageResistanceMultiplier = 1f - GetResistance("damage");
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
string identifier = affliction.Prefab.Identifier.ToLowerInvariant();
string type = affliction.Prefab.AfflictionType.ToLowerInvariant();
if (limbHealth.VitalityMultipliers.ContainsKey(identifier))
{
vitalityDecrease *= limbHealth.VitalityMultipliers[identifier];
}
if (limbHealth.VitalityTypeMultipliers.ContainsKey(type))
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type];
}
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
}
foreach (Affliction affliction in afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
}
private void Kill()
{
if (Unkillable) { return; }
var causeOfDeath = GetCauseOfDeath();
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
#if CLIENT
DisplayVitalityDelay = 0.0f;
DisplayedVitality = Vitality;
#endif
}
public Pair<CauseOfDeathType, Affliction> GetCauseOfDeath()
{
List<Affliction> currentAfflictions = GetAllAfflictions(true);
Affliction strongestAffliction = null;
float largestStrength = 0.0f;
foreach (Affliction affliction in currentAfflictions)
{
if (strongestAffliction == null || affliction.GetVitalityDecrease(this) > largestStrength)
{
strongestAffliction = affliction;
largestStrength = affliction.GetVitalityDecrease(this);
}
}
CauseOfDeathType causeOfDeath = strongestAffliction == null ? CauseOfDeathType.Unknown : CauseOfDeathType.Affliction;
if (strongestAffliction == oxygenLowAffliction)
{
causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation;
}
return new Pair<CauseOfDeathType, Affliction>(causeOfDeath, strongestAffliction);
}
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
{
List<Affliction> allAfflictions = new List<Affliction>(afflictions);
foreach (LimbHealth limbHealth in limbHealths)
{
allAfflictions.AddRange(limbHealth.Afflictions);
}
if (mergeSameAfflictions)
{
List<Affliction> mergedAfflictions = new List<Affliction>();
foreach (Affliction affliction in allAfflictions)
{
var existingAffliction = mergedAfflictions.Find(a => a.Prefab == affliction.Prefab);
if (existingAffliction == null)
{
var newAffliction = affliction.Prefab.Instantiate(affliction.Strength);
if (affliction.Source != null) { newAffliction.Source = affliction.Source; }
newAffliction.DamagePerSecond = affliction.DamagePerSecond;
newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
mergedAfflictions.Add(newAffliction);
}
else
{
existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
existingAffliction.Strength += affliction.Strength;
}
}
return mergedAfflictions;
}
return allAfflictions;
}
/// <summary>
/// Get the identifiers of the items that can be used to treat the character. Takes into account all the afflictions the character has,
/// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating)
/// </summary>
/// <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="randomization">Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random)</param>
public void GetSuitableTreatments(Dictionary<string, float> treatmentSuitability, bool normalize, float randomization = 0.0f)
{
//key = item identifier
//float = suitability
treatmentSuitability.Clear();
float minSuitability = -10, maxSuitability = 10;
foreach (Affliction affliction in GetAllAfflictions())
{
foreach (KeyValuePair<string, float> treatment in affliction.Prefab.TreatmentSuitability)
{
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
treatmentSuitability[treatment.Key] = treatment.Value * affliction.Strength;
}
else
{
treatmentSuitability[treatment.Key] += treatment.Value * affliction.Strength;
}
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
}
}
//normalize the suitabilities to a range of 0 to 1
if (normalize)
{
foreach (string treatment in treatmentSuitability.Keys.ToList())
{
treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability);
treatmentSuitability[treatment] = MathHelper.Lerp(treatmentSuitability[treatment], Rand.Range(0.0f, 1.0f), randomization);
}
}
else
{
foreach (string treatment in treatmentSuitability.Keys.ToList())
{
treatmentSuitability[treatment] += Rand.Range(-100.0f, 100.0f) * randomization;
}
}
}
public void ServerWrite(IWriteMessage msg)
{
List<Affliction> activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold);
msg.Write((byte)activeAfflictions.Count);
foreach (Affliction affliction in activeAfflictions)
{
msg.Write(affliction.Prefab.UIntIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
}
List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction limbAffliction in limbHealth.Afflictions)
{
if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) continue;
limbAfflictions.Add(new Pair<LimbHealth, Affliction>(limbHealth, limbAffliction));
}
}
msg.Write((byte)limbAfflictions.Count);
foreach (var limbAffliction in limbAfflictions)
{
msg.WriteRangedInteger(limbHealths.IndexOf(limbAffliction.First), 0, limbHealths.Count - 1);
msg.Write(limbAffliction.Second.Prefab.UIntIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength),
0.0f, limbAffliction.Second.Prefab.MaxStrength, 8);
}
}
public void Remove()
{
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions) =>
afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
}
}
@@ -0,0 +1,139 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class DamageModifier : ISerializableEntity
{
public string Name => "Damage Modifier";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
[Serialize(1.0f, false), Editable(DecimalCount = 2)]
public float DamageMultiplier
{
get;
private set;
}
[Serialize("0.0,360", false), Editable]
public Vector2 ArmorSector
{
get;
private set;
}
public Vector2 ArmorSectorInRadians => new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
[Serialize(false, false), Editable]
public bool DeflectProjectiles
{
get;
private set;
}
[Serialize("", true), Editable]
public string AfflictionIdentifiers
{
get
{
return rawAfflictionIdentifierString;
}
private set
{
rawAfflictionIdentifierString = value;
ParseAfflictionIdentifiers();
}
}
[Serialize("", true), Editable]
public string AfflictionTypes
{
get
{
return rawAfflictionTypeString;
}
private set
{
rawAfflictionTypeString = value;
ParseAfflictionTypes();
}
}
private string rawAfflictionIdentifierString;
private string rawAfflictionTypeString;
private string[] parsedAfflictionIdentifiers;
private string[] parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName)
{
Deserialize(element);
if (element.Attribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
}
}
private void ParseAfflictionTypes()
{
string[] splitValue = rawAfflictionTypeString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
splitValue[i] = splitValue[i].ToLowerInvariant().Trim();
}
parsedAfflictionTypes = splitValue;
}
private void ParseAfflictionIdentifiers()
{
string[] splitValue = rawAfflictionIdentifierString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
splitValue[i] = splitValue[i].ToLowerInvariant().Trim();
}
parsedAfflictionIdentifiers = splitValue;
}
public bool MatchesAfflictionIdentifier(string 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));
}
public bool MatchesAfflictionType(string 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));
}
/// <summary>
/// Returns true if the type or the identifier matches the defined types/identifiers.
/// </summary>
public bool MatchesAffliction(string identifier, string 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));
}
public bool MatchesAffliction(Affliction affliction) => MatchesAffliction(affliction.Identifier, affliction.Prefab.AfflictionType);
public void Serialize(XElement element)
{
if (element == null) { return; }
SerializableProperty.SerializeProperties(this, element);
}
public void Deserialize(XElement element)
{
if (element == null) { return; }
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
}
@@ -0,0 +1,227 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class Job
{
private readonly JobPrefab prefab;
private Dictionary<string, Skill> skills;
public string Name
{
get { return prefab.Name; }
}
public string Description
{
get { return prefab.Description; }
}
public JobPrefab Prefab
{
get { return prefab; }
}
public List<Skill> Skills
{
get { return skills.Values.ToList(); }
}
public int Variant;
public Job(JobPrefab jobPrefab, int variant = 0)
{
prefab = jobPrefab;
Variant = variant;
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
skills.Add(skillPrefab.Identifier, new Skill(skillPrefab));
}
}
public Job(XElement element)
{
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
JobPrefab p = null;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
p = JobPrefab.Random();
}
else
{
p = JobPrefab.Prefabs[identifier];
}
prefab = p;
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
skillIdentifier,
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
}
}
public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
var prefab = JobPrefab.Random(randSync);
var variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, variant);
}
public float GetSkillLevel(string skillIdentifier)
{
skills.TryGetValue(skillIdentifier, out Skill skill);
return (skill == null) ? 0.0f : skill.Level;
}
public void IncreaseSkillLevel(string skillIdentifier, float increase)
{
if (skills.TryGetValue(skillIdentifier, out Skill skill))
{
skill.Level += increase;
}
else
{
skills.Add(
skillIdentifier,
new Skill(skillIdentifier, increase));
}
}
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
{
if (!prefab.ItemSets.TryGetValue(Variant, out var spawnItems)) { return; }
foreach (XElement itemElement in spawnItems.GetChildElements("Item"))
{
InitializeJobItem(character, itemElement, spawnPoint);
}
}
private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null)
{
ItemPrefab itemPrefab;
if (itemElement.Attribute("name") != null)
{
string itemName = itemElement.Attribute("name").Value;
DebugConsole.ThrowError("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items.");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
return;
}
}
else
{
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
return;
}
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
}
#endif
if (itemElement.GetAttributeBool("equip", false))
{
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
{
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
Wearable wearable = ((List<ItemComponent>)item.Components)?.Find(c => c is Wearable) as Wearable;
if (wearable != null)
{
if (Variant > 0 && Variant <= wearable.Variants)
{
wearable.Variant = Variant;
}
else
{
wearable.Variant = wearable.Variant; //force server event
if (wearable.Variants > 0 && Variant == 0)
{
//set variant to the same as the wearable to get the rest of the character's gear
//to use the same variant (if possible)
Variant = wearable.Variant;
}
}
}
if (item.Prefab.Identifier == "idcard" && spawnPoint != null)
{
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
item.AddTag("name:" + character.Name);
item.AddTag("job:" + Name);
if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc))
item.Description = spawnPoint.IdCardDesc;
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null) parentItem.Combine(item, user: null);
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeJobItem(character, childItemElement, spawnPoint, item);
}
}
public XElement Save(XElement parentElement)
{
XElement jobElement = new XElement("job");
jobElement.Add(new XAttribute("name", Name));
jobElement.Add(new XAttribute("identifier", prefab.Identifier));
foreach (KeyValuePair<string, Skill> skill in skills)
{
jobElement.Add(new XElement("skill", new XAttribute("identifier", skill.Value.Identifier), new XAttribute("level", skill.Value.Level)));
}
parentElement.Add(jobElement);
return jobElement;
}
}
}
@@ -0,0 +1,292 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System;
using System.Linq;
namespace Barotrauma
{
public class AutonomousObjective
{
public string identifier;
public string option;
public float priorityModifier;
public AutonomousObjective(XElement element)
{
identifier = element.GetAttributeString("identifier", null);
//backwards compatibility
if (string.IsNullOrEmpty(identifier))
{
identifier = element.GetAttributeString("aitag", null);
}
option = element.GetAttributeString("option", null);
priorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
priorityModifier = MathHelper.Max(priorityModifier, 0);
}
}
partial class JobPrefab : IPrefab, IDisposable
{
public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public static XElement 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];
}
else
{
DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier);
return null;
}
}
public readonly Dictionary<int, XElement> ItemSets = new Dictionary<int, XElement>();
public readonly Dictionary<int, List<string>> ItemIdentifiers = new Dictionary<int, List<string>>();
public readonly Dictionary<int, Dictionary<string, bool>> ShowItemPreview = new Dictionary<int, Dictionary<string, bool>>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutomaticOrders = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
[Serialize("1,1,1,1", false)]
public Color UIColor
{
get;
private set;
}
[Serialize("notfound", false)]
public string Identifier
{
get;
private set;
}
[Serialize("notfound", false)]
public string Name
{
get;
private set;
}
public string OriginalName { get { return Identifier; } }
public ContentPackage ContentPackage { get; private set; }
[Serialize("", false)]
public string Description
{
get;
private set;
}
[Serialize(false, false)]
public bool OnlyJobSpecificDialog
{
get;
private set;
}
//the number of these characters in the crew the player starts with in the single player campaign
[Serialize(0, false)]
public int InitialCount
{
get;
private set;
}
//if set to true, a client that has chosen this as their preferred job will get it no matter what
[Serialize(false, false)]
public bool AllowAlways
{
get;
private set;
}
//how many crew members can have the job (only one captain etc)
[Serialize(100, false)]
public int MaxNumber
{
get;
private set;
}
//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)]
public int MinNumber
{
get;
private set;
}
[Serialize(0.0f, false)]
public float MinKarma
{
get;
private set;
}
[Serialize(10.0f, false)]
public float Commonness
{
get;
private set;
}
//how much the vitality of the character is increased/reduced from the default value
[Serialize(0.0f, false)]
public float VitalityModifier
{
get;
private set;
}
public Sprite Icon;
public string FilePath { get; private set; }
public XElement Element { get; private set; }
public XElement ClothingElement { get; private set; }
public int Variants { get; private set; }
public JobPrefab(XElement element, string filePath)
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier);
Identifier = Identifier.ToLowerInvariant();
Element = element;
int variant = 0;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "itemset":
ItemSets.Add(variant, subElement);
ItemIdentifiers[variant] = new List<string>();
ShowItemPreview[variant] = new Dictionary<string, bool>();
loadItemIdentifiers(subElement, variant);
variant++;
break;
case "skills":
foreach (XElement skillElement in subElement.Elements())
{
Skills.Add(new SkillPrefab(skillElement));
}
break;
case "autonomousobjectives":
subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order)));
break;
case "appropriateobjectives":
case "appropriateorders":
subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant()));
break;
case "jobicon":
Icon = new Sprite(subElement.FirstElement());
break;
}
}
void loadItemIdentifiers(XElement parentElement, int variant)
{
foreach (XElement itemElement in parentElement.GetChildElements("Item"))
{
if (itemElement.Element("name") != null)
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.");
continue;
}
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
if (string.IsNullOrWhiteSpace(itemIdentifier))
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier.");
}
else
{
ItemIdentifiers[variant].Add(itemIdentifier);
ShowItemPreview[variant][itemIdentifier] = itemElement.GetAttributeBool("showpreview", true);
}
loadItemIdentifiers(itemElement, variant);
}
}
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
ClothingElement = element.GetChildElement("PortraitClothing");
}
public static JobPrefab 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)
{
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.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; }
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement(), file.Path)
{
ContentPackage = file.ContentPackage
};
Prefabs.Add(job, true);
}
else
{
var job = new JobPrefab(element, file.Path)
{
ContentPackage = file.ContentPackage
};
Prefabs.Add(job, false);
}
}
NoJobElement = NoJobElement ?? mainElement.Element("NoJob");
NoJobElement = NoJobElement ?? mainElement.Element("nojob");
}
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
}
}
@@ -0,0 +1,54 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
class Skill
{
private SkillPrefab prefab;
private float level;
static string[] levelNames = new string[] {
"Untrained", "Incompetent", "Novice",
"Adequate", "Competent", "Proficient",
"Professional", "Master", "Legendary" };
string identifier;
public string Identifier
{
get { return identifier; }
}
public float Level
{
get { return level; }
set { level = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public Skill(SkillPrefab prefab)
{
this.prefab = prefab;
this.identifier = prefab.Identifier;
this.level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
}
public Skill(string identifier, float level)
{
this.identifier = identifier;
this.level = level;
}
/// <summary>
/// returns the "name" of some skill level (0-10 -> untrained, etc)
/// </summary>
public static string GetLevelName(float level)
{
level = MathHelper.Clamp(level, 0.0f, 100.0f);
int scaledLevel = (int)Math.Floor((level / 100.0f) * levelNames.Length);
return levelNames[Math.Min(scaledLevel, levelNames.Length - 1)];
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma
{
class SkillPrefab
{
public readonly string Identifier;
public Vector2 LevelRange { get; private set; }
public SkillPrefab(XElement element)
{
Identifier = element.GetAttributeString("identifier", "");
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
LevelRange = XMLExtensions.ParseVector2(levelString, false);
}
else
{
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
LevelRange = new Vector2(skillLevel, skillLevel);
}
}
}
}
@@ -0,0 +1,765 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
using JointParams = Barotrauma.RagdollParams.JointParams;
namespace Barotrauma
{
public enum LimbType
{
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist
};
partial class LimbJoint : RevoluteJoint
{
public bool IsSevered;
public bool CanBeSevered => Params.CanBeSevered;
public readonly JointParams Params;
public readonly Ragdoll ragdoll;
public readonly Limb LimbA, LimbB;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One)
{
Params = jointParams;
this.ragdoll = ragdoll;
LoadParams();
}
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2)
: base(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
{
CollideConnected = false;
MotorEnabled = true;
MaxMotorTorque = 0.25f;
LimbA = limbA;
LimbB = limbB;
}
public void LoadParams()
{
MaxMotorTorque = Params.Stiffness;
LimitEnabled = Params.LimitEnabled;
if (float.IsNaN(Params.LowerLimit))
{
Params.LowerLimit = 0;
}
if (float.IsNaN(Params.UpperLimit))
{
Params.UpperLimit = 0;
}
if (ragdoll.IsFlipped)
{
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Params.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Params.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
}
else
{
LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Params.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Params.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
}
}
}
partial class Limb : ISerializableEntity, ISpatialEntity
{
//how long it takes for severed limbs to fade out
private const float SeveredFadeOutTime = 10.0f;
public readonly Character character;
/// <summary>
/// Note that during the limb initialization, character.AnimController returns null, whereas this field is already assigned.
/// </summary>
public readonly Ragdoll ragdoll;
public readonly LimbParams Params;
//the physics body of the limb
public PhysicsBody body;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public bool inWater;
private readonly FixedMouseJoint pullJoint;
public readonly LimbType type;
public readonly bool ignoreCollisions;
private bool isSevered;
private float severedFadeOutTimer;
private Vector2? mouthPos;
public Vector2 MouthPos
{
get
{
if (!mouthPos.HasValue)
{
mouthPos = Params.MouthPos;
}
return mouthPos.Value;
}
set
{
mouthPos = value;
}
}
public readonly Attack attack;
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
private Direction dir;
public int HealthIndex => Params.HealthIndex;
public float Scale => Params.Ragdoll.LimbScale;
public float AttackPriority => Params.AttackPriority;
public bool DoesFlip => Params.Flip;
public float SteerForce => Params.SteerForce;
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
public bool IsSevered
{
get { return isSevered; }
set
{
if (isSevered == value) { return; }
isSevered = value;
if (isSevered)
{
ragdoll.SubtractMass(this);
}
if (!isSevered) severedFadeOutTimer = 0.0f;
#if CLIENT
if (isSevered) damageOverlayStrength = 100.0f;
#endif
}
}
public Submarine Submarine => character.Submarine;
public Vector2 WorldPosition
{
get { return character.Submarine == null ? Position : Position + character.Submarine.Position; }
}
public Vector2 Position
{
get { return ConvertUnits.ToDisplayUnits(body.SimPosition); }
}
public Vector2 SimPosition
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return Vector2.Zero;
}
return body.SimPosition;
}
}
public float Rotation
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return 0.0f;
}
return body.Rotation;
}
}
//where an animcontroller is trying to pull the limb, only used for debug visualization
public Vector2 AnimTargetPos { get; private set; }
public float Mass
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return 1.0f;
}
return body.Mass;
}
}
public bool Disabled { get; set; }
public Vector2 LinearVelocity
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return Vector2.Zero;
}
return body.LinearVelocity;
}
}
public float Dir
{
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
}
public int RefJointIndex => Params.RefJoint;
private List<WearableSprite> wearingItems;
public List<WearableSprite> WearingItems
{
get { return wearingItems; }
}
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>();
public bool PullJointEnabled
{
get { return pullJoint.Enabled; }
set { pullJoint.Enabled = value; }
}
public float PullJointMaxForce
{
get { return pullJoint.MaxForce; }
set { pullJoint.MaxForce = value; }
}
public Vector2 PullJointWorldAnchorA
{
get { return pullJoint.WorldAnchorA; }
set
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
if (Vector2.DistanceSquared(SimPosition, value) > 50.0f * 50.0f)
{
Vector2 diff = value - SimPosition;
string errorMsg = "Attempted to move the anchor A of a limb's pull joint extremely far from the limb (diff: " + diff +
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
pullJoint.WorldAnchorA = value;
}
}
public Vector2 PullJointWorldAnchorB
{
get { return pullJoint.WorldAnchorB; }
set
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
if (Vector2.DistanceSquared(pullJoint.WorldAnchorA, value) > 50.0f * 50.0f)
{
Vector2 diff = value - pullJoint.WorldAnchorA;
string errorMsg = "Attempted to move the anchor B of a limb's pull joint extremely far from the limb (diff: " + diff +
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
pullJoint.WorldAnchorB = value;
}
}
public Vector2 PullJointLocalAnchorA
{
get { return pullJoint.LocalAnchorA; }
}
public bool Removed
{
get;
private set;
}
public string Name => Params.Name;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
this.character = character;
this.Params = limbParams;
wearingItems = new List<WearableSprite>();
dir = Direction.Right;
body = new PhysicsBody(limbParams);
type = limbParams.Type;
if (limbParams.IgnoreCollisions)
{
body.CollisionCategories = Category.None;
body.CollidesWith = Category.None;
ignoreCollisions = true;
}
else
{
//limbs don't collide with each other
body.CollisionCategories = Physics.CollisionCharacter;
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
}
body.UserData = this;
pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale))
{
Enabled = false,
//MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass
// 150 or even 400 is too low if the joint is used for moving the character position from the mainlimb towards the collider position
MaxForce = 1000 * Mass
};
GameMain.World.Add(pullJoint);
var element = limbParams.Element;
body.BodyType = BodyType.Dynamic;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "attack":
attack = new Attack(subElement, (character == null ? "null" : character.Name) + ", limb " + type);
if (attack.DamageRange <= 0)
{
switch (body.BodyShape)
{
case PhysicsBody.Shape.Circle:
attack.DamageRange = body.radius;
break;
case PhysicsBody.Shape.Capsule:
attack.DamageRange = body.height / 2 + body.radius;
break;
case PhysicsBody.Shape.Rectangle:
attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length();
break;
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
break;
case "damagemodifier":
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
}
}
SerializableProperties = SerializableProperty.GetProperties(this);
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public void MoveToPos(Vector2 pos, float force, bool pullFromCenter = false)
{
Vector2 pullPos = body.SimPosition;
if (!pullFromCenter)
{
pullPos = pullJoint.WorldAnchorA;
}
AnimTargetPos = pos;
body.MoveToPos(pos, force, pullPos);
}
public void MirrorPullJoint()
{
pullJoint.LocalAnchorA = new Vector2(-pullJoint.LocalAnchorA.X, pullJoint.LocalAnchorA.Y);
}
public AttackResult AddDamage(Vector2 simPosition, float damage, float bleedingDamage, float burnDamage, bool playSound)
{
List<Affliction> afflictions = new List<Affliction>();
if (damage > 0.0f) afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage));
if (bleedingDamage > 0.0f) afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage));
if (burnDamage > 0.0f) afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage));
return AddDamage(simPosition, afflictions, playSound);
}
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound)
{
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList();
for (int i = 0; i < afflictionsCopy.Count; i++)
{
foreach (DamageModifier damageModifier in DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in wearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
}
}
AddDamageProjSpecific(simPosition, afflictionsCopy, playSound, appliedDamageModifiers);
return new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
}
partial void AddDamageProjSpecific(Vector2 simPosition, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
if (armorSector == Vector2.Zero) { return false; }
//sector 360 degrees or more -> always hits
if (Math.Abs(armorSector.Y - armorSector.X) >= MathHelper.TwoPi) { return true; }
float rotation = body.TransformedRotation;
float offset = (MathHelper.PiOver2 - MathUtils.GetMidAngle(armorSector.X, armorSector.Y)) * Dir;
float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition);
float sectorSize = GetArmorSectorSize(armorSector);
return hitAngle < sectorSize / 2;
}
protected float GetArmorSectorSize(Vector2 armorSector)
{
return Math.Abs(armorSector.X - armorSector.Y);
}
public void Update(float deltaTime)
{
UpdateProjSpecific(deltaTime);
if (inWater)
{
body.ApplyWaterForces();
}
if (isSevered)
{
severedFadeOutTimer += deltaTime;
if (severedFadeOutTimer >= SeveredFadeOutTime)
{
body.Enabled = false;
}
else if (character.CurrentHull == null && Hull.FindHull(WorldPosition) != null)
{
severedFadeOutTimer = SeveredFadeOutTime;
}
}
if (attack != null)
{
attack.UpdateCoolDown(deltaTime);
}
}
partial void UpdateProjSpecific(float deltaTime);
private readonly List<Body> contactBodies = new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary>
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default(AttackResult);
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime);
bool wasHit = false;
Body structureBody = null;
if (damageTarget != null)
{
switch (attack.HitDetectionType)
{
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
structureBody = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
// Ignore blocking checks on doors, because it causes cases where a Mudraptor cannot hit the hatch, for example.
wasHit = true;
}
else if (damageTarget is Structure wall && structureBody != null &&
(structureBody.UserData is Structure || (structureBody.UserData is Submarine sub && sub == wall.Submarine)))
{
// If the attack is aimed to a structure (wall) and hits a structure or the sub, it's successful
wasHit = true;
}
else
{
// If there is nothing between, the hit is successful
wasHit = structureBody == null;
}
}
break;
case HitDetection.Contact:
contactBodies.Clear();
if (damageTarget is Character targetCharacter)
{
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (!limb.IsSevered && limb.body?.FarseerBody != null) contactBodies.Add(limb.body.FarseerBody);
}
}
else if (damageTarget is Structure targetStructure)
{
if (character.Submarine == null && targetStructure.Submarine != null)
{
contactBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody);
}
else
{
contactBodies.AddRange(targetStructure.Bodies);
}
}
else if (damageTarget is Item)
{
Item targetItem = damageTarget as Item;
if (targetItem.body?.FarseerBody != null) contactBodies.Add(targetItem.body.FarseerBody);
}
ContactEdge contactEdge = body.FarseerBody.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.IsTouching &&
contactBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body))
{
structureBody = contactBodies.LastOrDefault();
wasHit = true;
break;
}
contactEdge = contactEdge.Next;
}
break;
}
}
if (wasHit)
{
wasHit = damageTarget != null;
}
if (wasHit)
{
bool playSound = false;
#if CLIENT
playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval;
if (playSound)
{
LastAttackSoundTime = SoundInterval;
}
#endif
if (damageTarget is Character targetCharacter && targetLimb != null)
{
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound);
}
else
{
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound);
}
if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
{
// TODO: use the hit pos?
var localFront = body.GetLocalFront(Params.GetSpriteOrientation());
var from = body.FarseerBody.GetWorldPoint(localFront);
var to = from;
var drawPos = body.DrawPosition;
StickTo(structureBody, from, to);
}
attack.ResetAttackTimer();
attack.SetCoolDown();
}
Vector2 diff = attackSimPos - SimPosition;
bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f;
if (applyForces)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
Limb limb = character.AnimController.Limbs[limbIndex];
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque);
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
else
{
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
body.ApplyLinearImpulse(
Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition),
forcePos,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
return wasHit;
}
private WeldJoint attachJoint;
private WeldJoint colliderJoint;
public bool IsStuck => attachJoint != null;
/// <summary>
/// Attach the limb to a target with WeldJoints.
/// Uses sim units.
/// </summary>
private void StickTo(Body target, Vector2 from, Vector2 to)
{
if (attachJoint != null)
{
// Already attached to the target body, no need to do anything
if (attachJoint.BodyB == target) { return; }
Release();
}
if (!ragdoll.IsStuck)
{
PhysicsBody mainLimbBody = ragdoll.MainLimb.body;
Body colliderBody = ragdoll.Collider.FarseerBody;
Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(ragdoll.MainLimb.Params.GetSpriteOrientation());
if (Dir < 0)
{
mainLimbLocalFront.X = -mainLimbLocalFront.X;
}
Vector2 mainLimbFront = mainLimbBody.FarseerBody.GetWorldPoint(mainLimbLocalFront);
colliderBody.SetTransform(mainLimbBody.SimPosition, mainLimbBody.Rotation);
// Attach the collider to the main body so that they don't go out of sync (TODO: why is the collider still rotated 90d off?)
colliderJoint = new WeldJoint(colliderBody, mainLimbBody.FarseerBody, mainLimbFront, mainLimbFront, true)
{
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.Add(colliderJoint);
}
attachJoint = new WeldJoint(body.FarseerBody, target, from, to, true)
{
FrequencyHz = 1,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.Add(attachJoint);
}
public void Release()
{
if (!IsStuck) { return; }
GameMain.World.Remove(attachJoint);
attachJoint = null;
if (colliderJoint != null)
{
GameMain.World.Remove(colliderJoint);
colliderJoint = null;
}
}
public void Remove()
{
body?.Remove();
body = null;
Release();
RemoveProjSpecific();
Removed = true;
}
partial void RemoveProjSpecific();
public void LoadParams()
{
pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.PullPos * Scale);
LoadParamsProjSpecific();
}
partial void LoadParamsProjSpecific();
}
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
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 List<string> AllowedDialogTags;
private float commonness;
public float Commonness
{
get { return commonness; }
}
public NPCPersonalityTrait(XElement element, string filePath)
{
FilePath = filePath;
Name = element.GetAttributeString("name", "");
AllowedDialogTags = new List<string>(element.GetAttributeStringArray("alloweddialogtags", new string[0]));
commonness = element.GetAttributeFloat("commonness", 1.0f);
list.Add(this);
}
public static NPCPersonalityTrait GetRandom(string seed)
{
var rand = new MTRandom(ToolBox.StringToInt(seed));
return ToolBox.SelectWeightedRandom(list, list.Select(t => t.commonness).ToList(), rand);
}
}
}
@@ -0,0 +1,435 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
public enum AnimationType
{
NotDefined,
Walk,
Run,
SwimSlow,
SwimFast
}
abstract class GroundedMovementParams : AnimationParams
{
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2)]
public Vector2 StepSize
{
get;
set;
}
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2)]
public float HeadPosition { get; set; }
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2)]
public float TorsoPosition { get; set; }
[Serialize(1f, true, 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)]
public float StepLiftAmount { 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)]
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)]
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)]
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 = 500)]
public float SteerTorque { get; set; }
}
abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
{
public string SpeciesName { get; private set; }
public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run;
public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast;
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED)]
public float MovementSpeed { get; set; }
[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)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float CycleSpeed { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
public float HeadAngle
{
get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians);
set
{
if (!float.IsNaN(value))
{
HeadAngleInRadians = MathHelper.ToRadians(value);
}
}
}
public float HeadAngleInRadians { get; private set; } = float.NaN;
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
public float TorsoAngle
{
get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians);
set
{
if (!float.IsNaN(value))
{
TorsoAngleInRadians = MathHelper.ToRadians(value);
}
}
}
public float TorsoAngleInRadians { get; private set; } = float.NaN;
[Serialize(AnimationType.NotDefined, true), Editable]
public virtual AnimationType AnimationType { get; protected set; }
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType.ToString()}";
public static string GetDefaultFile(string speciesName, AnimationType animType, ContentPackage contentPackage = null)
=> Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName, animType)}.xml");
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab?.XDocument == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
return string.Empty;
}
return GetFolder(prefab.XDocument, prefab.FilePath);
}
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations");
}
return folder;
}
/// <summary>
/// Selects a random filepath from multiple paths, matching the specified animation type.
/// </summary>
public static string GetRandomFilePath(IEnumerable<string> filePaths, AnimationType type)
{
return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.Server);
}
/// <summary>
/// Selects all file paths that match the specified animation type.
/// </summary>
public static IEnumerable<string> FilterFilesByType(IEnumerable<string> filePaths, AnimationType type)
{
return filePaths.Where(f => AnimationPredicate(f, type));
}
private static bool AnimationPredicate(string filePath, AnimationType type)
{
var doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return false; }
var typeString = doc.Root.GetAttributeString("animationtype", null);
if (string.IsNullOrWhiteSpace(typeString))
{
typeString = doc.Root.GetAttributeString("AnimationType", "NotDefined");
}
return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type;
}
public static T GetDefaultAnimParams<T>(string speciesName, AnimationType animType) where T : AnimationParams, new() => GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
/// <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 GetAnimParams<T>(string speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new()
{
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
if (fileName == null || !anims.TryGetValue(fileName, out AnimationParams anim))
{
string selectedFile = null;
string folder = GetFolder(speciesName);
if (Directory.Exists(folder))
{
var files = Directory.GetFiles(folder);
if (files.None())
{
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
var filteredFiles = FilterFilesByType(files, animType);
if (filteredFiles.None())
{
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified.
selectedFile = GetDefaultFile(speciesName, animType);
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => 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.");
selectedFile = GetDefaultFile(speciesName, animType);
}
}
}
else
{
DebugConsole.ThrowError($"[Animationparams] Invalid directory: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
if (selectedFile == null)
{
throw new Exception("[AnimationParams] Selected file null!");
}
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
T a = new T();
if (a.Load(selectedFile, speciesName))
{
fileName = Path.GetFileNameWithoutExtension(selectedFile);
if (!anims.ContainsKey(fileName))
{
anims.Add(fileName, a);
}
}
else
{
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}");
}
return a;
}
return (T)anim;
}
public static void ClearCache() => allAnimations.Clear();
public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type)
{
if (type == typeof(HumanWalkParams))
{
return Create<HumanWalkParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanRunParams))
{
return Create<HumanRunParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanSwimSlowParams))
{
return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanSwimFastParams))
{
return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishWalkParams))
{
return Create<FishWalkParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishRunParams))
{
return Create<FishRunParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishSwimSlowParams))
{
return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishSwimFastParams))
{
return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
}
throw new NotImplementedException(type.ToString());
}
/// <summary>
/// Note: Overrides old animations, if found!
/// </summary>
public static T Create<T>(string fullPath, string speciesName, AnimationType animationType) where T : AnimationParams, new()
{
if (animationType == AnimationType.NotDefined)
{
throw new Exception("Cannot create an animation file of type " + animationType.ToString());
}
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
var fileName = Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName))
{
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
anims.Remove(fileName);
}
var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
instance.doc = new XDocument(animationElement);
instance.UpdatePath(fullPath);
instance.IsLoaded = instance.Deserialize(animationElement);
instance.Save();
instance.Load(fullPath, speciesName);
anims.Add(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance as T;
}
public bool Serialize() => base.Serialize();
public bool Deserialize() => base.Deserialize();
protected bool Load(string file, string speciesName)
{
if (Load(file))
{
SpeciesName = speciesName;
return true;
}
return false;
}
protected override void UpdatePath(string newPath)
{
if (SpeciesName == null)
{
base.UpdatePath(newPath);
}
else
{
// Update the key by removing and re-adding the animation.
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
{
animations.Remove(Name);
}
base.UpdatePath(newPath);
if (animations != null)
{
if (!animations.ContainsKey(Name))
{
animations.Add(Name, this);
}
}
}
}
protected static string ParseFootAngles(Dictionary<int, float> footAngles)
{
//convert to the format "id1:angle,id2:angle,id3:angle"
return string.Join(",", footAngles.Select(kv => kv.Key + ": " + kv.Value.ToString("G", CultureInfo.InvariantCulture)).ToArray());
}
protected static void SetFootAngles(Dictionary<int, float> footAngles, string value)
{
footAngles.Clear();
if (string.IsNullOrEmpty(value))
{
return;
}
string[] keyValuePairs = value.Split(',');
foreach (string joinedKvp in keyValuePairs)
{
string[] keyValuePair = joinedKvp.Split(':');
if (keyValuePair.Length != 2 ||
!int.TryParse(keyValuePair[0].Trim(), out int limbIndex) ||
!float.TryParse(keyValuePair[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float angle))
{
DebugConsole.ThrowError("Failed to parse foot angles (" + value + ")");
continue;
}
footAngles[limbIndex] = angle;
}
}
public static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid)
{
if (isHumanoid)
{
switch (type)
{
case AnimationType.Walk:
return typeof(HumanWalkParams);
case AnimationType.Run:
return typeof(HumanRunParams);
case AnimationType.SwimSlow:
return typeof(HumanSwimSlowParams);
case AnimationType.SwimFast:
return typeof(HumanSwimFastParams);
default:
throw new NotImplementedException(type.ToString());
}
}
else
{
switch (type)
{
case AnimationType.Walk:
return typeof(FishWalkParams);
case AnimationType.Run:
return typeof(FishRunParams);
case AnimationType.SwimSlow:
return typeof(FishSwimSlowParams);
case AnimationType.SwimFast:
return typeof(FishSwimFastParams);
default:
throw new NotImplementedException(type.ToString());
}
}
}
#region Memento
public Memento<AnimationParams> Memento { get; protected set; } = new Memento<AnimationParams>();
public abstract void StoreSnapshot();
protected void StoreSnapshot<T>() where T : AnimationParams, new()
{
if (doc == null)
{
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!");
return;
}
Serialize();
var copy = new T
{
IsLoaded = true,
doc = new XDocument(doc)
};
copy.Deserialize();
copy.Serialize();
Memento.Store(copy);
}
public void Undo() => Deserialize(Memento.Undo().MainElement);
public void Redo() => Deserialize(Memento.Redo().MainElement);
public void ClearHistory() => Memento.Clear();
#endregion
}
}
@@ -0,0 +1,218 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
class FishWalkParams : FishGroundedParams
{
public static FishWalkParams GetDefaultAnimParams(Character character)
{
return Check(character) ? GetDefaultAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk) : Empty;
}
public static FishWalkParams GetAnimParams(Character character, string fileName = null)
{
return Check(character) ? GetAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk, fileName) : Empty;
}
protected static FishWalkParams Empty = new FishWalkParams();
public override void StoreSnapshot() => StoreSnapshot<FishWalkParams>();
}
class FishRunParams : FishGroundedParams
{
public static FishRunParams GetDefaultAnimParams(Character character)
{
return Check(character) ? GetDefaultAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run) : Empty;
}
public static FishRunParams GetAnimParams(Character character, string fileName = null)
{
return Check(character) ? GetAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run, fileName) : Empty;
}
protected static FishRunParams Empty = new FishRunParams();
public override void StoreSnapshot() => StoreSnapshot<FishRunParams>();
}
class FishSwimFastParams : FishSwimParams
{
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast);
public static FishSwimFastParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimFastParams>();
}
class FishSwimSlowParams : FishSwimParams
{
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow);
public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimSlowParams>();
}
abstract class FishGroundedParams : GroundedMovementParams, IFishAnimation
{
protected static bool Check(Character character)
{
if (!character.AnimController.CanWalk)
{
DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!");
return false;
}
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.")]
public bool Flip { 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)]
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)]
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)]
public float FootMoveForce { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
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 = 500)]
public float TorsoTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TailTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float FootTorque { get; set; }
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
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)]
public float ColliderStandAngle
{
get => MathHelper.ToDegrees(ColliderStandAngleInRadians);
set => ColliderStandAngleInRadians = MathHelper.ToRadians(value);
}
public float ColliderStandAngleInRadians { get; private set; }
[Serialize(null, true), Editable]
public string FootAngles
{
get => ParseFootAngles(FootAnglesInRadians);
set => SetFootAngles(FootAnglesInRadians, value);
}
/// <summary>
/// Key = limb id, value = angle in radians
/// </summary>
public Dictionary<int, float> FootAnglesInRadians { get; set; } = new Dictionary<int, float>();
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
public float TailAngle
{
get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians);
set
{
if (!float.IsNaN(value))
{
TailAngleInRadians = MathHelper.ToRadians(value);
}
}
}
public float TailAngleInRadians { get; private set; } = float.NaN;
}
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]
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.")]
public bool Flip { 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.")]
public bool Mirror { get; set; }
[Serialize(5f, true), Editable]
public float WaveAmplitude { get; set; }
[Serialize(10.0f, true), Editable]
public float WaveLength { get; set; }
[Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")]
public bool RotateTowardsMovement { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
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)]
public float TailTorqueMultiplier { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float FootTorque { get; set; }
[Serialize(null, true), Editable]
public string FootAngles
{
get => ParseFootAngles(FootAnglesInRadians);
set => SetFootAngles(FootAnglesInRadians, value);
}
/// <summary>
/// Key = limb id, value = angle in radians
/// </summary>
public Dictionary<int, float> FootAnglesInRadians { get; set; } = new Dictionary<int, float>();
/// <summary>
/// In degrees.
/// </summary>
[Serialize(float.NaN, true), Editable(-360f, 360f)]
public float TailAngle
{
get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians);
set
{
if (!float.IsNaN(value))
{
TailAngleInRadians = MathHelper.ToRadians(value);
}
}
}
public float TailAngleInRadians { get; private set; } = float.NaN;
}
interface IFishAnimation
{
bool Flip { get; set; }
string FootAngles { get; set; }
Dictionary<int, float> FootAnglesInRadians { get; set; }
float TailAngle { get; set; }
float TailAngleInRadians { get; }
float HeadTorque { get; set; }
float TorsoTorque { get; set; }
float TailTorque { get; set; }
float FootTorque { get; set; }
}
}
@@ -0,0 +1,169 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class HumanWalkParams : HumanGroundedParams
{
public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk);
public static HumanWalkParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<HumanWalkParams>();
}
class HumanRunParams : HumanGroundedParams
{
public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run);
public static HumanRunParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<HumanRunParams>();
}
class HumanSwimFastParams: HumanSwimParams
{
public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast);
public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<HumanSwimFastParams>();
}
class HumanSwimSlowParams : HumanSwimParams
{
public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow);
public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<HumanSwimSlowParams>();
}
abstract class HumanSwimParams : SwimParams, IHumanAnimation
{
[Serialize(0.5f, true), Editable(DecimalCount = 2)]
public float LegMoveAmount { get; set; }
[Serialize(5.0f, true), Editable]
public float LegCycleLength { get; set; }
[Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize(0.5f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(5.0f, true), Editable]
public float HandCycleSpeed { get; set; }
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0.0f, true), Editable(-360f, 360f)]
public float FootAngle
{
get => MathHelper.ToDegrees(FootAngleInRadians);
set
{
FootAngleInRadians = MathHelper.ToRadians(value);
}
}
public float FootAngleInRadians { get; private set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootRotateStrength { 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)]
public float GetUpForce { get; set; }
// -- TODO: use a separate clip for crawling -> replace these when implemented.
[Serialize(0.65f, true, description: "Height of the torso when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)]
public float CrouchingTorsoPos { get; set; }
[Serialize(0.65f, true, description: "Height of the head when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)]
public float CrouchingHeadPos { get; set; }
/// <summary>
/// In degrees
/// </summary>
[Serialize(-10f, true, description: "Angle of the torso when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
public float CrouchingTorsoAngle { get; set; }
/// <summary>
/// In degrees
/// </summary>
[Serialize(-10f, true, description: "Angle of the head when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
public float CrouchingHeadAngle { get; set; }
// --
[Serialize(0.25f, true, 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)]
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)]
public float FootMoveStrength { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0.0f, true), Editable(-360f, 360f)]
public float FootAngle
{
get => MathHelper.ToDegrees(FootAngleInRadians);
set
{
FootAngleInRadians = MathHelper.ToRadians(value);
}
}
public float FootAngleInRadians { get; private set; }
[Serialize(20.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootRotateStrength { get; 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)]
public Vector2 FootMoveOffset { get; 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)]
public Vector2 CrouchingFootMoveOffset { 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)]
public float LegBendTorque { get; set; }
[Serialize("0.4, 0.15", true, 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)]
public Vector2 HandMoveOffset { get; set; }
[Serialize(0.7f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)]
public float HandMoveStrength { 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)]
public float HandClampY { get; set; }
}
public interface IHumanAnimation
{
float FootAngle { get; set; }
float FootAngleInRadians { get; }
float FootRotateStrength { get; set; }
}
}
@@ -0,0 +1,632 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml;
using System.Linq;
using Barotrauma.Extensions;
#if CLIENT
using SoundType = Barotrauma.CharacterSound.SoundType;
#endif
namespace Barotrauma
{
/// <summary>
/// Contains character data that should be editable in the character editor.
/// </summary>
class CharacterParams : EditableParams
{
[Serialize("", true), Editable]
public string SpeciesName { 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]
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(false, true), Editable]
public bool Humanoid { get; private set; }
[Serialize(false, true), Editable]
public bool Husk { get; private set; }
[Serialize(false, true), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, true), Editable]
public bool CanSpeak { get; set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Noise { get; set; }
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Visibility { get; set; }
[Serialize("blood", true), Editable]
public string BloodDecal { get; private 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)]
public float EatingSpeed { 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)]
public float PathFinderPriority { get; set; }
public readonly string File;
public readonly List<SubParam> SubParams = new List<SubParam>();
public readonly List<SoundParams> Sounds = new List<SoundParams>();
public readonly List<ParticleParams> BloodEmitters = new List<ParticleParams>();
public readonly List<ParticleParams> GibEmitters = new List<ParticleParams>();
public readonly List<ParticleParams> DamageEmitters = new List<ParticleParams>();
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
public HealthParams Health { get; private set; }
public AIParams AI { get; private set; }
public CharacterParams(string file)
{
File = file;
Load();
}
protected override string GetName() => "Character Config File";
public override XElement MainElement => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
public bool Load()
{
bool success = base.Load(File);
if (string.IsNullOrEmpty(SpeciesName) && MainElement != null)
{
//backwards compatibility
SpeciesName = MainElement.GetAttributeString("name", "");
}
CreateSubParams();
return success;
}
public bool Save(string fileNameWithoutExtension = null)
{
Serialize();
return base.Save(fileNameWithoutExtension, new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineOnAttributes = false
});
}
public override bool Reset(bool forceReload = false)
{
if (forceReload)
{
return Load();
}
Deserialize(OriginalElement, alsoChildren: true);
SubParams.ForEach(sp => sp.Reset());
return true;
}
public bool CompareGroup(string group) => !string.IsNullOrWhiteSpace(group) && !string.IsNullOrWhiteSpace(Group) && group.Equals(Group, StringComparison.OrdinalIgnoreCase);
protected void CreateSubParams()
{
SubParams.Clear();
var health = MainElement.GetChildElement("health");
if (health != null)
{
Health = new HealthParams(health, this);
SubParams.Add(Health);
}
// TODO: support for multiple ai elements?
var ai = MainElement.GetChildElement("ai");
if (ai != null)
{
AI = new AIParams(ai, this);
SubParams.Add(AI);
}
foreach (var element in MainElement.GetChildElements("bloodemitter"))
{
var emitter = new ParticleParams(element, this);
BloodEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var element in MainElement.GetChildElements("gibemitter"))
{
var emitter = new ParticleParams(element, this);
GibEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var element in MainElement.GetChildElements("damageemitter"))
{
var emitter = new ParticleParams(element, this);
GibEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var soundElement in MainElement.GetChildElements("sound"))
{
var sound = new SoundParams(soundElement, this);
Sounds.Add(sound);
SubParams.Add(sound);
}
foreach (var inventoryElement in MainElement.GetChildElements("inventory"))
{
var inventory = new InventoryParams(inventoryElement, this);
Inventories.Add(inventory);
SubParams.Add(inventory);
}
}
public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
{
if (base.Deserialize(element))
{
//backwards compatibility
if (string.IsNullOrEmpty(SpeciesName))
{
SpeciesName = element.GetAttributeString("name", "[NAME NOT GIVEN]");
}
if (alsoChildren)
{
SubParams.ForEach(p => p.Deserialize(recursive));
}
return true;
}
return false;
}
public bool Serialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
{
if (base.Serialize(element))
{
if (alsoChildren)
{
SubParams.ForEach(p => p.Serialize(recursive));
}
return true;
}
return false;
}
#if CLIENT
public void AddToEditor(ParamsEditor editor, bool alsoChildren = true, bool recursive = true, int space = 0)
{
base.AddToEditor(editor);
if (alsoChildren)
{
SubParams.ForEach(s => s.AddToEditor(editor, recursive));
}
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, (int)(space * GUI.yScale)), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
{
CanBeFocused = false
};
}
}
#endif
public bool AddSound() => TryAddSubParam(new XElement("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 AddBloodEmitter() => AddEmitter("bloodemitter");
public void AddGibEmitter() => AddEmitter("gibemitter");
public void AddDamageEmitter() => AddEmitter("damageemitter");
private void AddEmitter(string type)
{
switch (type)
{
case "gibemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters);
break;
case "bloodemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters);
break;
case "damageemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters);
break;
default: throw new NotImplementedException(type);
}
}
public bool RemoveSound(SoundParams soundParams) => RemoveSubParam(soundParams);
public bool RemoveBloodEmitter(ParticleParams emitter) => RemoveSubParam(emitter, BloodEmitters);
public bool RemoveGibEmitter(ParticleParams emitter) => RemoveSubParam(emitter, GibEmitters);
public bool RemoveDamageEmitter(ParticleParams emitter) => RemoveSubParam(emitter, DamageEmitters);
public bool RemoveInventory(InventoryParams inventory) => RemoveSubParam(inventory, Inventories);
protected bool RemoveSubParam<T>(T subParam, IList<T> collection = null) where T : SubParam
{
if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; }
if (collection != null && !collection.Contains(subParam)) { return false; }
if (!SubParams.Contains(subParam)) { return false; }
collection?.Remove(subParam);
SubParams.Remove(subParam);
subParam.Element.Remove();
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
{
subParam = constructor(element, this);
if (collection != null && filter != null)
{
if (filter(collection)) { return false; }
}
MainElement.Add(element);
SubParams.Add(subParam);
collection?.Add(subParam);
return subParam != null;
}
#region Subparams
public class SoundParams : SubParam
{
public override string Name => "Sound";
[Serialize("", true), Editable]
public string File { get; private set; }
#if CLIENT
[Serialize(SoundType.Idle, true), Editable]
public SoundType State { get; private set; }
#endif
[Serialize(1000f, true), Editable(minValue: 0f, maxValue: 10000f)]
public float Range { get; private set; }
[Serialize(1.0f, true), 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; }
public SoundParams(XElement element, CharacterParams character) : base(element, character) { }
}
public class ParticleParams : SubParam
{
private string name;
public override string Name
{
get
{
if (name == null && Element != null)
{
name = Element.Name.ToString().FormatCamelCaseWithSpaces();
}
return name;
}
}
[Serialize("", true), Editable]
public string Particle { get; set; }
[Serialize(0f, true), Editable(-360f, 360f, decimals: 0)]
public float AngleMin { get; private set; }
[Serialize(0f, true), Editable(-360f, 360f, decimals: 0)]
public float AngleMax { get; private set; }
[Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)]
public float ScaleMin { get; private set; }
[Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)]
public float ScaleMax { get; private set; }
[Serialize(0f, true), Editable(0f, 10000f, decimals: 0)]
public float VelocityMin { get; private set; }
[Serialize(0f, true), Editable(0f, 10000f, decimals: 0)]
public float VelocityMax { get; private set; }
[Serialize(0f, true), Editable(0f, 100f, decimals: 2)]
public float EmitInterval { get; private set; }
[Serialize(0, true), Editable(0, 1000)]
public int ParticlesPerSecond { get; private set; }
[Serialize(0, true), Editable(0, 1000)]
public int ParticleAmount { get; private set; }
[Serialize(false, true), Editable]
public bool HighQualityCollisionDetection { get; private set; }
[Serialize(false, true), Editable]
public bool CopyEntityAngle { get; private set; }
public ParticleParams(XElement 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)]
public float Vitality { get; set; }
[Serialize(true, true), Editable]
public bool DoesBleed { get; set; }
[Serialize(float.NegativeInfinity, true), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
public float CrushDepth { get; set; }
// Make editable?
[Serialize(false, true)]
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 = 10, 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 = 10, DecimalCount = 2)]
public float BurnReduction { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ConstantHealthRegeneration { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HealthRegenerationWhenEating { get; private set; }
// TODO: limbhealths, sprite?
public HealthParams(XElement element, CharacterParams character) : base(element, character) { }
}
public class InventoryParams : SubParam
{
public class InventoryItem : SubParam
{
public override string Name => "Item";
[Serialize("", true, description: "Item identifier."), Editable()]
public string Identifier { get; private set; }
public InventoryItem(XElement 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()]
public string Slots { get; private set; }
[Serialize(false, true), 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)]
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)
{
foreach (var itemElement in element.GetChildElements("item"))
{
var item = new InventoryItem(itemElement, character);
SubParams.Add(item);
Items.Add(item);
}
}
public void AddItem(string identifier = null)
{
identifier = identifier ?? "";
var element = new XElement("item", new XAttribute("identifier", identifier));
Element.Add(element);
var item = new InventoryItem(element, Character);
SubParams.Add(item);
Items.Add(item);
}
public bool RemoveItem(InventoryItem item) => RemoveSubParam(item, Items);
}
public class AIParams : SubParam
{
public override string Name => "AI";
[Serialize(1.0f, true, 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)]
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)]
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)]
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)]
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)]
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()]
public bool AttackWhenProvoked { get; private set; }
[Serialize(true, true, 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(false, true, description: "Does the character try to break inside the sub?"), Editable()]
public bool AggressiveBoarding { get; private set; }
// TODO: latchonto, swarming
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();
public AIParams(XElement element, CharacterParams character) : base(element, character)
{
element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _));
element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _));
}
private bool TryAddTarget(XElement targetElement, out TargetParams target)
{
string tag = targetElement.GetAttributeString("tag", null);
if (HasTag(tag))
{
target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
return false;
}
else
{
target = new TargetParams(targetElement, Character);
targets.Add(target);
SubParams.Add(target);
return true;
}
}
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)
{
var element = TargetParams.CreateNewElement(tag, state, priority);
if (TryAddTarget(element, out targetParams))
{
Element.Add(element);
return true;
}
else
{
return false;
}
}
public bool HasTag(string tag)
{
if (tag == null) { return false; }
return targets.Any(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase));
}
public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets);
public bool TryGetTarget(string targetTag, out TargetParams target)
{
target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetTag, StringComparison.OrdinalIgnoreCase));
return target != null;
}
public TargetParams GetTarget(string targetTag, bool throwError = true)
{
if (!TryGetTarget(targetTag, out TargetParams target))
{
if (throwError)
{
DebugConsole.ThrowError($"Cannot find a target with the tag {targetTag}!");
}
}
return target;
}
}
public class TargetParams : SubParam
{
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()]
public string Tag { get; private set; }
[Serialize(AIState.Idle, true), 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)]
public float Priority { get; set; }
[Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. Eg. 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; }
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
public static XElement CreateNewElement(string tag, AIState state, float priority)
{
return new XElement("target",
new XAttribute("tag", tag),
new XAttribute("state", state),
new XAttribute("priority", priority));
}
}
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 List<SubParam> SubParams { get; set; } = new List<SubParam>();
public CharacterParams Character { get; private set; }
public SubParam(XElement element, CharacterParams character)
{
Element = element;
Character = character;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public virtual bool Deserialize(bool recursive = true)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, Element);
if (recursive)
{
SubParams.ForEach(sp => sp.Deserialize(true));
}
return SerializableProperties != null;
}
public virtual bool Serialize(bool recursive = true)
{
SerializableProperty.SerializeProperties(this, Element, true);
if (recursive)
{
SubParams.ForEach(sp => sp.Serialize(true));
}
return true;
}
public virtual void Reset()
{
// Don't use recursion, because the reset method might be overriden
Deserialize(false);
SubParams.ForEach(sp => sp.Reset());
}
protected bool RemoveSubParam<T>(T subParam, IList<T> collection = null) where T : SubParam
{
if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; }
if (collection != null && !collection.Contains(subParam)) { return false; }
if (!SubParams.Contains(subParam)) { return false; }
collection?.Remove(subParam);
SubParams.Remove(subParam);
subParam.Element.Remove();
return true;
}
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, ScalableFont titleFont = null)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUI.LargeFont);
if (recursive)
{
SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUI.SmallFont));
}
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: new Color(20, 20, 20, 255))
{
CanBeFocused = false
};
}
}
#endif
}
#endregion
}
}
@@ -0,0 +1,136 @@
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
abstract class EditableParams : ISerializableEntity
{
public bool IsLoaded { get; protected set; }
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; }
protected XDocument doc;
public XDocument Doc
{
get
{
if (!IsLoaded)
{
DebugConsole.ThrowError("[Params] Not loaded!");
return new XDocument();
}
return doc;
}
protected set
{
doc = value;
}
}
public virtual XElement MainElement => doc.Root;
public XElement OriginalElement { get; protected set; }
protected virtual string GetName() => Path.GetFileNameWithoutExtension(FullPath).FormatCamelCaseWithSpaces();
protected virtual bool Deserialize(XElement element = null)
{
element = element ?? MainElement;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
return SerializableProperties != null;
}
protected virtual bool Serialize(XElement element = null)
{
element = element ?? MainElement;
if (element == null)
{
DebugConsole.ThrowError("[EditableParams] The XML element is null!");
return false;
}
SerializableProperty.SerializeProperties(this, element, true);
return true;
}
protected virtual bool Load(string file)
{
UpdatePath(file);
doc = XMLExtensions.TryLoadXml(FullPath);
if (doc == null) { return false; }
IsLoaded = Deserialize(MainElement);
OriginalElement = new XElement(MainElement);
return IsLoaded;
}
protected virtual void UpdatePath(string fullPath)
{
FullPath = fullPath;
Name = GetName();
FileName = Path.GetFileName(FullPath);
Folder = Path.GetDirectoryName(FullPath);
}
public virtual bool Save(string fileNameWithoutExtension = null, XmlWriterSettings settings = null)
{
if (!Directory.Exists(Folder))
{
Directory.CreateDirectory(Folder);
}
OriginalElement = MainElement;
Serialize();
if (settings == null)
{
settings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineOnAttributes = true
};
}
if (fileNameWithoutExtension != null)
{
UpdatePath(Path.Combine(Folder, $"{fileNameWithoutExtension}.xml"));
}
using (var writer = XmlWriter.Create(FullPath, settings))
{
Doc.WriteTo(writer);
writer.Flush();
}
return true;
}
public virtual bool Reset(bool forceReload = false)
{
if (forceReload)
{
return Load(FullPath);
}
return Deserialize(OriginalElement);
}
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor, int space = 0)
{
if (!IsLoaded)
{
DebugConsole.ThrowError("[Params] Not loaded!");
return;
}
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUI.LargeFont);
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
{
CanBeFocused = false
};
}
}
#endif
}
}
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class SkillSettings : ISerializableEntity
{
public static SkillSettings Current
{
get;
private set;
}
[Serialize(4.0f, true)]
public float SingleRoundSkillGainMultiplier { get; set; }
private float skillIncreasePerRepair;
[Serialize(5.0f, true)]
public float SkillIncreasePerRepair
{
get { return skillIncreasePerRepair * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerRepair = value; }
}
private float skillIncreasePerSabotage;
[Serialize(3.0f, true)]
public float SkillIncreasePerSabotage
{
get { return skillIncreasePerSabotage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSabotage = value; }
}
private float skillIncreasePerCprRevive;
[Serialize(0.5f, true)]
public float SkillIncreasePerCprRevive
{
get { return skillIncreasePerCprRevive * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerCprRevive = value; }
}
private float skillIncreasePerRepairedStructureDamage;
[Serialize(0.005f, true)]
public float SkillIncreasePerRepairedStructureDamage
{
get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerRepairedStructureDamage = value; }
}
private float skillIncreasePerSecondWhenSteering;
[Serialize(0.005f, true)]
public float SkillIncreasePerSecondWhenSteering
{
get { return skillIncreasePerSecondWhenSteering * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSecondWhenSteering = value; }
}
private float skillIncreasePerFabricatorRequiredSkill;
[Serialize(0.5f, true)]
public float SkillIncreasePerFabricatorRequiredSkill
{
get { return skillIncreasePerFabricatorRequiredSkill * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFabricatorRequiredSkill = value; }
}
private float skillIncreasePerHostileDamage;
[Serialize(0.01f, true)]
public float SkillIncreasePerHostileDamage
{
get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerHostileDamage = value; }
}
private float skillIncreasePerSecondWhenOperatingTurret;
[Serialize(0.001f, true)]
public float SkillIncreasePerSecondWhenOperatingTurret
{
get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSecondWhenOperatingTurret = value; }
}
private float skillIncreasePerFriendlyHealed;
[Serialize(0.001f, true)]
public float SkillIncreasePerFriendlyHealed
{
get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFriendlyHealed = value; }
}
[Serialize(1.1f, true)]
public float AssistantSkillIncreaseMultiplier
{
get;
set;
}
private SkillSettings(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public string Name => "SkillSettings";
public Dictionary<string, 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("Now skill settings found in the selected content packages. Using default values.");
Current = new SkillSettings(null);
}
}
private float GetCurrentSkillGainMultiplier()
{
if (GameMain.GameSession?.GameMode is CampaignMode)
{
return 1.0f;
}
else
{
return SingleRoundSkillGainMultiplier;
}
}
}
}
@@ -0,0 +1,648 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
public enum ContentType
{
None,
Submarine,
Jobs,
Item,
ItemAssembly,
Character,
Structure,
Outpost,
Text,
Executable,
ServerExecutable,
LocationTypes,
MapGenerationParameters,
LevelGenerationParameters,
LevelObjectPrefabs,
RandomEvents,
Missions,
BackgroundCreaturePrefabs,
Sounds,
RuinConfig,
Particles,
Decals,
NPCConversations,
Afflictions,
Tutorials,
UIStyle,
TraitorMissions,
EventManagerSettings,
Orders,
SkillSettings
}
public class ContentPackage
{
public static string Folder = "Data/ContentPackages/";
public static List<ContentPackage> List = new List<ContentPackage>();
//these types of files are included in the MD5 hash calculation,
//meaning that the players must have the exact same files to play together
private static HashSet<ContentType> multiplayerIncompatibleContent = new HashSet<ContentType>
{
ContentType.Jobs,
ContentType.Item,
ContentType.Character,
ContentType.Structure,
ContentType.LocationTypes,
ContentType.MapGenerationParameters,
ContentType.LevelGenerationParameters,
ContentType.Missions,
ContentType.LevelObjectPrefabs,
ContentType.RuinConfig,
ContentType.Outpost,
ContentType.Afflictions,
ContentType.Orders
};
//at least one file of each these types is required in core content packages
private static HashSet<ContentType> corePackageRequiredFiles = new HashSet<ContentType>
{
ContentType.Jobs,
ContentType.Item,
ContentType.Character,
ContentType.Structure,
ContentType.Outpost,
ContentType.Text,
ContentType.Executable,
ContentType.ServerExecutable,
ContentType.LocationTypes,
ContentType.MapGenerationParameters,
ContentType.LevelGenerationParameters,
ContentType.RandomEvents,
ContentType.Missions,
ContentType.RuinConfig,
ContentType.Afflictions,
ContentType.UIStyle,
ContentType.EventManagerSettings,
ContentType.Orders
};
public static IEnumerable<ContentType> CorePackageRequiredFiles
{
get { return corePackageRequiredFiles; }
}
public static bool IngameModSwap = false;
public string Name { get; set; }
public string Path
{
get;
set;
}
public string SteamWorkshopUrl;
public DateTime? InstallTime;
public bool HideInWorkshopMenu
{
get;
private set;
}
private Md5Hash md5Hash;
public Md5Hash MD5hash
{
get
{
if (md5Hash == null)
{
//TODO: before re-enabling content package hash caching, make sure the hash gets recalculated when any file in the content package changes, not just when the filelist.xml changes.
/*md5Hash = Md5Hash.FetchFromCache(Path);
if (md5Hash == null)
{
CalculateHash();
md5Hash.SaveToCache(Path);
}*/
CalculateHash();
}
return md5Hash;
}
}
//core packages are content packages that are required for the game to work
//e.g. they include the executable, some location types, level generation params and other files the game won't work without
//one (and only one) core package must always be selected
public bool CorePackage
{
get;
set;
}
public Version GameVersion
{
get; set;
}
public List<ContentFile> Files;
public bool HasMultiplayerIncompatibleContent
{
get { return Files.Any(f => multiplayerIncompatibleContent.Contains(f.Type)); }
}
private ContentPackage()
{
Files = new List<ContentFile>();
}
public ContentPackage(string filePath, string setPath = "")
: this()
{
filePath = filePath.CleanUpPath();
if (!string.IsNullOrEmpty(setPath)) { setPath = setPath.CleanUpPath(); }
XDocument doc = XMLExtensions.TryLoadXml(filePath);
Path = setPath == string.Empty ? filePath : setPath;
if (doc?.Root == null)
{
DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!");
return;
}
Name = doc.Root.GetAttributeString("name", "");
HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false);
CorePackage = doc.Root.GetAttributeBool("corepackage", false);
SteamWorkshopUrl = doc.Root.GetAttributeString("steamworkshopurl", "");
GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0"));
if (doc.Root.Attribute("installtime") != null)
{
InstallTime = ToolBox.Epoch.ToDateTime(doc.Root.GetAttributeUInt("installtime", 0));
}
List<string> errorMsgs = new List<string>();
foreach (XElement subElement in doc.Root.Elements())
{
if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type))
{
errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type.");
type = ContentType.None;
}
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
}
bool compatible = IsCompatible();
//If we know that the package is not compatible, don't display error messages.
if (compatible)
{
foreach (string errorMsg in errorMsgs)
{
DebugConsole.ThrowError(errorMsg);
}
}
}
private bool? hasErrors;
public bool HasErrors
{
get
{
if (!hasErrors.HasValue)
{
hasErrors = !CheckErrors(out _);
}
return hasErrors.Value;
}
}
private List<string> errorMessages;
public IEnumerable<string> ErrorMessages
{
get
{
if (errorMessages == null) { CheckErrors(out _); }
return errorMessages;
}
}
public override string ToString()
{
return Name;
}
public bool IsCompatible()
{
if (Files.All(f => f.Type == ContentType.Submarine))
{
return true;
}
//content package compatibility checks were added in 0.8.9.1
//v0.8.9.1 is not compatible with older content packages
if (GameVersion < new Version(0, 8, 9, 1))
{
return false;
}
//do additional checks here if later versions add changes that break compatibility
return true;
}
public bool ContainsRequiredCorePackageFiles()
{
return corePackageRequiredFiles.All(fileType => Files.Any(file => file.Type == fileType));
}
public bool ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes)
{
missingContentTypes = new List<ContentType>();
foreach (ContentType contentType in corePackageRequiredFiles)
{
if (!Files.Any(file => file.Type == contentType))
{
missingContentTypes.Add(contentType);
}
}
return missingContentTypes.Count == 0;
}
public bool CheckErrors(out List<string> errorMessages)
{
this.errorMessages = errorMessages = new List<string>();
foreach (ContentFile file in Files)
{
switch (file.Type)
{
case ContentType.Executable:
case ContentType.ServerExecutable:
case ContentType.None:
case ContentType.Outpost:
case ContentType.Submarine:
break;
default:
try
{
XDocument.Load(file.Path);
}
catch (Exception e)
{
if (TextManager.Initialized)
{
errorMessages.Add(TextManager.GetWithVariables("xmlfileinvalid",
new string[] { "[filepath]", "[errormessage]" },
new string[] { file.Path, e.Message }));
}
else
{
errorMessages.Add($"XML File Invalid. PATH: {file.Path}, ERROR: {e.Message}");
#if DEBUG
throw;
#endif
}
}
break;
}
}
if (CorePackage && !ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes))
{
errorMessages.Add(TextManager.GetWithVariables("ContentPackageCantMakeCorePackage",
new string[2] { "[packagename]", "[missingfiletypes]" },
new string[2] { Name, string.Join(", ", missingContentTypes) },
new bool[2] { false, true }));
}
VerifyFiles(out List<string> missingFileMessages);
errorMessages.AddRange(missingFileMessages);
hasErrors = errorMessages.Count > 0;
return !hasErrors.Value;
}
/// <summary>
/// Make sure all the files defined in the content package are present
/// </summary>
/// <returns></returns>
public bool VerifyFiles(out List<string> errorMessages)
{
errorMessages = new List<string>();
foreach (ContentFile file in Files)
{
//TODO: determine executable extension on platform and check for the presence of the executables
if (file.Type == ContentType.Executable) { continue; }
if (file.Type == ContentType.ServerExecutable) { continue; }
if (!File.Exists(file.Path))
{
errorMessages.Add("File \"" + file.Path + "\" not found.");
continue;
}
}
return errorMessages.Count == 0;
}
public static ContentPackage CreatePackage(string name, string path, bool corePackage)
{
ContentPackage newPackage = new ContentPackage()
{
Name = name,
Path = path,
CorePackage = corePackage,
GameVersion = GameMain.Version
};
return newPackage;
}
public ContentFile AddFile(string path, ContentType type)
{
if (Files.Find(file => file.Path == path && file.Type == type) != null) return null;
ContentFile cf = new ContentFile(path, type);
Files.Add(cf);
return cf;
}
public void RemoveFile(ContentFile file)
{
Files.Remove(file);
}
public void Save(string filePath)
{
XDocument doc = new XDocument();
doc.Add(new XElement("contentpackage",
new XAttribute("name", Name),
new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)),
new XAttribute("corepackage", CorePackage)));
doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString()));
if (!string.IsNullOrEmpty(SteamWorkshopUrl))
{
doc.Root.Add(new XAttribute("steamworkshopurl", SteamWorkshopUrl));
}
if (InstallTime != null)
{
doc.Root.Add(new XAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value)));
}
foreach (ContentFile file in Files)
{
doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform())));
}
doc.Save(filePath);
}
public void CalculateHash(bool logging = false)
{
List<byte[]> hashes = new List<byte[]>();
if (logging)
{
DebugConsole.NewMessage("****************************** Calculating cp hash " + Name);
}
foreach (ContentFile file in Files)
{
if (!multiplayerIncompatibleContent.Contains(file.Type)) { continue; }
try
{
var hash = CalculateFileHash(file);
if (logging)
{
var fileMd5 = new Md5Hash(hash);
DebugConsole.NewMessage(" " + file.Path + ": " + fileMd5.Hash);
}
hashes.Add(hash);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while calculating content package hash: ", e);
}
}
byte[] bytes = new byte[hashes.Count * 16];
for (int i = 0; i < hashes.Count; i++)
{
hashes[i].CopyTo(bytes, i * 16);
}
md5Hash = new Md5Hash(bytes);
if (logging)
{
DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.Hash);
}
}
private byte[] CalculateFileHash(ContentFile file)
{
using (MD5 md5 = MD5.Create())
{
List<string> filePaths = new List<string> { file.Path };
List<byte> data = new List<byte>();
switch (file.Type)
{
case ContentType.Character:
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc.Root;
var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
var ragdollFolder = RagdollParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
if (Directory.Exists(ragdollFolder))
{
Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
var animationFolder = AnimationParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
if (Directory.Exists(animationFolder))
{
Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
break;
}
if (filePaths.Count > 1)
{
using (MD5 tempMd5 = MD5.Create())
{
filePaths = filePaths.OrderBy(f => ToolBox.StringToUInt32Hash(f.CleanUpPathCrossPlatform(true), tempMd5)).ToList();
}
}
foreach (string filePath in filePaths)
{
if (!File.Exists(filePath)) continue;
using (var stream = File.OpenRead(filePath))
{
byte[] fileData = new byte[stream.Length];
stream.Read(fileData, 0, (int)stream.Length);
if (filePath.EndsWith(".xml", true, System.Globalization.CultureInfo.InvariantCulture))
{
string text = System.Text.Encoding.UTF8.GetString(fileData);
text = text.Replace("\n", "").Replace("\r", "").Replace("\\","/");
fileData = System.Text.Encoding.UTF8.GetBytes(text);
}
data.AddRange(fileData);
}
}
return md5.ComputeHash(data.ToArray());
}
}
public static bool IsModFilePathAllowed(ContentFile contentFile)
{
string path = contentFile.Path;
return IsModFilePathAllowed(path);
}
/// <summary>
/// Are mods allowed to install a file into the specified path. If a content package XML includes files
/// with a prohibited path, they are treated as references to external files. For example, a mod could include
/// some vanilla files in the XML, in which case the game will simply use the vanilla files present in the game folder.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool IsModFilePathAllowed(string path)
{
while (true)
{
string temp = System.IO.Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(temp)) { break; }
path = temp;
}
return path == "Mods";
}
/// <summary>
/// Returns all xml files from all the loaded content packages.
/// </summary>
public static IEnumerable<string> GetAllContentFiles(IEnumerable<ContentPackage> contentPackages)
{
return contentPackages.SelectMany(f => f.Files).Select(f => f.Path).Where(p => p.EndsWith(".xml"));
}
public static IEnumerable<ContentFile> GetFilesOfType(IEnumerable<ContentPackage> contentPackages, ContentType type)
{
return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type);
}
public IEnumerable<string> GetFilesOfType(ContentType type)
{
return Files.Where(f => f.Type == type).Select(f => f.Path);
}
public static void LoadAll()
{
string folder = ContentPackage.Folder;
if (!Directory.Exists(folder))
{
try
{
Directory.CreateDirectory(folder);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to create directory \"" + folder + "\"", e);
return;
}
}
string[] files = Directory.GetFiles(folder, "*.xml");
List.Clear();
foreach (string filePath in files)
{
List.Add(new ContentPackage(filePath));
}
string[] modDirectories = Directory.GetDirectories("Mods");
foreach (string modDirectory in modDirectories)
{
if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; }
string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName);
if (File.Exists(modFilePath))
{
List.Add(new ContentPackage(modFilePath));
}
}
List = List
.OrderByDescending(p => p.CorePackage)
.ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p))
.ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p))
.ToList();
}
public static void SortContentPackages()
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ToList();
if (GameMain.Config != null)
{
var sortedSelected = GameMain.Config.SelectedContentPackages
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ToList();
GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected);
var reportList = List.Where(p => GameMain.Config.SelectedContentPackages.Contains(p));
DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }");
}
}
public void Delete()
{
try
{
GameMain.Config.DeselectContentPackage(this);
GameMain.Config.SaveNewPlayerConfig();
List.Remove(this);
File.Delete(Path);
SortContentPackages();
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to delete content package \"" + Name + "\".", e);
return;
}
}
}
public class ContentFile
{
public string Path;
public ContentType Type;
public ContentPackage ContentPackage;
public ContentFile(string path, ContentType type, ContentPackage contentPackage = null)
{
Path = path.CleanUpPath();
Type = type;
ContentPackage = contentPackage;
}
public override string ToString()
{
return Path;
}
}
}
@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Barotrauma
{
enum CoroutineStatus
{
Running, Success, Failure
}
class CoroutineHandle
{
public readonly IEnumerator<object> Coroutine;
public readonly string Name;
public Exception Exception;
public volatile bool AbortRequested;
public Thread Thread;
public CoroutineHandle(IEnumerator<object> coroutine, string name = "", bool useSeparateThread = false)
{
Coroutine = coroutine;
Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name;
Exception = null;
}
}
// Keeps track of all running coroutines, and runs them till the end.
static class CoroutineManager
{
static readonly List<CoroutineHandle> Coroutines = new List<CoroutineHandle>();
public static float UnscaledDeltaTime, DeltaTime;
public static CoroutineHandle StartCoroutine(IEnumerable<object> func, string name = "", bool useSeparateThread = false)
{
var handle = new CoroutineHandle(func.GetEnumerator(), name);
lock (Coroutines)
{
Coroutines.Add(handle);
}
handle.Thread = null;
if (useSeparateThread)
{
handle.Thread = new Thread(() => { ExecuteCoroutineThread(handle); })
{
Name = "Coroutine Thread (" + handle.Name + ")",
IsBackground = true
};
handle.Thread.Start();
}
return handle;
}
public static CoroutineHandle Invoke(Action action)
{
return StartCoroutine(DoInvokeAfter(action, 0.0f));
}
public static CoroutineHandle InvokeAfter(Action action, float delay)
{
return StartCoroutine(DoInvokeAfter(action, delay));
}
private static IEnumerable<object> DoInvokeAfter(Action action, float delay)
{
if (action == null)
{
yield return CoroutineStatus.Failure;
}
if (delay > 0.0f)
{
yield return new WaitForSeconds(delay);
}
action();
yield return CoroutineStatus.Success;
}
public static bool IsCoroutineRunning(string name)
{
lock (Coroutines)
{
return Coroutines.Any(c => c.Name == name);
}
}
public static bool IsCoroutineRunning(CoroutineHandle handle)
{
lock (Coroutines)
{
return Coroutines.Contains(handle);
}
}
public static void StopCoroutines(string name)
{
lock (Coroutines)
{
Coroutines.ForEach(c =>
{
if (c.Name == name)
{
c.AbortRequested = true;
if (c.Thread != null)
{
bool joined = false;
while (!joined)
{
#if CLIENT
CrossThread.ProcessTasks();
#endif
joined = c.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
}
});
Coroutines.RemoveAll(c => c.Name == name);
}
}
public static void StopCoroutines(CoroutineHandle handle)
{
lock (Coroutines)
{
Coroutines.RemoveAll(c => c == handle);
}
}
public static void ExecuteCoroutineThread(CoroutineHandle handle)
{
try
{
while (!handle.AbortRequested)
{
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
if (wfs != null)
{
Thread.Sleep((int)(wfs.TotalTime * 1000));
}
else
{
switch ((CoroutineStatus)handle.Coroutine.Current)
{
case CoroutineStatus.Success:
return;
case CoroutineStatus.Failure:
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
return;
}
}
}
Thread.Yield();
if (!handle.Coroutine.MoveNext()) return;
}
}
catch (ThreadAbortException)
{
//not an error, don't worry about it
}
catch (Exception e)
{
handle.Exception = e;
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has thrown an exception", e);
}
}
private static bool IsDone(CoroutineHandle handle)
{
#if !DEBUG
try
{
#endif
if (handle.Thread == null)
{
if (handle.AbortRequested) { return true; }
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
if (wfs != null)
{
if (!wfs.CheckFinished(UnscaledDeltaTime)) return false;
}
else
{
switch ((CoroutineStatus)handle.Coroutine.Current)
{
case CoroutineStatus.Success:
return true;
case CoroutineStatus.Failure:
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
return true;
}
}
}
handle.Coroutine.MoveNext();
return false;
}
else
{
if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped))
{
if (handle.Exception!=null || (CoroutineStatus)handle.Coroutine.Current == CoroutineStatus.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
return true;
}
return false;
}
#if !DEBUG
}
catch (Exception e)
{
#if CLIENT && WINDOWS
if (e is SharpDX.SharpDXException) { throw; }
#endif
DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.ToString());
handle.Exception = e;
return true;
}
#endif
}
// Updating just means stepping through all the coroutines
public static void Update(float unscaledDeltaTime, float deltaTime)
{
UnscaledDeltaTime = unscaledDeltaTime;
DeltaTime = deltaTime;
List<CoroutineHandle> coroutineList;
lock (Coroutines)
{
coroutineList = Coroutines.ToList();
}
foreach (var coroutine in coroutineList)
{
if (IsDone(coroutine))
{
lock (Coroutines)
{
Coroutines.Remove(coroutine);
}
}
}
}
}
class WaitForSeconds
{
public readonly float TotalTime;
float timer;
bool ignorePause;
public WaitForSeconds(float time, bool ignorePause = true)
{
timer = time;
TotalTime = time;
this.ignorePause = ignorePause;
}
public bool CheckFinished(float deltaTime)
{
#if !SERVER
if (ignorePause || !GUI.PauseMenuOpen)
{
timer -= deltaTime;
}
#else
timer -= deltaTime;
#endif
return timer <= 0.0f;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class ArtifactEvent : ScriptedEvent
{
private ItemPrefab itemPrefab;
private Item item;
private int state;
private Vector2 spawnPos;
private bool spawnPending;
public override Vector2 DebugDrawPos
{
get { return spawnPos; }
}
public override string ToString()
{
return "ArtifactEvent (" + (itemPrefab == null ? "null" : itemPrefab.Name) + ")";
}
public ArtifactEvent(ScriptedEventPrefab prefab)
: base(prefab)
{
if (prefab.ConfigElement.Attribute("itemname") != null)
{
DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.");
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
}
}
else
{
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier);
}
}
}
public override void Init(bool affectSubImmediately)
{
spawnPos = Level.Loaded.GetRandomItemPos(
(Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin,
500.0f, 10000.0f, 30.0f);
spawnPending = true;
}
private void SpawnItem()
{
item = new Item(itemPrefab, spawnPos, null);
item.body.FarseerBody.BodyType = FarseerPhysics.BodyType.Kinematic;
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) continue;
if (itemContainer.Combine(item, user: null)) break; // Placement successful
}
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("Initialized ArtifactEvent (" + item.Name + ")", Color.White);
}
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(item, false);
}
#endif
}
public override void Update(float deltaTime)
{
if (spawnPending)
{
SpawnItem();
spawnPending = false;
}
switch (state)
{
case 0:
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic; }
if (item.CurrentHull == null) return;
state = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return;
Finished();
state = 2;
break;
}
}
}
}
@@ -0,0 +1,517 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class EventManager
{
const float IntensityUpdateInterval = 5.0f;
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
//The "intensity" of the current situation (a value between 0.0 - 1.0).
//High when a disaster has struck, low when nothing special is going on.
private float currentIntensity;
//The exact intensity of the current situation, current intensity is lerped towards this value
private float targetIntensity;
//How low the intensity has to be for an event to be triggered.
//Gradually increases with time, so additional problems can still appear eventually even if
//the sub is laying broken on the ocean floor or if the players are trying to abuse the system
//by intentionally keeping the intensity high by causing breaches, damaging themselves or such
private float eventThreshold = 0.2f;
//New events can't be triggered when the cooldown is active.
private float eventCoolDown;
private float intensityUpdateTimer;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger;
private float roundDuration;
private readonly List<ScriptedEventSet> pendingEventSets = new List<ScriptedEventSet>();
private readonly Dictionary<ScriptedEventSet, List<ScriptedEvent>> selectedEvents = new Dictionary<ScriptedEventSet, List<ScriptedEvent>>();
private readonly List<ScriptedEvent> activeEvents = new List<ScriptedEvent>();
#if DEBUG && SERVER
private DateTime nextIntensityLogTime;
#endif
private EventManagerSettings settings;
private readonly bool isClient;
public float CurrentIntensity
{
get { return currentIntensity; }
}
public List<ScriptedEvent> ActiveEvents
{
get { return activeEvents; }
}
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
}
public bool Enabled = true;
public void StartRound(Level level)
{
if (isClient) { return; }
pendingEventSets.Clear();
selectedEvents.Clear();
this.level = level;
SelectSettings();
var initialEventSet = SelectRandomEvents(ScriptedEventSet.List);
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet);
}
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
intensityUpdateTimer = 0.0f;
CalculateCurrentIntensity(0.0f);
currentIntensity = targetIntensity;
eventCoolDown = 0.0f;
}
private void SelectSettings()
{
if (EventManagerSettings.List.Count == 0)
{
throw new InvalidOperationException("Could not select EventManager settings (no settings loaded).");
}
if (level == null)
{
throw new InvalidOperationException("Could not select EventManager settings (level not set).");
}
var suitableSettings = EventManagerSettings.List.FindAll(s =>
level.Difficulty >= s.MinLevelDifficulty &&
level.Difficulty <= s.MaxLevelDifficulty);
if (suitableSettings.Count == 0)
{
DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")");
settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)];
}
else
{
settings = suitableSettings[Rand.Int(suitableSettings.Count, Rand.RandSync.Server)];
}
if (settings != null)
{
eventThreshold = settings.DefaultEventThreshold;
}
}
public IEnumerable<ContentFile> GetFilesToPreload()
{
foreach (List<ScriptedEvent> eventList in selectedEvents.Values)
{
foreach (ScriptedEvent scriptedEvent in eventList)
{
foreach (ContentFile contentFile in scriptedEvent.GetFilesToPreload())
{
yield return contentFile;
}
}
}
}
public void PreloadContent(IEnumerable<ContentFile> contentFiles)
{
foreach (ContentFile file in contentFiles)
{
switch (file.Type)
{
case ContentType.Character:
#if CLIENT
CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(file.Path);
if (characterPrefab?.XDocument == null)
{
throw new Exception($"Failed to load the character config file from {file.Path}!");
}
var doc = characterPrefab.XDocument;
var rootElement = doc.Root;
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
foreach (var soundElement in mainElement.GetChildElements("sound"))
{
var sound = Submarine.LoadRoundSound(soundElement);
}
string speciesName = mainElement.GetAttributeString("speciesname", null);
if (string.IsNullOrWhiteSpace(speciesName))
{
speciesName = mainElement.GetAttributeString("name", null);
if (!string.IsNullOrWhiteSpace(speciesName))
{
DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
throw new Exception($"Species name null in {file.Path}");
}
}
bool humanoid = mainElement.GetAttributeBool("humanoid", false);
RagdollParams ragdollParams;
if (humanoid)
{
ragdollParams = RagdollParams.GetRagdollParams<HumanRagdollParams>(speciesName);
}
else
{
ragdollParams = RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName);
}
if (ragdollParams != null)
{
HashSet<string> texturePaths = new HashSet<string>
{
ragdollParams.Texture
};
foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs)
{
if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); }
if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); }
if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); }
foreach (var decorativeSprite in limb.decorativeSpriteParams)
{
if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); }
}
}
foreach (string texturePath in texturePaths)
{
preloadedSprites.Add(new Sprite(texturePath, Vector2.Zero));
}
}
#endif
break;
}
}
}
public void EndRound()
{
pendingEventSets.Clear();
selectedEvents.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
}
private void CreateEvents(ScriptedEventSet eventSet)
{
int applyCount = 1;
if (eventSet.PerRuin)
{
applyCount = Level.Loaded.Ruins.Count();
}
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
{
if (eventSet.EventPrefabs.Count > 0)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand);
if (eventPrefab != null)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
}
selectedEvents[eventSet].Add(newEvent);
}
}
if (eventSet.ChildSets.Count > 0)
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
if (newEventSet != null) { CreateEvents(newEventSet); }
}
}
else
{
foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
}
selectedEvents[eventSet].Add(newEvent);
}
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
CreateEvents(childEventSet);
}
}
}
}
private ScriptedEventSet SelectRandomEvents(List<ScriptedEventSet> eventSets)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty);
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
float randomNumber = (float)rand.NextDouble() * totalCommonness;
foreach (ScriptedEventSet eventSet in allowedEventSets)
{
float commonness = eventSet.GetCommonness(level);
if (randomNumber <= commonness)
{
return eventSet;
}
randomNumber -= commonness;
}
return null;
}
private bool CanStartEventSet(ScriptedEventSet eventSet)
{
float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition);
float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition);
float distanceTraveled = MathHelper.Clamp(
(Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X),
0.0f, 1.0f);
//don't create new events if within 50 meters of the start/end of the level
if (!eventSet.AllowAtStart)
{
if (distanceTraveled <= 0.0f ||
distFromStart * Physics.DisplayToRealWorldRatio < 50.0f ||
distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f)
{
return false;
}
}
if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
roundDuration < eventSet.MinMissionTime)
{
return false;
}
if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
{
return false;
}
return true;
}
public void Update(float deltaTime)
{
if (!Enabled) { return; }
//clients only calculate the intensity but don't create any events
//(the intensity is used for controlling the background music)
CalculateCurrentIntensity(deltaTime);
#if DEBUG && SERVER
if (DateTime.Now > nextIntensityLogTime)
{
DebugConsole.NewMessage("EventManager intensity: " + (int)Math.Round(currentIntensity * 100) + " %");
nextIntensityLogTime = DateTime.Now + new TimeSpan(0, minutes: 1, seconds: 0);
}
#endif
if (isClient) { return; }
roundDuration += deltaTime;
if (settings == null)
{
DebugConsole.ThrowError("Event settings not set before updating EventManager. Attempting to select...");
SelectSettings();
if (settings == null)
{
DebugConsole.ThrowError("Could not select EventManager settings. Disabling EventManager for the round...");
#if SERVER
GameMain.Server?.SendChatMessage("Could not select EventManager settings. Disabling EventManager for the round...", Networking.ChatMessageType.Error);
#endif
Enabled = false;
return;
}
}
eventThreshold += settings.EventThresholdIncrease * deltaTime;
if (eventCoolDown > 0.0f)
{
eventCoolDown -= deltaTime;
}
else if (currentIntensity < eventThreshold)
{
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
var eventSet = pendingEventSets[i];
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (!selectedEvents.ContainsKey(eventSet))
{
//no events selected from this event set
continue;
}
//start events in this set
foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet])
{
activeEvents.Add(scriptedEvent);
}
//add child event sets to pending
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
if (selectedEvents.ContainsKey(childEventSet))
{
pendingEventSets.Add(childEventSet);
}
}
}
eventThreshold = settings.DefaultEventThreshold;
eventCoolDown = settings.EventCooldown;
}
foreach (ScriptedEvent ev in activeEvents)
{
if (!ev.IsFinished) { ev.Update(deltaTime); }
}
}
private void CalculateCurrentIntensity(float deltaTime)
{
intensityUpdateTimer -= deltaTime;
if (intensityUpdateTimer > 0.0f) { return; }
intensityUpdateTimer = IntensityUpdateInterval;
// crew health --------------------------------------------------------
avgCrewHealth = 0.0f;
int characterCount = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.TeamID == Character.TeamType.FriendlyNPC) { continue; }
if (character.AIController is HumanAIController || character.IsRemotePlayer)
{
avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f);
characterCount++;
}
}
if (characterCount > 0)
{
avgCrewHealth /= characterCount;
}
else
{
avgCrewHealth = 0.5f;
}
// enemy amount --------------------------------------------------------
enemyDanger = 0.0f;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.IsUnconscious || !character.Enabled) continue;
EnemyAIController enemyAI = character.AIController as EnemyAIController;
if (enemyAI == null) continue;
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
enemyDanger += enemyAI.CombatStrength / 1000.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
//enemy outside and targeting the sub or something in it
//moloch adds 0.24 to enemy danger, a crawler 0.02
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
// hull status (gaps, flooding, fire) --------------------------------------------------------
float holeCount = 0.0f;
floodingAmount = 0.0f;
int hullCount = 0;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; }
hullCount++;
foreach (Gap gap in hull.ConnectedGaps)
{
if (!gap.IsRoomToRoom) holeCount += gap.Open;
}
floodingAmount += hull.WaterVolume / hull.Volume;
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
}
if (hullCount > 0)
{
floodingAmount = floodingAmount / hullCount;
}
//hull integrity at 0.0 if there are 10 or more wide-open holes
avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f);
//a fire of any size bumps up the fire amount to 20%
//if the total width of the fires is 1000 or more, the fire amount is considered to be at 100%
fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f);
//flooding less than 10% of the sub is ignored
//to prevent ballast tanks from affecting the intensity
if (floodingAmount < 0.1f) floodingAmount = 0.0f;
// calculate final intensity --------------------------------------------------------
targetIntensity =
((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f;
targetIntensity += fireAmount * 0.5f;
targetIntensity += enemyDanger;
targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f);
if (targetIntensity > currentIntensity)
{
//50 seconds for intensity to go from 0.0 to 1.0
currentIntensity = MathHelper.Min(currentIntensity + 0.02f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity);
}
}
}
}
@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class EventManagerSettings
{
public static readonly List<EventManagerSettings> List = new List<EventManagerSettings>();
public readonly string Identifier;
public readonly string Name;
//How much the event threshold increases per second. 0.0005f = 0.03f per minute
public readonly float EventThresholdIncrease = 0.0005f;
//The threshold is reset to this value after an event has been triggered.
public readonly float DefaultEventThreshold = 0.2f;
public readonly float EventCooldown = 360.0f;
public readonly float MinLevelDifficulty = 0.0f;
public readonly float MaxLevelDifficulty = 100.0f;
static EventManagerSettings()
{
foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings))
{
Load(file);
}
}
private static void Load(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var mainElement = doc.Root;
bool allowOverriding = false;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement subElement in mainElement.Elements())
{
var element = subElement.IsOverride() ? subElement.FirstElement() : subElement;
string identifier = element.Name.ToString();
var duplicate = List.FirstOrDefault(e => e.Identifier.ToString().Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (duplicate != null)
{
if (allowOverriding || subElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file.Path}'", Color.Yellow);
List.Remove(duplicate);
}
else
{
DebugConsole.ThrowError($"Error in '{file.Path}': Another element with the name '{identifier}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
continue;
}
}
List.Add(new EventManagerSettings(element));
}
List.Sort((x, y) => { return Math.Sign((x.MinLevelDifficulty + x.MaxLevelDifficulty) / 2.0f - (y.MinLevelDifficulty + y.MaxLevelDifficulty) / 2.0f); });
}
public EventManagerSettings(XElement element)
{
Identifier = element.Name.ToString();
Name = TextManager.Get("difficulty." + Identifier, returnNull: true) ?? Identifier;
EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f);
DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f);
EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f);
MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f);
MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f);
}
}
}
@@ -0,0 +1,83 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class MalfunctionEvent : ScriptedEvent
{
private string[] targetItemIdentifiers;
private List<Item> targetItems;
private int minItemAmount, maxItemAmount;
private float decreaseConditionAmount;
private float duration;
private float timer;
public override string ToString()
{
return "MalfunctionEvent (" + string.Join(", ", targetItemIdentifiers) + ")";
}
public MalfunctionEvent(ScriptedEventPrefab prefab)
: base(prefab)
{
targetItems = new List<Item>();
minItemAmount = prefab.ConfigElement.GetAttributeInt("minitemamount", 1);
maxItemAmount = prefab.ConfigElement.GetAttributeInt("maxitemamount", minItemAmount);
decreaseConditionAmount = prefab.ConfigElement.GetAttributeFloat("decreaseconditionamount", 0.0f);
duration = prefab.ConfigElement.GetAttributeFloat("duration", 0.0f);
targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("itemidentifiers", new string[0]);
}
public override bool CanAffectSubImmediately(Level level)
{
return Item.ItemList.Count(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)) >= maxItemAmount;
}
public override void Init(bool affectSubImmediately)
{
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.Server);
for (int i = 0; i < itemAmount; i++)
{
if (matchingItems.Count == 0) break;
targetItems.Add(matchingItems[Rand.Int(matchingItems.Count, Rand.RandSync.Server)]);
}
}
public override void Update(float deltaTime)
{
if (isFinished) return;
if (targetItems.Count == 0 || timer >= duration)
{
Finished();
return;
}
targetItems.RemoveAll(i => i.Removed || i.Condition <= 0.0f);
foreach (Item item in targetItems)
{
if (duration <= 0.0f)
{
item.Condition = 0.0f;
}
else
{
item.Condition -= decreaseConditionAmount / duration * deltaTime;
}
}
timer += deltaTime;
}
}
}
@@ -0,0 +1,136 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class CargoMission : Mission
{
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
private int requiredDeliveryAmount;
public CargoMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
itemConfig = prefab.ConfigElement.Element("Items");
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0);
}
private void InitItems()
{
items.Clear();
if (itemConfig == null)
{
DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)");
return;
}
foreach (XElement subElement in itemConfig.Elements())
{
LoadItemAsChild(subElement, null);
}
if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; }
}
private void LoadItemAsChild(XElement element, Item parent)
{
ItemPrefab itemPrefab;
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in cargo mission \"" + Name + "\" - use item identifiers instead of names to configure the items.");
string itemName = element.GetAttributeString("name", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found");
return;
}
}
else
{
string itemIdentifier = element.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found");
return;
}
}
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + element.Name.ToString() + "\" not found");
return;
}
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, true);
if (cargoSpawnPos == null)
{
DebugConsole.ThrowError("Couldn't spawn items for cargo mission, cargo spawnpoint not found");
return;
}
var cargoRoom = cargoSpawnPos.CurrentHull;
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
return;
}
Vector2 position = new Vector2(
cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server),
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
var item = new Item(itemPrefab, position, cargoRoom.Submarine);
item.FindHull();
items.Add(item);
if (parent != null) parent.Combine(item, user: null);
foreach (XElement subElement in element.Elements())
{
int amount = subElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
{
LoadItemAsChild(subElement, item);
}
}
}
public override void Start(Level level)
{
if (!IsClient)
{
InitItems();
}
}
public override void End()
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndPosition)
{
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
if (deliveredItemCount >= requiredDeliveryAmount)
{
GiveReward();
completed = true;
}
}
foreach (Item item in items)
{
if (!item.Removed) { item.Remove(); }
}
items.Clear();
}
}
}
@@ -0,0 +1,148 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
{
partial class CombatMission : Mission
{
private Submarine[] subs;
private List<Character>[] crews;
private readonly string[] descriptions;
private static string[] teamNames = { "Team A", "Team B" };
public override bool AllowRespawn
{
get { return false; }
}
private Character.TeamType Winner
{
get
{
if (GameMain.GameSession?.WinningTeam == null) { return Character.TeamType.None; }
return GameMain.GameSession.WinningTeam.Value;
}
}
public override string SuccessMessage
{
get
{
if (Winner == Character.TeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; }
//disable success message for now if it hasn't been translated
if (!TextManager.ContainsTag("MissionSuccess." + Prefab.TextIdentifier)) { return ""; }
var loser = Winner == Character.TeamType.Team1 ?
Character.TeamType.Team2 :
Character.TeamType.Team1;
return base.SuccessMessage
.Replace("[loser]", GetTeamName(loser))
.Replace("[winner]", GetTeamName(Winner));
}
}
public override int TeamCount
{
get { return 2; }
}
public CombatMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
descriptions = new string[]
{
TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("descriptionneutral", ""),
TextManager.Get("MissionDescription1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("description1", ""),
TextManager.Get("MissionDescription2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("description2", "")
};
for (int i = 0; i < descriptions.Length; i++)
{
for (int n = 0; n < 2; n++)
{
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].Name);
}
}
teamNames = new string[]
{
TextManager.Get("MissionTeam1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname1", "Team A"),
TextManager.Get("MissionTeam2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname2", "Team B")
};
}
public static string GetTeamName(Character.TeamType teamID)
{
if (teamID == Character.TeamType.Team1)
{
return teamNames.Length > 0 ? teamNames[0] : "Team 1";
}
else if (teamID == Character.TeamType.Team2)
{
return teamNames.Length > 1 ? teamNames[1] : "Team 2";
}
return "Invalid Team";
}
public bool IsInWinningTeam(Character character)
{
return character != null &&
Winner != Character.TeamType.None &&
Winner == character.TeamID;
}
public override void Start(Level level)
{
if (GameMain.NetworkMember == null)
{
DebugConsole.ThrowError("Combat missions cannot be played in the single player mode.");
return;
}
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
subs[0].TeamID = Character.TeamType.Team1; subs[1].TeamID = Character.TeamType.Team2;
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
//prevent wifi components from communicating between subs
List<WifiComponent> wifiComponents = new List<WifiComponent>();
foreach (Item item in Item.ItemList)
{
wifiComponents.AddRange(item.GetComponents<WifiComponent>());
}
foreach (WifiComponent wifiComponent in wifiComponents)
{
for (int i = 0; i < 2; i++)
{
if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine))
{
wifiComponent.TeamID = subs[i].TeamID;
}
}
}
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
foreach (Submarine submarine in Submarine.Loaded)
{
//hide all subs from sonar to make sneak attacks possible
submarine.ShowSonarMarker = false;
}
}
public override void End()
{
if (GameMain.NetworkMember == null) return;
if (Winner != Character.TeamType.None)
{
GiveReward();
completed = true;
}
}
}
}
@@ -0,0 +1,204 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
abstract partial class Mission
{
public readonly MissionPrefab Prefab;
protected bool completed;
protected int state;
public int State
{
get { return state; }
protected set
{
if (state != value)
{
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(state);
#endif
ShowMessage(State);
}
}
}
protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
public readonly List<string> Headers;
public readonly List<string> Messages;
public string Name
{
get { return Prefab.Name; }
}
private string successMessage;
public virtual string SuccessMessage
{
get { return successMessage; }
private set { successMessage = value; }
}
private string failureMessage;
public virtual string FailureMessage
{
get { return failureMessage; }
private set { failureMessage = value; }
}
protected string description;
public virtual string Description
{
get { return description; }
private set { description = value; }
}
public int Reward
{
get { return Prefab.Reward; }
}
public bool Completed
{
get { return completed; }
set { completed = value; }
}
public virtual bool AllowRespawn
{
get { return true; }
}
public virtual int TeamCount
{
get { return 1; }
}
public virtual IEnumerable<Vector2> SonarPositions
{
get { return Enumerable.Empty<Vector2>(); }
}
public string SonarLabel
{
get { return Prefab.SonarLabel; }
}
public string SonarIconIdentifier
{
get { return Prefab.SonarIconIdentifier; }
}
public readonly Location[] Locations;
public Mission(MissionPrefab prefab, Location[] locations)
{
System.Diagnostics.Debug.Assert(locations.Length == 2);
Prefab = prefab;
description = prefab.Description;
successMessage = prefab.SuccessMessage;
FailureMessage = prefab.FailureMessage;
Headers = new List<string>(prefab.Headers);
Messages = new List<string>(prefab.Messages);
Locations = locations;
for (int n = 0; n < 2; n++)
{
if (description != null) description = description.Replace("[location" + (n + 1) + "]", locations[n].Name);
if (successMessage != null) successMessage = successMessage.Replace("[location" + (n + 1) + "]", locations[n].Name);
if (failureMessage != null) failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locations[n].Name);
for (int m = 0; m < Messages.Count; m++)
{
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locations[n].Name);
}
}
if (description != null) description = description.Replace("[reward]", Reward.ToString("N0"));
if (successMessage != null) successMessage = successMessage.Replace("[reward]", Reward.ToString("N0"));
if (failureMessage != null) failureMessage = failureMessage.Replace("[reward]", Reward.ToString("N0"));
for (int m = 0; m < Messages.Count; m++)
{
Messages[m] = Messages[m].Replace("[reward]", Reward.ToString("N0"));
}
}
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
{
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
}
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
{
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
if (missionType == MissionType.None)
{
return null;
}
else
{
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.Type)) != 0));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
if (requireCorrectLocationType)
{
allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1]));
}
if (allowedMissions.Count == 0)
{
return null;
}
int probabilitySum = allowedMissions.Sum(m => m.Commonness);
int randomNumber = rand.NextInt32() % probabilitySum;
foreach (MissionPrefab missionPrefab in allowedMissions)
{
if (randomNumber <= missionPrefab.Commonness)
{
return missionPrefab.Instantiate(locations);
}
randomNumber -= missionPrefab.Commonness;
}
return null;
}
public virtual void Start(Level level) { }
public virtual void Update(float deltaTime) { }
public virtual void AssignTeamIDs(List<Networking.Client> clients)
{
clients.ForEach(c => c.TeamID = Character.TeamType.Team1);
}
protected void ShowMessage(int missionState)
{
ShowMessageProjSpecific(missionState);
}
partial void ShowMessageProjSpecific(int missionState);
/// <summary>
/// End the mission and give a reward if it was completed successfully
/// </summary>
public virtual void End()
{
completed = true;
GiveReward();
}
public void GiveReward()
{
if (!(GameMain.GameSession.GameMode is CampaignMode mode)) { return; }
mode.Money += Reward;
}
}
}
@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
[Flags]
public enum MissionType
{
None = 0x0,
Salvage = 0x1,
Monster = 0x2,
Cargo = 0x4,
Combat = 0x8,
All = 0xf
}
partial class MissionPrefab
{
public static readonly List<MissionPrefab> List = new List<MissionPrefab>();
private static readonly Dictionary<MissionType, Type> missionClasses = new Dictionary<MissionType, Type>()
{
{ MissionType.Salvage, typeof(SalvageMission) },
{ MissionType.Monster, typeof(MonsterMission) },
{ MissionType.Cargo, typeof(CargoMission) },
{ MissionType.Combat, typeof(CombatMission) },
};
private readonly ConstructorInfo constructor;
public readonly MissionType Type;
public readonly bool MultiplayerOnly, SingleplayerOnly;
public readonly string Identifier;
public readonly string TextIdentifier;
public readonly string Name;
public readonly string Description;
public readonly string SuccessMessage;
public readonly string FailureMessage;
public readonly string SonarLabel;
public readonly string SonarIconIdentifier;
public readonly string AchievementIdentifier;
public readonly int Commonness;
public readonly int Reward;
public readonly List<string> Headers;
public readonly List<string> Messages;
//the mission can only be received when travelling from Pair.First to Pair.Second
public readonly List<Pair<string, string>> AllowedLocationTypes;
public readonly XElement ConfigElement;
public static void Init()
{
List.Clear();
var files = GameMain.Instance.GetFilesOfType(ContentType.Missions);
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
bool allowOverride = false;
var mainElement = doc.Root;
if (mainElement.IsOverride())
{
allowOverride = true;
mainElement = mainElement.FirstElement();
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
var identifier = element.GetAttributeString("identifier", string.Empty);
var duplicate = List.Find(m => m.Identifier == identifier);
if (duplicate != null)
{
if (allowOverride || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding a mission with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow);
List.Remove(duplicate);
}
else
{
DebugConsole.ThrowError($"Duplicate mission found with the identifier '{identifier}' in file '{file.Path}'! Add <override></override> tags as the parent of the mission definition to allow overriding.");
// TODO: Don't allow adding duplicates when the issue with multiple missions is solved.
//continue;
}
}
List.Add(new MissionPrefab(element));
}
}
}
public MissionPrefab(XElement element)
{
ConfigElement = element;
Identifier = element.GetAttributeString("identifier", "");
TextIdentifier = element.GetAttributeString("textidentifier", null) ?? Identifier;
Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", "");
Reward = element.GetAttributeInt("reward", 1);
Commonness = element.GetAttributeInt("commonness", 1);
SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully");
FailureMessage = TextManager.Get("MissionFailure." + TextIdentifier, true) ?? "";
if (string.IsNullOrEmpty(FailureMessage) && TextManager.ContainsTag("missionfailed"))
{
FailureMessage = TextManager.Get("missionfailed", returnNull: true) ?? "";
}
if (string.IsNullOrEmpty(FailureMessage) && GameMain.Config.Language == "English")
{
FailureMessage = element.GetAttributeString("failuremessage", "");
}
SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false);
AchievementIdentifier = element.GetAttributeString("achievementidentifier", "");
Headers = new List<string>();
Messages = new List<string>();
AllowedLocationTypes = new List<Pair<string, string>>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "message":
int index = Messages.Count;
Headers.Add(TextManager.Get("MissionHeader" + index + "." + TextIdentifier, true) ?? subElement.GetAttributeString("header", ""));
Messages.Add(TextManager.Get("MissionMessage" + index + "." + TextIdentifier, true) ?? subElement.GetAttributeString("text", ""));
break;
case "locationtype":
AllowedLocationTypes.Add(new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
break;
}
}
string missionTypeName = element.GetAttributeString("type", "");
if (!Enum.TryParse(missionTypeName, out Type))
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
return;
}
if (Type == MissionType.None)
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
return;
}
constructor = missionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public bool IsAllowed(Location from, Location to)
{
foreach (Pair<string, string> allowedLocationType in AllowedLocationTypes)
{
if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
public Mission Instantiate(Location[] locations)
{
return constructor?.Invoke(new object[] { this, locations }) as Mission;
}
}
}
@@ -0,0 +1,197 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
partial class MonsterMission : Mission
{
private readonly string monsterFile;
private readonly int monsterCount;
//string = filename, point = min,max
private readonly HashSet<Tuple<string, Point>> monsterFiles = new HashSet<Tuple<string, Point>>();
private readonly List<Character> monsters = new List<Character>();
private readonly List<Vector2> sonarPositions = new List<Vector2>();
private readonly List<Vector2> tempSonarPositions = new List<Vector2>();
private readonly float maxSonarMarkerDistance = 10000.0f;
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return sonarPositions;
}
}
}
public MonsterMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", null);
if (!string.IsNullOrEmpty(monsterFile))
{
var characterPrefab = CharacterPrefab.FindByFilePath(monsterFile);
if (characterPrefab != null)
{
monsterFile = characterPrefab.Identifier;
}
}
maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f);
monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255);
string monsterFileName = monsterFile;
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
string monster = monsterElement.GetAttributeString("character", string.Empty);
if (monsterFileName == null)
{
monsterFileName = monster;
}
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = monsterElement.GetAttributeInt("amount", 1);
}
int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
monsterFiles.Add(new Tuple<string, Point>(monster, new Point(min, max)));
}
description = description.Replace("[monster]",
TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFileName)));
}
public override void Start(Level level)
{
if (monsters.Count > 0)
{
throw new Exception($"monsters.Count > 0 ({monsters.Count})");
}
if (tempSonarPositions.Count > 0)
{
throw new Exception($"tempSonarPositions.Count > 0 ({tempSonarPositions.Count})");
}
if (!IsClient)
{
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
if (!string.IsNullOrEmpty(monsterFile))
{
for (int i = 0; i < monsterCount; i++)
{
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
foreach (var monster in monsterFiles)
{
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
for (int i = 0; i < amount; i++)
{
monsters.Add(Character.Create(monster.Item1, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
InitializeMonsters(monsters);
}
}
private void InitializeMonsters(IEnumerable<Character> monsters)
{
monsters.ForEach(m => m.Enabled = false);
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
foreach (Character monster in monsters)
{
tempSonarPositions.Add(monster.WorldPosition + Rand.Vector(maxSonarMarkerDistance));
}
if (monsters.Count() != tempSonarPositions.Count)
{
throw new Exception($"monsters.Count != tempSonarPositions.Count ({monsters.Count()} != {tempSonarPositions.Count})");
}
}
public override void Update(float deltaTime)
{
switch (State)
{
case 0:
//keep sonar markers within maxSonarMarkerDistance from the monster(s)
for (int i = 0; i < tempSonarPositions.Count; i++)
{
if (monsters.Count != tempSonarPositions.Count)
{
throw new Exception($"monsters.Count != tempSonarPositions.Count ({monsters.Count} != {tempSonarPositions.Count})");
}
if (i < 0 || i >= monsters.Count)
{
throw new Exception($"Index {i} outside of bounds 0-{monsters.Count} ({tempSonarPositions.Count})");
}
if (monsters[i].Removed || monsters[i].IsDead) { continue; }
Vector2 diff = tempSonarPositions[i] - monsters[i].Position;
float maxDist = maxSonarMarkerDistance;
Submarine refSub = Character.Controlled?.Submarine ?? Submarine.MainSub;
if (refSub != null)
{
Vector2 refPos = refSub == null ? Vector2.Zero : refSub.WorldPosition;
float subDist = Vector2.Distance(refPos, tempSonarPositions[i]) / maxDist;
maxDist = Math.Min(subDist * subDist * maxDist, maxDist);
maxDist = Math.Min(Vector2.Distance(refPos, monsters[i].Position), maxDist);
}
if (diff.LengthSquared() > maxDist * maxDist)
{
tempSonarPositions[i] = monsters[i].Position + Vector2.Normalize(diff) * maxDist;
}
}
sonarPositions.Clear();
for (int i = 0; i < monsters.Count; i++)
{
if (monsters[i].Removed || monsters[i].IsDead) { continue; }
//don't add another label if there's another monster roughly at the same spot
if (sonarPositions.All(p => Vector2.DistanceSquared(p, tempSonarPositions[i]) > 1000.0f * 1000.0f))
{
sonarPositions.Add(tempSonarPositions[i]);
}
}
if (!IsClient && monsters.All(m => IsEliminated(m)))
{
State = 1;
}
break;
}
}
public override void End()
{
tempSonarPositions.Clear();
monsters.Clear();
if (State < 1) { return; }
GiveReward();
completed = true;
}
public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee;
}
}
@@ -0,0 +1,120 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class SalvageMission : Mission
{
private readonly ItemPrefab itemPrefab;
private Item item;
private readonly Level.PositionType spawnPositionType;
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (item == null)
{
Enumerable.Empty<Vector2>();
}
else
{
yield return ConvertUnits.ToDisplayUnits(item.SimPosition);
}
}
}
public SalvageMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
if (prefab.ConfigElement.Attribute("itemname") != null)
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
}
}
else
{
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier);
}
}
string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) ||
!Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType))
{
spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin;
}
}
public override void Start(Level level)
{
if (!IsClient)
{
//ruin items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin ? 0.0f : Level.Loaded.Size.X * 0.3f;
Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
item = new Item(itemPrefab, position, null);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
if (item.HasTag("alien"))
{
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) { continue; }
if (itemContainer.Combine(item, user: null)) { break; } // Placement successful
}
}
}
}
public override void Update(float deltaTime)
{
if (IsClient)
{
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
return;
}
switch (State)
{
case 0:
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
if (item.CurrentHull?.Submarine == null) { return; }
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
State = 2;
break;
}
}
public override void End()
{
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) { return; }
item?.Remove();
item = null;
GiveReward();
completed = true;
}
}
}
@@ -0,0 +1,328 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class MonsterEvent : ScriptedEvent
{
private readonly string speciesName;
private readonly int minAmount, maxAmount;
private List<Character> monsters;
private readonly bool spawnDeep;
private Vector2? spawnPos;
private readonly bool disallowed;
private readonly Level.PositionType spawnPosType;
private bool spawnPending;
public override Vector2 DebugDrawPos
{
get { return spawnPos ?? Vector2.Zero; }
}
public override string ToString()
{
if (maxAmount <= 1)
{
return "MonsterEvent (" + speciesName + ")";
}
else if (minAmount < maxAmount)
{
return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")";
}
else
{
return "MonsterEvent (" + speciesName + " x" + maxAmount + ")";
}
}
public MonsterEvent(ScriptedEventPrefab prefab)
: base (prefab)
{
speciesName = prefab.ConfigElement.GetAttributeString("characterfile", "");
CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesName);
if (characterPrefab != null)
{
speciesName = characterPrefab.Identifier;
}
if (string.IsNullOrEmpty(speciesName))
{
throw new Exception("speciesname is null!");
}
int defaultAmount = prefab.ConfigElement.GetAttributeInt("amount", 1);
minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
{
spawnPosType = Level.PositionType.MainPath;
}
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
if (GameMain.NetworkMember != null)
{
List<string> monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList();
string tryKey = monsterNames.Find(s => speciesName.ToLower() == s.ToLower());
if (!string.IsNullOrWhiteSpace(tryKey))
{
if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) disallowed = true; //spawn was disallowed by host
}
}
}
public override IEnumerable<ContentFile> GetFilesToPreload()
{
string path = CharacterPrefab.FindBySpeciesName(speciesName)?.FilePath;
if (string.IsNullOrWhiteSpace(path))
{
DebugConsole.ThrowError($"Failed to find config file for species \"{speciesName}\"");
yield break;
}
else
{
yield return new ContentFile(path, ContentType.Character);
}
}
public override bool CanAffectSubImmediately(Level level)
{
float maxRange = Items.Components.Sonar.DefaultSonarRange * 0.8f;
List<Vector2> positions = GetAvailableSpawnPositions();
foreach (Vector2 position in positions)
{
if (Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition) < maxRange * maxRange)
{
return true;
}
}
return false;
}
public override void Init(bool affectSubImmediately)
{
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("Initialized MonsterEvent (" + speciesName + ")", Color.White);
}
}
private List<Vector2> GetAvailableSpawnPositions()
{
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType));
List<Vector2> positions = new List<Vector2>();
foreach (var allowedPosition in availablePositions)
{
if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(allowedPosition.Position.ToVector2())))) { continue; }
positions.Add(allowedPosition.Position.ToVector2());
}
if (spawnDeep)
{
for (int i = 0; i < positions.Count; i++)
{
positions[i] = new Vector2(positions[i].X, positions[i].Y - Level.Loaded.Size.Y);
}
}
positions.RemoveAll(pos => pos.Y < Level.Loaded.GetBottomPosition(pos.X).Y);
return positions;
}
private void FindSpawnPosition(bool affectSubImmediately)
{
if (disallowed) { return; }
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
if (affectSubImmediately && spawnPosType != Level.PositionType.Ruin)
{
if (availablePositions.Count == 0)
{
//no suitable position found, disable the event
Finished();
return;
}
float closestDist = float.PositiveInfinity;
//find the closest spawnposition that isn't too close to any of the subs
foreach (Vector2 position in availablePositions)
{
float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.IsOutpost) { continue; }
float minDistToSub = GetMinDistanceToSub(sub);
if (dist > minDistToSub * minDistToSub && dist < closestDist)
{
closestDist = dist;
spawnPos = position;
}
}
}
//only found a spawnpos that's very far from the sub, pick one that's closer
//and wait for the sub to move further before spawning
if (closestDist > 15000.0f * 15000.0f)
{
foreach (Vector2 position in availablePositions)
{
float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
spawnPos = position;
}
}
}
}
else
{
float minDist = spawnPosType == Level.PositionType.Ruin ? 0.0f : 20000.0f;
availablePositions.RemoveAll(p => Vector2.Distance(Submarine.MainSub.WorldPosition, p) < minDist);
if (availablePositions.Count == 0)
{
//no suitable position found, disable the event
Finished();
return;
}
spawnPos = availablePositions[Rand.Int(availablePositions.Count, Rand.RandSync.Server)];
}
spawnPending = true;
}
private float GetMinDistanceToSub(Submarine submarine)
{
//9000 units is slightly less than the default range of the sonar
return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), 9000.0f);
}
public override void Update(float deltaTime)
{
if (disallowed)
{
Finished();
return;
}
if (isFinished) { return; }
if (spawnPos == null)
{
FindSpawnPosition(affectSubImmediately: true);
spawnPending = true;
}
bool spawnReady = false;
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.IsOutpost) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
//unnecessary monsters in places the players might never visit during the round
if (spawnPosType == Level.PositionType.Ruin ||
spawnPosType == Level.PositionType.Cave)
{
bool someoneNearby = false;
float minDist = Items.Components.Sonar.DefaultSonarRange * 0.8f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.IsOutpost) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c == Character.Controlled || c.IsRemotePlayer)
{
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
}
if (!someoneNearby) { return; }
}
spawnPending = false;
//+1 because Range returns an integer less than the max value
int amount = Rand.Range(minAmount, maxAmount + 1);
monsters = new List<Character>();
float offsetAmount = spawnPosType == Level.PositionType.MainPath ? 1000 : 100;
for (int i = 0; i < amount; i++)
{
CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
monsters.Add(Character.Create(speciesName, spawnPos.Value + Rand.Vector(offsetAmount), Level.Loaded.Seed + i.ToString(), null, false, true, true));
if (monsters.Count == amount)
{
spawnReady = true;
//this will do nothing if the monsters have no swarm behavior defined,
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
}
}, Rand.Range(0f, amount / 2));
}
}
if (!spawnReady) { return; }
Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
#if CLIENT
if (Character.Controlled != null) { targetEntity = Character.Controlled; }
#endif
bool monstersDead = true;
foreach (Character monster in monsters)
{
if (!monster.IsDead)
{
monstersDead = false;
if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f)
{
break;
}
}
}
if (monstersDead) { Finished(); }
}
}
}
@@ -0,0 +1,86 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class ScriptedEvent
{
protected bool isFinished;
private readonly ScriptedEventPrefab prefab;
public bool IsFinished
{
get { return isFinished; }
}
public override string ToString()
{
return "ScriptedEvent (" + prefab.EventType.ToString() +")";
}
public virtual Vector2 DebugDrawPos
{
get
{
return Vector2.Zero;
}
}
public ScriptedEvent(ScriptedEventPrefab prefab)
{
this.prefab = prefab;
}
public virtual IEnumerable<ContentFile> GetFilesToPreload()
{
yield break;
}
public virtual void Init(bool affectSubImmediately)
{
}
public virtual void Update(float deltaTime)
{
}
public virtual void Finished()
{
isFinished = true;
}
public virtual bool CanAffectSubImmediately(Level level)
{
return true;
}
/*public static List<ScriptedEvent> GenerateInitialEvents(Random random, Level level)
{
if (ScriptedEventPrefab.List == null)
{
ScriptedEventPrefab.LoadPrefabs();
}
List<ScriptedEvent> events = new List<ScriptedEvent>();
foreach (ScriptedEventPrefab scriptedEvent in ScriptedEventPrefab.List)
{
int minCount = scriptedEvent.MinEventCount.ContainsKey(level.GenerationParams.Name) ?
scriptedEvent.MinEventCount[level.GenerationParams.Name] : scriptedEvent.MinEventCount[""];
int maxCount = scriptedEvent.MaxEventCount.ContainsKey(level.GenerationParams.Name) ?
scriptedEvent.MaxEventCount[level.GenerationParams.Name] : scriptedEvent.MaxEventCount[""];
minCount = Math.Min(minCount, maxCount);
int count = random.Next(maxCount - minCount) + minCount;
for (int i = 0; i < count; i++)
{
ScriptedEvent eventInstance = scriptedEvent.CreateInstance();
events.Add(eventInstance);
}
}
return events;
}*/
}
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
class ScriptedEventPrefab
{
public readonly XElement ConfigElement;
public readonly Type EventType;
public readonly string MusicType;
public float Commonness;
public ScriptedEventPrefab(XElement element)
{
ConfigElement = element;
MusicType = element.GetAttributeString("musictype", "default");
try
{
EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true);
if (EventType == null)
{
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
}
}
catch
{
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
}
Commonness = element.GetAttributeFloat("commonness", 1.0f);
}
public ScriptedEvent CreateInstance()
{
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(ScriptedEventPrefab) });
object instance = null;
try
{
instance = constructor.Invoke(new object[] { this });
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
return (ScriptedEvent)instance;
}
}
}
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class ScriptedEventSet
{
public static List<ScriptedEventSet> List
{
get;
private set;
}
//0-100
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
public readonly bool ChooseRandom;
public readonly float MinDistanceTraveled;
public readonly float MinMissionTime;
//the events in this set are delayed if the current EventManager intensity is not between these values
public readonly float MinIntensity, MaxIntensity;
public readonly bool AllowAtStart;
public readonly bool PerRuin;
public readonly Dictionary<string, float> Commonness;
public readonly List<ScriptedEventPrefab> EventPrefabs;
public readonly List<ScriptedEventSet> ChildSets;
public string DebugIdentifier
{
get;
private set;
} = "";
private ScriptedEventSet(XElement element, string debugIdentifier)
{
DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier;
Commonness = new Dictionary<string, float>();
EventPrefabs = new List<ScriptedEventPrefab>();
ChildSets = new List<ScriptedEventSet>();
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
MinIntensity = element.GetAttributeFloat("minintensity", 0.0f);
MaxIntensity = Math.Max(element.GetAttributeFloat("maxintensity", 100.0f), MinIntensity);
ChooseRandom = element.GetAttributeBool("chooserandom", false);
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
AllowAtStart = element.GetAttributeBool("allowatstart", false);
PerRuin = element.GetAttributeBool("perruin", false);
Commonness[""] = 1.0f;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "commonness":
Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f);
foreach (XElement overrideElement in subElement.Elements())
{
if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase))
{
string levelType = overrideElement.GetAttributeString("leveltype", "");
if (!Commonness.ContainsKey(levelType))
{
Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f));
}
}
}
break;
case "eventset":
ChildSets.Add(new ScriptedEventSet(subElement, this.DebugIdentifier + "-" + ChildSets.Count));
break;
default:
EventPrefabs.Add(new ScriptedEventPrefab(subElement));
break;
}
}
}
public float GetCommonness(Level level)
{
string key = level.GenerationParams?.Name ?? "";
return Commonness.ContainsKey(key) ?
Commonness[key] : Commonness[""];
}
public static void LoadPrefabs()
{
List = new List<ScriptedEventSet>();
var configFiles = GameMain.Instance.GetFilesOfType(ContentType.RandomEvents);
if (!configFiles.Any())
{
DebugConsole.ThrowError("No config files for random events found in the selected content package");
return;
}
foreach (ContentFile configFile in configFiles)
{
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { continue; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
DebugConsole.NewMessage($"Overriding all random events using the file {configFile.Path}", Color.Yellow);
List.Clear();
}
int i = 0;
foreach (XElement element in doc.Root.Elements())
{
if (!element.Name.ToString().Equals("eventset", StringComparison.OrdinalIgnoreCase)) { continue; }
List.Add(new ScriptedEventSet(element, i.ToString()));
i++;
}
}
}
}
}
@@ -0,0 +1,114 @@
using System.Collections.Generic;
using System;
using System.Linq;
namespace Barotrauma.Extensions
{
public static class IEnumerableExtensions
{
/// <summary>
/// Randomizes the collection (using OrderBy) and returns it.
/// </summary>
public static IOrderedEnumerable<T> Randomize<T>(this IEnumerable<T> source, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return source.OrderBy(i => Rand.Value(randSync));
}
/// <summary>
/// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm.
/// </summary>
public static void Shuffle<T>(this IList<T> list, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = Rand.Int(n + 1, randSync);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
public static T GetRandom<T>(this IEnumerable<T> source, Func<T, bool> predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return source.Where(predicate).GetRandom(randSync);
}
public static T GetRandom<T>(this IEnumerable<T> source, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
int count = source.Count();
return count == 0 ? default(T) : source.ElementAt(Rand.Range(0, count, randSync));
}
/// <summary>
/// Executes an action that modifies the collection on each element (such as removing items from the list).
/// Creates a temporary list.
/// </summary>
public static void ForEachMod<T>(this IEnumerable<T> source, Action<T> action)
{
var temp = new List<T>(source);
temp.ForEach(action);
}
/// <summary>
/// Generic version of List.ForEach.
/// Performs the specified action on each element of the collection (short hand for a foreach loop).
/// </summary>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
{
action(item);
}
}
/// <summary>
/// Shorthand for !source.Any(predicate) -> i.e. not any.
/// </summary>
public static bool None<T>(this IEnumerable<T> source, Func<T, bool> predicate = null)
{
if (predicate == null)
{
return !source.Any();
}
else
{
return !source.Any(predicate);
}
}
public static bool Multiple<T>(this IEnumerable<T> source, Func<T, bool> predicate = null)
{
if (predicate == null)
{
return source.Count() > 1;
}
else
{
return source.Count(predicate) > 1;
}
}
public static IEnumerable<T> ToEnumerable<T>(this T item)
{
yield return item;
}
// source: https://stackoverflow.com/questions/19237868/get-all-children-to-one-list-recursive-c-sharp
public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
{
var result = source.SelectMany(selector);
if (!result.Any())
{
return result;
}
return result.Concat(result.SelectManyRecursive(selector));
}
public static void AddIfNotNull<T>(this IList<T> source, T value)
{
if (value != null) { source.Add(value); }
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class PointExtensions
{
public static Point Multiply(this Point p, float f)
{
return new Point((int)(p.X * f), (int)(p.Y * f));
}
public static Point Multiply(this Point p, int i)
{
return new Point(p.X * i, p.Y * i);
}
public static Point Multiply(this Point p, Vector2 v)
{
return new Point((int)(p.X * v.X), (int)(p.Y * v.Y));
}
public static Point Divide(this Point p, int i)
{
if (i == 0) { return Point.Zero; }
return new Point(p.X / i, p.Y / i);
}
public static Point Divide(this Point p, float f)
{
if (f == 0) { return Point.Zero; }
return new Point((int)(p.X / f), (int)(p.Y / f));
}
public static Point Divide(this Point p, Vector2 v)
{
if (v.X == 0 || v.Y == 0) { return Point.Zero; }
return new Point((int)(p.X / v.X), (int)(p.Y / v.Y));
}
/// <summary>
/// Negates the X and Y components.
/// </summary>
public static Point Inverse(this Point p)
{
return new Point(-p.X, -p.Y);
}
/// <summary>
/// Flips the X and Y components.
/// </summary>
public static Point Flip(this Point p)
{
return new Point(p.Y, p.X);
}
public static Point Clamp(this Point p, Point min, Point max)
{
return new Point(MathHelper.Clamp(p.X, min.X, max.X), MathHelper.Clamp(p.Y, min.Y, max.Y));
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class RectangleExtensions
{
public static Rectangle Multiply(this Rectangle rect, float f)
{
Vector2 location = new Vector2(rect.X, rect.Y) * f;
return new Rectangle(new Point((int)location.X, (int)location.Y), rect.MultiplySize(f));
}
public static Rectangle Divide(this Rectangle rect, float f)
{
Vector2 location = new Vector2(rect.X, rect.Y) / f;
return new Rectangle(new Point((int)location.X, (int)location.Y), rect.DivideSize(f));
}
public static Point DivideSize(this Rectangle rect, float f)
{
return new Point((int)(rect.Width / f), (int)(rect.Height / f));
}
public static Point DivideSize(this Rectangle rect, Vector2 f)
{
return new Point((int)(rect.Width / f.X), (int)(rect.Height / f.Y));
}
public static Point MultiplySize(this Rectangle rect, float f)
{
return new Point((int)(rect.Width * f), (int)(rect.Height * f));
}
public static Point MultiplySize(this Rectangle rect, Vector2 f)
{
return new Point((int)(rect.Width * f.X), (int)(rect.Height * f.Y));
}
public static Vector2 CalculateRelativeSize(this Rectangle rect, Rectangle relativeRect)
{
return new Vector2(rect.Width, rect.Height) / new Vector2(relativeRect.Width, relativeRect.Height);
}
public static Rectangle ScaleSize(this Rectangle rect, Rectangle relativeTo)
{
return rect.ScaleSize(rect.CalculateRelativeSize(relativeTo));
}
public static Rectangle ScaleSize(this Rectangle rect, Vector2 scale)
{
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
public static Rectangle ScaleSize(this Rectangle rect, float scale)
{
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
}
}
@@ -0,0 +1,169 @@
using System.Globalization;
using Microsoft.Xna.Framework;
using System.Linq;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
public static class StringFormatter
{
public static string Replace(this string s, string replacement, Func<char, bool> predicate)
{
var newString = new string[s.Length];
for (int i = 0; i < s.Length; i++)
{
char letter = s[i];
string newLetter = letter.ToString();
if (predicate(letter))
{
newLetter = replacement;
}
newString[i] = newLetter;
}
return new string(newString.SelectMany(str => str.ToCharArray()).ToArray());
}
public static string Remove(this string s, string substring)
{
return s.Replace(substring, string.Empty);
}
public static string Remove(this string s, Func<char, bool> predicate)
{
return new string(s.ToCharArray().Where(c => !predicate(c)).ToArray());
}
public static string RemoveWhitespace(this string s)
{
return s.Remove(c => char.IsWhiteSpace(c));
}
public static string FormatSingleDecimal(this float value)
{
return value.ToString("F1", CultureInfo.InvariantCulture);
}
public static string FormatDoubleDecimal(this float value)
{
return value.ToString("F2", CultureInfo.InvariantCulture);
}
public static string FormatZeroDecimal(this float value)
{
return value.ToString("F0", CultureInfo.InvariantCulture);
}
public static string Format(this float value, int decimalCount)
{
return value.ToString($"F{decimalCount.ToString()}", CultureInfo.InvariantCulture);
}
public static string FormatSingleDecimal(this Vector2 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()})";
}
public static string FormatDoubleDecimal(this Vector2 value)
{
return $"({value.X.FormatDoubleDecimal()}, {value.Y.FormatDoubleDecimal()})";
}
public static string FormatZeroDecimal(this Vector2 value)
{
return $"({value.X.FormatZeroDecimal()}, {value.Y.FormatZeroDecimal()})";
}
public static string Format(this Vector2 value, int decimalCount)
{
return $"({value.X.Format(decimalCount)}, {value.Y.Format(decimalCount)})";
}
/// <summary>
/// Capitalises the first letter (invariant) and forces the rest to lower case (invariant).
/// </summary>
public static string CapitaliseFirstInvariant(this string s)
{
if (string.IsNullOrEmpty(s)) { return string.Empty; }
return s.Substring(0, 1).ToUpperInvariant() + s.Substring(1, s.Length - 1).ToLowerInvariant();
}
/// <summary>
/// Adds spaces into a CamelCase string.
/// </summary>
public static string FormatCamelCaseWithSpaces(this string str)
{
return new string(InsertSpacesBeforeCaps(str).ToArray());
IEnumerable<char> InsertSpacesBeforeCaps(IEnumerable<char> input)
{
int i = 0;
int lastChar = input.Count() - 1;
foreach (char c in input)
{
if (char.IsUpper(c) && i > 0)
{
yield return ' ';
}
yield return c;
i++;
}
}
}
public static ICollection<string> ParseCommaSeparatedStringToCollection(string input, ICollection<string> texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(','))
{
if (string.IsNullOrWhiteSpace(value)) { continue; }
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
public static ICollection<string> ParseSeparatedStringToCollection(string input, string[] separators, ICollection<string> texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(separators, StringSplitOptions.RemoveEmptyEntries))
{
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
}
}
@@ -0,0 +1,96 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class VectorExtensions
{
/// <summary>
/// Unity's Angle implementation without the conversion to degrees.
/// Returns the angle in radians between two vectors.
/// 0 - Pi.
/// </summary>
public static float Angle(this Vector2 from, Vector2 to)
{
return (float)Math.Acos(MathHelper.Clamp(Vector2.Dot(Vector2.Normalize(from), Vector2.Normalize(to)), -1f, 1f));
}
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians).
/// </summary>
public static Vector2 Forward(float radians, float length = 1)
{
return new Vector2((float)Math.Cos(radians), (float)Math.Sin(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians).
/// </summary>
public static Vector2 Backward(float radians, float length = 1)
{
return -Forward(radians, length);
}
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 ForwardFlipped(float radians, float length = 1)
{
return new Vector2((float)Math.Sin(radians), (float)Math.Cos(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 BackwardFlipped(float radians, float length = 1)
{
return -ForwardFlipped(radians, length);
}
/// <summary>
/// Creates a normalized perpendicular vector to the right from a forward vector.
/// </summary>
public static Vector2 Right(this Vector2 forward)
{
var normV = Vector2.Normalize(forward);
return new Vector2(normV.Y, -normV.X);
}
/// <summary>
/// Creates a normalized perpendicular vector to the left from a forward vector.
/// </summary>
public static Vector2 Left(this Vector2 forward)
{
return -forward.Right();
}
/// <summary>
/// Transforms a vector relative to the given up vector.
/// </summary>
public static Vector2 TransformVector(this Vector2 v, Vector2 up)
{
up = Vector2.Normalize(up);
return (up * v.Y) + (up.Right() * v.X);
}
/// <summary>
/// Flips the x and y components.
/// </summary>
public static Vector2 Flip(this Vector2 v) => new Vector2(v.Y, v.X);
/// <summary>
/// Returns the sum of the x and y components.
/// </summary>
public static float Combine(this Vector2 v) => v.X + v.Y;
public static Vector2 Clamp(this Vector2 v, Vector2 min, Vector2 max)
{
return Vector2.Clamp(v, min, max);
}
public static bool NearlyEquals(this Vector2 v, Vector2 other)
{
return MathUtils.NearlyEqual(v.X, other.X) && MathUtils.NearlyEqual(v.Y, other.Y);
}
}
}
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
public class PerformanceCounter
{
public long TotalFrames { get; private set; }
public double TotalSeconds { get; private set; }
public double AverageFramesPerSecond { get; private set; }
public double CurrentFramesPerSecond { get; private set; }
public const int MaximumSamples = 10;
private Queue<double> sampleBuffer = new Queue<double>();
private Dictionary<string, Queue<long>> elapsedTicks = new Dictionary<string, Queue<long>>();
private Dictionary<string, long> avgTicksPerFrame = new Dictionary<string, long>();
#if CLIENT
internal Graph UpdateTimeGraph = new Graph(500), UpdateIterationsGraph = new Graph(500), DrawTimeGraph = new Graph(500);
#endif
public IEnumerable<string> GetSavedIdentifiers
{
get { return avgTicksPerFrame.Keys; }
}
public void AddElapsedTicks(string identifier, long ticks)
{
if (!elapsedTicks.ContainsKey(identifier)) elapsedTicks.Add(identifier, new Queue<long>());
elapsedTicks[identifier].Enqueue(ticks);
if (elapsedTicks[identifier].Count > MaximumSamples)
{
elapsedTicks[identifier].Dequeue();
avgTicksPerFrame[identifier] = (long)elapsedTicks[identifier].Average(i => i);
}
}
public float GetAverageElapsedMillisecs(string identifier)
{
if (!avgTicksPerFrame.ContainsKey(identifier)) return 0.0f;
return avgTicksPerFrame[identifier] / (float)TimeSpan.TicksPerMillisecond;
}
public bool Update(double deltaTime)
{
if (deltaTime == 0.0f) { return false; }
CurrentFramesPerSecond = (1.0 / deltaTime);
sampleBuffer.Enqueue(CurrentFramesPerSecond);
if (sampleBuffer.Count > MaximumSamples)
{
sampleBuffer.Dequeue();
AverageFramesPerSecond = sampleBuffer.Average(i => i);
}
else
{
AverageFramesPerSecond = CurrentFramesPerSecond;
}
if (AverageFramesPerSecond < 0 || AverageFramesPerSecond > 500) { }
TotalFrames++;
TotalSeconds += deltaTime;
return true;
}
}
}
@@ -0,0 +1,131 @@
using GameAnalyticsSDK.Net;
using System;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
namespace Barotrauma
{
public static class GameAnalyticsManager
{
private static HashSet<string> sentEventIdentifiers = new HashSet<string>();
public static void Init()
{
#if DEBUG
try
{
GameAnalytics.SetEnabledInfoLog(true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
GameSettings.SendUserStatistics = false;
return;
}
#endif
string exePath = Assembly.GetEntryAssembly().Location;
string exeName = null;
Md5Hash exeHash = null;
exeName = Path.GetFileNameWithoutExtension(exePath).Replace(":", "");
var md5 = MD5.Create();
try
{
using (var stream = File.OpenRead(exePath))
{
exeHash = new Md5Hash(stream);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e);
}
try
{
GameAnalytics.ConfigureBuild(GameMain.Version.ToString()
+ (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash));
GameAnalytics.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor");
GameAnalytics.Initialize("a3a073c20982de7c15d21e840e149122", "9010ad9a671233b8d9610d76cec8c897d9ff3ba7");
GameAnalytics.AddDesignEvent("Executable:"
+ (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash));
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
GameSettings.SendUserStatistics = false;
return;
}
if (GameMain.Config?.SelectedContentPackages.Count > 0)
{
StringBuilder sb = new StringBuilder("ContentPackage: ");
int i = 0;
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
{
string trimmedName = cp.Name.Replace(":", "").Replace(" ", "");
sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length)));
if (i < GameMain.Config.SelectedContentPackages.Count - 1) { sb.Append(" "); }
}
GameAnalytics.AddDesignEvent(sb.ToString());
}
}
/// <summary>
/// Adds an error event to GameAnalytics if an event with the same identifier has not been added yet.
/// </summary>
public static void AddErrorEventOnce(string identifier, EGAErrorSeverity errorSeverity, string message)
{
if (!GameSettings.SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.SelectedPackages != null)
{
if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
{
message = "[MODDED] " + message;
}
}
GameAnalytics.AddErrorEvent(errorSeverity, message);
sentEventIdentifiers.Add(identifier);
}
public static void AddDesignEvent(string eventID)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddDesignEvent(eventID);
}
public static void AddDesignEvent(string eventID, double value)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddDesignEvent(eventID, value);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddProgressionEvent(progressionStatus, progression01);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddProgressionEvent(progressionStatus, progression01, progression02);
}
public static void SetCustomDimension01(string dimension)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.SetCustomDimension01(dimension);
}
}
}
@@ -0,0 +1,174 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
static class AutoItemPlacer
{
private static readonly List<Item> spawnedItems = new List<Item>();
public static bool OutputDebugInfo = false;
public static void PlaceIfNeeded(GameMode gameMode)
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
CampaignMode campaign = gameMode as CampaignMode;
if (campaign == null || !campaign.InitialSuppliesSpawned)
{
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.IsOutpost));
Place(subs);
}
if (campaign != null) { campaign.InitialSuppliesSpawned = true; }
}
}
private static void Place(IEnumerable<Submarine> subs)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
DebugConsole.ThrowError("Clients are not allowed to use AutoItemPlacer.\n" + Environment.StackTrace);
return;
}
int sizeApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(100);
var prefabsWithContainer = new List<ItemPrefab>(sizeApprox / 3);
var prefabsWithoutContainer = new List<ItemPrefab>(sizeApprox);
var removals = new List<ItemPrefab>();
foreach (Item item in Item.ItemList)
{
if (!subs.Contains(item.Submarine)) { continue; }
containers.AddRange(item.GetComponents<ItemContainer>());
}
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
if (!(prefab is ItemPrefab ip)) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)))
{
prefabsWithContainer.Add(ip);
}
else
{
prefabsWithoutContainer.Add(ip);
}
}
spawnedItems.Clear();
var validContainers = new Dictionary<ItemContainer, PreferredContainer>();
prefabsWithContainer.Shuffle();
// Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc)
for (int i = 0; i < prefabsWithContainer.Count; i++)
{
var itemPrefab = prefabsWithContainer[i];
if (itemPrefab == null) { continue; }
if (SpawnItems(itemPrefab))
{
removals.Add(itemPrefab);
}
}
// Remove containers that we successfully spawned items into so that they are not counted in in the second pass.
removals.ForEach(i => prefabsWithContainer.Remove(i));
// Another pass for items with containers because also they can spawn inside other items (like smg magazine)
prefabsWithContainer.ForEach(i => SpawnItems(i));
// Spawn items that don't have containers last
prefabsWithoutContainer.Shuffle();
prefabsWithoutContainer.ForEach(i => SpawnItems(i));
if (OutputDebugInfo)
{
DebugConsole.NewMessage("Automatically placed items: ");
foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct())
{
DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName));
}
}
bool SpawnItems(ItemPrefab itemPrefab)
{
if (itemPrefab == null)
{
string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return false;
}
bool success = false;
foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers)
{
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0) { continue; }
validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true);
if (validContainers.None())
{
validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: false);
}
foreach (var validContainer in validContainers)
{
if (SpawnItem(itemPrefab, containers, validContainer))
{
success = true;
}
}
}
return success;
}
}
private static Dictionary<ItemContainer, PreferredContainer> GetValidContainers(PreferredContainer preferredContainer, IEnumerable<ItemContainer> allContainers, Dictionary<ItemContainer, PreferredContainer> validContainers, bool primary)
{
validContainers.Clear();
foreach (ItemContainer container in allContainers)
{
if (!container.AutoFill) { continue; }
if (primary)
{
if (!ItemPrefab.IsContainerPreferred(preferredContainer.Primary, container)) { continue; }
}
else
{
if (!ItemPrefab.IsContainerPreferred(preferredContainer.Secondary, container)) { continue; }
}
if (!validContainers.ContainsKey(container))
{
validContainers.Add(container, preferredContainer);
}
}
return validContainers;
}
private static bool SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
bool success = false;
if (Rand.Value() > validContainer.Value.SpawnProbability) { return success; }
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1);
for (int i = 0; i < amount; i++)
{
if (validContainer.Key.Inventory.IsFull())
{
containers.Remove(validContainer.Key);
break;
}
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine);
spawnedItems.Add(item);
#if SERVER
Entity.Spawner.CreateNetworkEvent(item, remove: false);
#endif
validContainer.Key.Inventory.TryPutItem(item, null);
containers.AddRange(item.GetComponents<ItemContainer>());
success = true;
}
return success;
}
}
}
@@ -0,0 +1,213 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class PurchasedItem
{
public readonly ItemPrefab ItemPrefab;
public int Quantity;
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
{
this.ItemPrefab = itemPrefab;
this.Quantity = quantity;
}
}
class CargoManager
{
private readonly List<PurchasedItem> purchasedItems;
private readonly CampaignMode campaign;
public Action OnItemsChanged;
public List<PurchasedItem> PurchasedItems
{
get { return purchasedItems; }
}
public CargoManager(CampaignMode campaign)
{
purchasedItems = new List<PurchasedItem>();
this.campaign = campaign;
}
public void SetPurchasedItems(List<PurchasedItem> items)
{
purchasedItems.Clear();
purchasedItems.AddRange(items);
OnItemsChanged?.Invoke();
}
public void PurchaseItem(ItemPrefab item, int quantity = 1)
{
PurchasedItem purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item);
campaign.Money -= item.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
if (purchasedItem != null)
{
purchasedItem.Quantity += quantity;
}
else
{
purchasedItem = new PurchasedItem(item, quantity);
purchasedItems.Add(purchasedItem);
}
OnItemsChanged?.Invoke();
}
public void SellItem(PurchasedItem purchasedItem, int quantity = 1)
{
quantity = Math.Min(purchasedItem.Quantity, quantity);
campaign.Money += purchasedItem.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
purchasedItem.Quantity -= quantity;
if (purchasedItem != null && purchasedItem.Quantity <= 0)
{
PurchasedItems.Remove(purchasedItem);
}
OnItemsChanged?.Invoke();
}
public int GetTotalItemCost()
{
if (purchasedItems == null) return 0;
return purchasedItems.Sum(i => i.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * i.Quantity);
}
public void CreateItems()
{
CreateItems(purchasedItems);
OnItemsChanged?.Invoke();
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn)
{
if (itemsToSpawn.Count == 0) { return; }
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub);
if (wp == null)
{
DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!");
return;
}
Hull cargoRoom = Hull.FindHull(wp.WorldPosition);
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
return;
}
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true));
#endif
Dictionary<ItemContainer, int> availableContainers = new Dictionary<ItemContainer, int>();
ItemPrefab containerPrefab = null;
foreach (PurchasedItem pi in itemsToSpawn)
{
Vector2 position = new Vector2(
Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20),
cargoRoom.Rect.Y - cargoRoom.Rect.Height + pi.ItemPrefab.Size.Y / 2);
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier))
{
itemContainer = availableContainers.Keys.ToList().Find(ac =>
ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()));
if (itemContainer == null)
{
containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (containerPrefab == null)
{
DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + containerPrefab.Name + "\"!");
continue;
}
Item containerItem = new Item(containerPrefab, position, wp.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!");
continue;
}
availableContainers.Add(itemContainer, itemContainer.Capacity);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
}
#endif
}
}
for (int i = 0; i < pi.Quantity; i++)
{
if (itemContainer == null)
{
//no container, place at the waypoint
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine);
}
else
{
new Item(pi.ItemPrefab, position, wp.Submarine);
}
continue;
}
//if the intial container has been removed due to it running out of space, add a new container
//of the same type and begin filling it
if (!availableContainers.ContainsKey(itemContainer))
{
Item containerItemOverFlow = new Item(containerPrefab, position, wp.Submarine);
itemContainer = containerItemOverFlow.GetComponent<ItemContainer>();
availableContainers.Add(itemContainer, itemContainer.Capacity);
#if SERVER
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
}
#endif
}
//place in the container
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory);
}
else
{
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
itemContainer.Inventory.TryPutItem(item, null);
}
//reduce the number of available slots in the container
//if there is a container
if (availableContainers.ContainsKey(itemContainer))
{
availableContainers[itemContainer]--;
}
if (availableContainers.ContainsKey(itemContainer) && availableContainers[itemContainer] <= 0)
{
availableContainers.Remove(itemContainer);
}
}
}
itemsToSpawn.Clear();
}
}
}
@@ -0,0 +1,110 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
partial class CrewManager
{
const float ConversationIntervalMin = 100.0f;
const float ConversationIntervalMax = 180.0f;
private float conversationTimer, conversationLineTimer;
private List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
public List<Pair<Order, float>> ActiveOrders { get; } = new List<Pair<Order, float>>();
public bool IsSinglePlayer { get; private set; }
public CrewManager(bool isSinglePlayer)
{
IsSinglePlayer = isSinglePlayer;
conversationTimer = 5.0f;
InitProjectSpecific();
}
partial void InitProjectSpecific();
public bool AddOrder(Order order, float fadeOutTime)
{
if (order.TargetEntity == null)
{
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace);
return false;
}
Pair<Order, float> existingOrder = ActiveOrders.Find(o => o.First.Prefab == order.Prefab && o.First.TargetEntity == order.TargetEntity);
if (existingOrder != null)
{
existingOrder.Second = fadeOutTime;
return false;
}
else
{
ActiveOrders.Add(new Pair<Order, float>(order, fadeOutTime));
return true;
}
}
public void RemoveOrder(Order order)
{
ActiveOrders.RemoveAll(o => o.First == order);
}
public void Update(float deltaTime)
{
foreach (Pair<Order, float> order in ActiveOrders)
{
order.Second -= deltaTime;
}
ActiveOrders.RemoveAll(o => o.Second <= 0.0f);
UpdateConversations(deltaTime);
UpdateProjectSpecific(deltaTime);
}
#region Dialog
public void AddConversation(List<Pair<Character, string>> conversationLines)
{
if (conversationLines == null || conversationLines.Count == 0) { return; }
pendingConversationLines.AddRange(conversationLines);
}
partial void CreateRandomConversation();
private void UpdateConversations(float deltaTime)
{
conversationTimer -= deltaTime;
if (conversationTimer <= 0.0f)
{
CreateRandomConversation();
conversationTimer = Rand.Range(ConversationIntervalMin, ConversationIntervalMax);
}
if (pendingConversationLines.Count > 0)
{
conversationLineTimer -= deltaTime;
if (conversationLineTimer <= 0.0f)
{
//speaker of the next line can't speak, interrupt the conversation
if (pendingConversationLines[0].First.SpeechImpediment >= 100.0f)
{
pendingConversationLines.Clear();
return;
}
pendingConversationLines[0].First.Speak(pendingConversationLines[0].Second, null);
if (pendingConversationLines.Count > 1)
{
conversationLineTimer = MathHelper.Clamp(pendingConversationLines[0].Second.Length * 0.1f, 1.0f, 5.0f);
}
pendingConversationLines.RemoveAt(0);
}
}
}
#endregion
partial void UpdateProjectSpecific(float deltaTime);
}
}
@@ -0,0 +1,253 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
abstract partial class CampaignMode : GameMode
{
public readonly CargoManager CargoManager;
public bool CheatsEnabled;
const int InitialMoney = 8700;
public const int HullRepairCost = 500, ItemRepairCost = 500, ShuttleReplaceCost = 1000;
protected bool watchmenSpawned;
protected Character startWatchman, endWatchman;
//key = dialog flag, double = Timing.TotalTime when the line was last said
private Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs;
public bool InitialSuppliesSpawned;
protected Map map;
public Map Map
{
get { return map; }
}
public override Mission Mission
{
get
{
return Map.CurrentLocation?.SelectedMission;
}
}
private int money;
public int Money
{
get { return money; }
set { money = Math.Max(value, 0); }
}
public CampaignMode(GameModePreset preset, object param)
: base(preset, param)
{
Money = InitialMoney;
CargoManager = new CargoManager(this);
}
public void GenerateMap(string seed)
{
map = new Map(seed);
}
protected List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
{
//leave subs behind if they're not docked to the leaving sub and not at the same exit
return Submarine.Loaded.FindAll(s =>
s != leavingSub &&
!leavingSub.DockedTo.Contains(s) &&
s != Level.Loaded.StartOutpost && s != Level.Loaded.EndOutpost &&
(s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition));
}
public override void Start()
{
base.Start();
dialogLastSpoken.Clear();
watchmenSpawned = false;
startWatchman = null;
endWatchman = null;
if (PurchasedHullRepairs)
{
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine == null || wall.Submarine.IsOutpost) { continue; }
if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine))
{
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -wall.Prefab.Health);
}
}
}
PurchasedHullRepairs = false;
}
if (PurchasedItemRepairs)
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || item.Submarine.IsOutpost) { continue; }
if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine))
{
if (item.GetComponent<Items.Components.Repairable>() != null)
{
item.Condition = item.Prefab.Health;
}
}
}
PurchasedItemRepairs = false;
}
PurchasedLostShuttles = false;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (!IsRunning) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (!watchmenSpawned)
{
if (Level.Loaded.StartOutpost != null) { startWatchman = SpawnWatchman(Level.Loaded.StartOutpost); }
if (Level.Loaded.EndOutpost != null) { endWatchman = SpawnWatchman(Level.Loaded.EndOutpost); }
watchmenSpawned = true;
#if SERVER
(this as MultiPlayerCampaign).LastUpdateID++;
#endif
}
else
{
foreach (Character character in Character.CharacterList)
{
#if SERVER
if (string.IsNullOrEmpty(character.OwnerClientEndPoint)) { continue; }
#else
if (!CrewManager.GetCharacters().Contains(character)) { continue; }
#endif
if (character.Submarine == Level.Loaded.StartOutpost &&
Vector2.DistanceSquared(character.WorldPosition, startWatchman.WorldPosition) < 500.0f * 500.0f)
{
CreateDialog(new List<Character> { startWatchman }, "EnterStartOutpost", 5 * 60.0f);
}
else if (character.Submarine == Level.Loaded.EndOutpost &&
Vector2.DistanceSquared(character.WorldPosition, endWatchman.WorldPosition) < 500.0f * 500.0f)
{
CreateDialog(new List<Character> { endWatchman }, "EnterEndOutpost", 5 * 60.0f);
}
}
}
}
protected void CreateDialog(List<Character> speakers, string conversationTag, float minInterval)
{
if (dialogLastSpoken.TryGetValue(conversationTag, out double lastTime))
{
if (Timing.TotalTime - lastTime < minInterval) { return; }
}
CrewManager.AddConversation(
NPCConversation.CreateRandom(speakers, new List<string>() { conversationTag }));
dialogLastSpoken[conversationTag] = Timing.TotalTime;
}
private Character SpawnWatchman(Submarine outpost)
{
WayPoint watchmanSpawnpoint = WayPoint.WayPointList.Find(wp => wp.Submarine == outpost);
if (watchmanSpawnpoint == null)
{
DebugConsole.ThrowError("Failed to spawn a watchman at the outpost. No spawnpoints found inside the outpost.");
return null;
}
string seed = outpost == Level.Loaded.StartOutpost ? map.SelectedLocation.Name : map.CurrentLocation.Name;
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
JobPrefab watchmanJob = JobPrefab.Get("watchman");
var variant = Rand.Range(0, watchmanJob.Variants, Rand.RandSync.Server);
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: watchmanJob, variant: variant);
var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition,
Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end"));
InitializeWatchman(spawnedCharacter);
var objectiveManager = (spawnedCharacter.AIController as HumanAIController)?.ObjectiveManager;
if (objectiveManager != null)
{
var moveOrder = new AIObjectiveGoTo(watchmanSpawnpoint, spawnedCharacter, objectiveManager, repeat: true, getDivingGearIfNeeded: false);
moveOrder.Completed += () =>
{
// Turn towards the center of the sub. Doesn't work in all possible cases, but this is the simplest solution for now.
spawnedCharacter.AnimController.TargetDir = spawnedCharacter.Submarine.WorldPosition.X > spawnedCharacter.WorldPosition.X ? Direction.Right : Direction.Left;
};
objectiveManager.SetOrder(moveOrder);
}
if (watchmanJob != null)
{
spawnedCharacter.GiveJobItems();
}
return spawnedCharacter;
}
protected void InitializeWatchman(Character character)
{
character.CharacterHealth.UseHealthWindow = false;
character.CharacterHealth.Unkillable = true;
character.CanInventoryBeAccessed = false;
character.CanBeDragged = false;
character.TeamID = Character.TeamType.FriendlyNPC;
character.SetCustomInteract(
WatchmanInteract,
#if CLIENT
hudText: TextManager.GetWithVariable("TalkHint", "[key]", GameMain.Config.KeyBindText(InputType.Select)));
#else
hudText: TextManager.Get("TalkHint"));
#endif
}
protected abstract void WatchmanInteract(Character watchman, Character interactor);
public abstract void Save(XElement element);
public void LogState()
{
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
DebugConsole.NewMessage(" Money: " + Money, Color.White);
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White);
DebugConsole.NewMessage(" Available destinations: ", Color.White);
for (int i = 0; i < map.CurrentLocation.Connections.Count; i++)
{
Location destination = map.CurrentLocation.Connections[i].OtherLocation(map.CurrentLocation);
if (destination == map.SelectedLocation)
{
DebugConsole.NewMessage(" " + i + ". " + destination.Name + " [SELECTED]", Color.White);
}
else
{
DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White);
}
}
if (map.CurrentLocation?.SelectedMission != null)
{
DebugConsole.NewMessage(" Selected mission: " + map.CurrentLocation.SelectedMission.Name, Color.White);
DebugConsole.NewMessage("\n" + map.CurrentLocation.SelectedMission.Description, Color.White);
}
}
public override void Remove()
{
base.Remove();
map?.Remove();
map = null;
}
}
}
@@ -0,0 +1,85 @@
using Barotrauma.Networking;
using System.Xml.Linq;
namespace Barotrauma
{
partial class CharacterCampaignData
{
public CharacterInfo CharacterInfo
{
get;
private set;
}
public readonly string Name;
public string ClientEndPoint
{
get;
private set;
}
public ulong SteamID
{
get;
private set;
}
private XElement itemData;
partial void InitProjSpecific(Client client);
public CharacterCampaignData(Client client)
{
Name = client.Name;
InitProjSpecific(client);
if (client.Character.Inventory != null)
{
itemData = new XElement("inventory");
client.Character.SaveInventory(client.Character.Inventory, itemData);
}
}
public CharacterCampaignData(XElement element)
{
Name = element.GetAttributeString("name", "Unnamed");
ClientEndPoint = element.GetAttributeString("endpoint", null) ?? element.GetAttributeString("ip", "");
string steamID = element.GetAttributeString("steamid", "");
if (!string.IsNullOrEmpty(steamID))
{
ulong.TryParse(steamID, out ulong parsedID);
SteamID = parsedID;
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "character":
case "characterinfo":
CharacterInfo = new CharacterInfo(subElement);
break;
case "inventory":
itemData = subElement;
break;
}
}
}
public XElement Save()
{
XElement element = new XElement("CharacterCampaignData",
new XAttribute("name", Name),
new XAttribute("endpoint", ClientEndPoint),
new XAttribute("steamid", SteamID));
CharacterInfo?.Save(element);
if (itemData != null)
{
element.Add(itemData);
}
return element;
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
namespace Barotrauma
{
partial class GameMode
{
public static List<GameModePreset> PresetList = new List<GameModePreset>();
protected DateTime startTime;
protected bool isRunning;
protected GameModePreset preset;
private string endMessage;
protected CrewManager CrewManager
{
get { return GameMain.GameSession?.CrewManager; }
}
public virtual Mission Mission
{
get { return null; }
}
public bool IsRunning
{
get { return isRunning; }
}
public bool IsSinglePlayer
{
get { return preset.IsSinglePlayer; }
}
public string Name
{
get { return preset.Name; }
}
public string EndMessage
{
get { return endMessage; }
}
public GameModePreset Preset
{
get { return preset; }
}
public GameMode(GameModePreset preset, object param)
{
this.preset = preset;
}
public virtual void Start()
{
startTime = DateTime.Now;
endMessage = "The round has ended!";
isRunning = true;
}
public virtual void ShowStartMessage() { }
public virtual void AddToGUIUpdateList()
{
#if CLIENT
if (!isRunning) return;
GameMain.GameSession?.CrewManager.AddToGUIUpdateList();
#endif
}
public virtual void Update(float deltaTime)
{
CrewManager?.Update(deltaTime);
}
public virtual void End(string endMessage = "")
{
isRunning = false;
if (endMessage != "" || this.endMessage == null) this.endMessage = endMessage;
GameMain.GameSession.EndRound(endMessage);
}
public virtual void Remove() { }
}
}

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