38f1ddb...178a853: v0.8.9.1, removed content folder

This commit is contained in:
Joonas Rikkonen
2019-03-18 19:46:58 +02:00
parent 38f1ddb6fe
commit 6c0679c297
1054 changed files with 151673 additions and 144931 deletions
@@ -2,15 +2,17 @@
namespace Barotrauma
{
partial class AIController : ISteerable
abstract partial class AIController : ISteerable
{
public enum AIState { None, Attack, GoTo, Escape, Eat }
public enum AIState { Idle, Attack, GoTo, Escape, Eat }
public bool Enabled;
public readonly Character Character;
protected AIState state;
private AIState state;
protected AITarget selectedAiTarget;
protected SteeringManager steeringManager;
@@ -24,7 +26,7 @@ namespace Barotrauma
get { return Character.AnimController.TargetMovement; }
set { Character.AnimController.TargetMovement = value; }
}
public Vector2 SimPosition
{
get { return Character.SimPosition; }
@@ -40,10 +42,35 @@ namespace Barotrauma
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 AITarget SelectedAiTarget
{
get { return selectedAiTarget; }
}
public AIState State
{
get { return state; }
set { state = value; }
set
{
if (state == value) return;
OnStateChanged(state, value);
state = value;
}
}
public AIController (Character c)
@@ -53,13 +80,13 @@ namespace Barotrauma
Enabled = true;
}
public virtual void OnAttacked(Character attacker, float amount) { }
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
public virtual void SelectTarget(AITarget target) { }
public virtual void Update(float deltaTime) { }
//protected Structure lastStructurePicked;
protected virtual void OnStateChanged(AIState from, AIState to) { }
}
}
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -20,15 +21,45 @@ namespace Barotrauma
public float SoundRange
{
get { return soundRange; }
set { soundRange = Math.Max(value, 0.0f); }
set { soundRange = Math.Max(value, MinSoundRange); }
}
public float SightRange
{
get { return sightRange; }
set { sightRange = Math.Max(value, 0.0f); }
set { sightRange = Math.Max(value, MinSightRange); }
}
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 string SonarLabel;
public bool Enabled = true;
public float MinSoundRange, MinSightRange;
public Vector2 WorldPosition
{
get
@@ -67,12 +98,27 @@ namespace Barotrauma
}
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = MinSightRange = element.GetAttributeFloat("sightrange", 0.0f);
SoundRange = MinSoundRange = element.GetAttributeFloat("soundrange", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
}
public AITarget(Entity e)
{
Entity = e;
List.Add(this);
}
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);
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,7 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -11,10 +13,19 @@ namespace Barotrauma
private AIObjectiveManager objectiveManager;
private AITarget selectedAiTarget;
private float updateObjectiveTimer;
private bool shouldCrouch;
private float crouchRaycastTimer;
const float CrouchRaycastInterval = 1.0f;
private SteeringManager outsideSteering, insideSteering;
public override AIObjectiveManager ObjectiveManager
{
get { return objectiveManager; }
}
public Order CurrentOrder
{
get;
@@ -29,7 +40,8 @@ namespace Barotrauma
public HumanAIController(Character c) : base(c)
{
steeringManager = new IndoorsSteeringManager(this, true);
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
objectiveManager.AddObjective(new AIObjectiveFindSafety(c));
@@ -44,12 +56,23 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.IsUnconscious) return;
if (Character.Submarine != null || selectedAiTarget?.Entity?.Submarine != null)
{
if (steeringManager != insideSteering) insideSteering.Reset();
steeringManager = insideSteering;
}
else
{
if (steeringManager != outsideSteering) outsideSteering.Reset();
steeringManager = outsideSteering;
}
(Character.AnimController as HumanoidAnimController).Crouching = shouldCrouch;
CheckCrouching(deltaTime);
Character.ClearInputs();
//steeringManager = Character.AnimController.CurrentHull == null ? outdoorsSteeringManager : indoorsSteeringManager;
if (updateObjectiveTimer>0.0f)
if (updateObjectiveTimer > 0.0f)
{
updateObjectiveTimer -= deltaTime;
}
@@ -59,22 +82,23 @@ namespace Barotrauma
updateObjectiveTimer = UpdateObjectiveInterval;
}
if (Character.SpeechImpediment < 100.0f)
{
ReportProblems();
UpdateSpeaking();
}
objectiveManager.DoCurrentObjective(deltaTime);
float currObjectivePriority = objectiveManager.GetCurrentPriority(Character);
float moveSpeed = 1.0f;
float currObjectivePriority = objectiveManager.GetCurrentPriority();
if (currObjectivePriority > 30.0f)
{
moveSpeed *= Character.AnimController.InWater ? Character.AnimController.SwimSpeedMultiplier : Character.AnimController.RunSpeedMultiplier;
}
steeringManager.Update(moveSpeed);
bool run = currObjectivePriority > 30.0f;
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
var currPath = (steeringManager as IndoorsSteeringManager).CurrentPath;
var currPath = (steeringManager as IndoorsSteeringManager)?.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
@@ -84,22 +108,34 @@ namespace Barotrauma
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
(Character.AnimController as HumanoidAnimController).Crouching = false;
if (!Character.AnimController.InWater)
{
Character.AnimController.TargetMovement = new Vector2(
Vector2 targetMovement = new Vector2(
Character.AnimController.TargetMovement.X,
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)) * Character.SpeedMultiplier;
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
Character.SpeedMultiplier = 1.0f;
float maxSpeed = Character.GetCurrentMaxSpeed(run);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
if (run || Character.SpeedMultiplier <= 0.0f) targetMovement *= Character.SpeedMultiplier;
Character.SpeedMultiplier = 1.0f; // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
}
if (Character.SelectedConstruction != null && Character.SelectedConstruction.GetComponent<Items.Components.Ladder>()!=null)
if (Character.AnimController.Anim == AnimController.Animation.Climbing &&
Character.SelectedConstruction != null &&
Character.SelectedConstruction.GetComponent<Items.Components.Ladder>() != null)
{
if (currPath != null && currPath.CurrentNode != null && currPath.CurrentNode.Ladders != null)
{
Character.AnimController.TargetMovement = new Vector2( 0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
}
@@ -112,7 +148,7 @@ namespace Barotrauma
//-> take the suit off
if (canTakeOffSuit && (Character.Oxygen < 50.0f || objectiveManager.CurrentObjective is AIObjectiveIdle))
{
var divingSuit = Character.Inventory.FindItem("Diving Suit");
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null) divingSuit.Drop(Character);
}
@@ -136,27 +172,104 @@ namespace Barotrauma
Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
}
}
public override void OnAttacked(Character attacker, float amount)
private void ReportProblems()
{
if (amount <= 0.0f) return;
if (GameMain.Client != null) return;
var enemy = attacker as Character;
if (enemy == null || enemy == Character) return;
Order newOrder = null;
if (Character.CurrentHull != null)
{
if (Character.CurrentHull.FireSources.Count > 0)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
objectiveManager.AddObjective(new AIObjectiveCombat(Character, enemy));
if (Character.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor == null && g.Open > 0.0f))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
//the objective in the manager is not necessarily the same as the one we just instantiated,
//because the objective isn't added if there's already an identical objective in the manager
var combatObjective = objectiveManager.GetObjective<AIObjectiveCombat>();
combatObjective.MaxEnemyDamage = Math.Max(amount, combatObjective.MaxEnemyDamage);
foreach (Character c in Character.CharacterList)
{
if (c.CurrentHull == Character.CurrentHull && !c.IsDead &&
(c.AIController is EnemyAIController || c.TeamID != Character.TeamID))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
}
}
if (Character.CurrentHull != null && (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
if (newOrder != null)
{
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(
newOrder.GetChatMessage("", Character.CurrentHull?.RoomName), ChatMessageType.Order);
if (GameMain.Server != null)
{
OrderChatMessage msg = new OrderChatMessage(newOrder, "", Character.CurrentHull, null, Character);
GameMain.Server.SendOrderChatMessage(msg);
}
}
}
}
public void SetOrder(Order order, string option)
private void UpdateSpeaking()
{
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen"), null, 0, "lowoxygen", 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding"), null, 0, "bleeding", 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
{
Character.Speak(TextManager.Get("DialogPressure").Replace("[roomname]", Character.CurrentHull.RoomName), null, 0, "pressure", 30.0f);
}
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
float totalDamage = attackResult.Damage;
if (totalDamage <= 0.0f || attacker == null) return;
if (attacker.AnimController.Anim == AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
{
// Don't attack characters that damage you while doing cpr, because let's assume that they are helping you.
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () =>
{
//the objective in the manager is not necessarily the same as the one we just instantiated,
//because the objective isn't added if there's already an identical objective in the manager
var combatObjective = objectiveManager.GetObjective<AIObjectiveCombat>();
combatObjective.MaxEnemyDamage = Math.Max(totalDamage, combatObjective.MaxEnemyDamage);
});
}
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
{
CurrentOrderOption = option;
CurrentOrder = order;
objectiveManager.SetOrder(order, option);
objectiveManager.SetOrder(order, option, orderGiver);
if (speak && Character.SpeechImpediment < 100.0f) Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
SetOrderProjSpecific(order);
}
@@ -166,5 +279,21 @@ namespace Barotrauma
{
selectedAiTarget = target;
}
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
if (crouchRaycastTimer > 0.0f) return;
crouchRaycastTimer = CrouchRaycastInterval;
//start the raycast in front of the character in the direction it's heading to
Vector2 startPos = Character.SimPosition;
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
//do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
}
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
private PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors;
private bool canOpenDoors, canBreakDoors;
private Character character;
@@ -33,20 +33,27 @@ namespace Barotrauma
get { return currentTarget; }
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors)
public bool IsPathDirty
{
get;
private set;
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors)
: base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), 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 = 1)
public override void Update(float speed)
{
base.Update(speed);
@@ -58,16 +65,18 @@ namespace Barotrauma
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
findPathTimer = 1.0f;
IsPathDirty = false;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float speed = 1)
protected override Vector2 DoSteeringSeek(Vector2 target, float speed)
{
//find a new path if one hasn't been found yet or the target is different from the current target
if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f)
{
if (findPathTimer > 0.0f) return Vector2.Zero;
IsPathDirty = true;
if (findPathTimer > 0.0f) return Vector2.Zero;
currentTarget = target;
Vector2 pos = host.SimPosition;
if (character != null && character.Submarine == null)
@@ -75,15 +84,16 @@ namespace Barotrauma
var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
pos -= targetHull.SimPosition;
pos -= targetHull.Submarine.SimPosition;
}
}
currentPath = pathFinder.FindPath(pos, target);
currentPath = pathFinder.FindPath(pos, target, "(Character: " + character.Name + ")");
findPathTimer = Rand.Range(1.0f, 1.2f);
return DiffToCurrentNode();
IsPathDirty = false;
return DiffToCurrentNode();
}
Vector2 diff = DiffToCurrentNode();
@@ -116,8 +126,11 @@ namespace Barotrauma
}
return currentTarget - pos2;
}
if (canOpenDoors && !character.LockHands) CheckDoorsInPath();
if (canOpenDoors && !character.LockHands)
{
CheckDoorsInPath();
}
Vector2 pos = host.SimPosition;
@@ -134,10 +147,12 @@ namespace Barotrauma
pos -= FarseerPhysics.ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position-character.Submarine.Position);
}
}
}
if (currentPath.CurrentNode != null && currentPath.CurrentNode.Ladders != null)
//only humanoids can climb ladders
if (currentPath.CurrentNode != null &&
currentPath.CurrentNode.Ladders != null &&
character.AnimController is HumanoidAnimController)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
@@ -158,9 +173,6 @@ namespace Barotrauma
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
if (heightFromFloor <= 0.0f)
{
@@ -179,10 +191,29 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
}
else
{
//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 (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders == currentPath.NextNode?.Ladders &&
Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
currentPath.SkipToNextNode();
}
}
character.AnimController.IgnorePlatforms = false;
return diff;
}
else if (character.AnimController.InWater)
{
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < collider.radius * collider.radius)
{
currentPath.SkipToNextNode();
}
}
else
{
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
@@ -203,42 +234,54 @@ namespace Barotrauma
{
for (int i = 0; i < 2; i++)
{
WayPoint node = null;
WayPoint nextNode = null;
if (i==0)
{
node = currentPath.CurrentNode;
nextNode = currentPath.NextNode;
}
else
{
node = currentPath.PrevNode;
nextNode = currentPath.CurrentNode;
}
if (node == null || node.ConnectedGap == null || node.ConnectedGap.ConnectedDoor == null) continue;
if (nextNode == null) continue;
var door = node.ConnectedGap.ConnectedDoor;
Door door = null;
bool shouldBeOpen = false;
if (door.LinkedGap.IsHorizontal)
if (currentPath.Nodes.Count == 1)
{
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
door = currentPath.Nodes.First().ConnectedDoor;
shouldBeOpen = door != null;
}
else
{
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y);
WayPoint node = null;
WayPoint nextNode = null;
if (i == 0)
{
node = currentPath.CurrentNode;
nextNode = currentPath.NextNode;
}
else
{
node = currentPath.PrevNode;
nextNode = currentPath.CurrentNode;
}
if (node?.ConnectedDoor == null) continue;
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f;
if (nextNode == 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 = node.ConnectedGap.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
}
else
{
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -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)
{
@@ -267,7 +310,7 @@ namespace Barotrauma
return;
}
closestButton.Item.TryInteract(character, false, true, true);
closestButton.Item.TryInteract(character, false, true, false);
break;
}
}
@@ -281,23 +324,37 @@ namespace Barotrauma
float penalty = 0.0f;
if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f)
{
if (nextNode.Waypoint.ConnectedGap.ConnectedDoor == null)
if (nextNode.Waypoint.ConnectedDoor == null)
{
penalty = 100.0f;
}
//door closed and the character can't open doors -> node can't be traversed
if (!canOpenDoors || character.LockHands) return null;
var doorButtons = nextNode.Waypoint.ConnectedGap.ConnectedDoor.Item.GetConnectedComponents<Controller>();
if (!doorButtons.Any()) return null;
foreach (Controller button in doorButtons)
if (!canBreakDoors)
{
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
Math.Sign(node.Position.X - nextNode.Position.X)) continue;
//door closed and the character can't open doors -> node can't be traversed
if (!canOpenDoors || character.LockHands) return null;
if (!button.HasRequiredItems(character, false)) return null;
var doorButtons = nextNode.Waypoint.ConnectedDoor.Item.GetConnectedComponents<Controller>();
if (!doorButtons.Any()) return null;
foreach (Controller button in doorButtons)
{
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
Math.Sign(node.Position.X - nextNode.Position.X)) continue;
if (!button.HasRequiredItems(character, false)) 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;
}
}
@@ -0,0 +1,296 @@
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 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 AIController.AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
{
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)
{
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;
wallAttachPos = ConvertUnits.ToSimUnits(intersection);
break;
}
}
if (WallAttachPos != Vector2.Zero) 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, character.AnimController.GetCurrentSpeed(false) * 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos, character.AnimController.GetCurrentSpeed(true));
}
}
break;
case AIController.AIState.Attack:
if (enemyAI.AttackingLimb != null)
{
if (attachToSub && 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.Range * enemyAI.AttackingLimb.attack.Range)
{
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos);
}
}
}
}
break;
default:
WallAttachPos = null;
DeattachFromBody();
break;
}
if (attachTargetBody != null && deattachTimer < 0.0f)
{
Entity entity = attachTargetBody.UserData as Entity;
Submarine attachedSub = entity is Submarine ? (Submarine)entity : 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.AddJoint(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.AddJoint(colliderJoint);
attachJoints.Add(colliderJoint);
}
public void DeattachFromBody()
{
foreach (Joint joint in attachJoints)
{
GameMain.World.RemoveJoint(joint);
}
attachJoints.Clear();
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
{
DeattachFromBody();
character.OnDeath -= OnCharacterDeath;
}
}
}
@@ -0,0 +1,315 @@
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 static List<NPCConversation> list = new List<NPCConversation>();
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<string> filePaths)
{
//language, identifier, filepath
List<Tuple<string, string, string>> contentPackageFiles = new List<Tuple<string, string, string>>();
foreach (string filePath in filePaths)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
contentPackageFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
}
List<Tuple<string, string, string>> translationFiles = new List<Tuple<string, string, string>>();
foreach (string filePath in Directory.GetFiles(Path.Combine("Content", "NPCConversations")))
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
translationFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
}
//get the languages and identifiers of the files
for (int i = 0; i < contentPackageFiles.Count; i++)
{
var contentPackageFile = contentPackageFiles[i];
//correct language, all good
if (contentPackageFile.Item1 == TextManager.Language) continue;
//attempt to find a translation file with the correct language and a matching identifier
//if it fails, we'll just use the original file with the incorrect language
var translation = translationFiles.Find(t => t.Item1 == TextManager.Language && t.Item2 == contentPackageFile.Item2);
if (translation != null) contentPackageFiles[i] = translation; //replace with the translation file
}
foreach (var file in contentPackageFiles)
{
Load(file.Item3);
}
}
private static void Load(string file)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null || doc.Root == null) return;
string language = doc.Root.GetAttributeString("Language", "English");
if (language != TextManager.Language) return;
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conversation":
list.Add(new NPCConversation(subElement));
break;
case "personalitytrait":
new NPCPersonalityTrait(subElement);
break;
}
}
}
public NPCConversation(XElement element)
{
Line = element.GetAttributeString("line", "");
speakerIndex = element.GetAttributeInt("speaker", 0);
AllowedJobs = new List<JobPrefab>();
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
var jobPrefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == allowedJobIdentifier.ToLowerInvariant());
if (jobPrefab != null) AllowedJobs.Add(jobPrefab);
}
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));
}
}
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: list);
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 = list.FindAll(conversation => requiredFlags.All(f => conversation.Flags.Contains(f)));
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 (selectedConversation.AllowedJobs.Count > 0 && !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);
}
}
}
@@ -38,7 +38,7 @@ namespace Barotrauma
/// </summary>
public void TryComplete(float deltaTime)
{
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted);
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted || ShouldInterruptSubObjective(s));
foreach (AIObjective objective in subObjectives)
{
@@ -66,6 +66,18 @@ namespace Barotrauma
return currentSubObjective;
}
public void SortSubObjectives(AIObjectiveManager objectiveManager)
{
if (!subObjectives.Any()) return;
subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager)));
subObjectives[0].SortSubObjectives(objectiveManager);
}
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective)
{
return false;
}
protected abstract void Act(float deltaTime);
public abstract bool IsCompleted();
@@ -0,0 +1,66 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjective
{
private List<PowerContainer> availableBatteries;
private string orderOption;
public AIObjectiveChargeBatteries(Character character, string option)
: base(character, option)
{
orderOption = option;
availableBatteries = new List<PowerContainer>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null) continue;
if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) continue;
var powerContainer = item.GetComponent<PowerContainer>();
availableBatteries.Add(powerContainer);
}
if (availableBatteries.Count == 0)
{
character?.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveChargeBatteries other && other.orderOption == orderOption;
}
protected override void Act(float deltaTime)
{
if (availableBatteries.Count == 0)
{
AddSubObjective(new AIObjectiveIdle(character));
return;
}
foreach (PowerContainer battery in availableBatteries)
{
AddSubObjective(new AIObjectiveOperateItem(battery, character, orderOption, false));
}
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -17,21 +18,13 @@ namespace Barotrauma
private AIObjectiveFindSafety escapeObjective;
float coolDownTimer;
private AIObjectiveContainItem reloadWeaponObjective;
private readonly float enemyStrength;
private float coolDownTimer;
public AIObjectiveCombat(Character character, Character enemy)
: base(character, "")
public AIObjectiveCombat(Character character, Character enemy) : base(character, "")
{
this.enemy = enemy;
foreach (Limb limb in enemy.AnimController.Limbs)
{
if (limb.attack == null) continue;
enemyStrength += limb.attack.GetDamage(1.0f);
}
coolDownTimer = CoolDown;
}
@@ -39,26 +32,66 @@ namespace Barotrauma
{
coolDownTimer -= deltaTime;
var weapon = character.Inventory.FindItem("weapon");
var weapon = character.Inventory.FindItemByTag("weapon");
if (weapon == null)
{
Escape(deltaTime);
}
else
{
//TODO: make sure the weapon is ready to use (projectiles/batteries loaded)
if (!character.SelectedItems.Contains(weapon))
{
if (character.Inventory.TryPutItem(weapon, 3, false, false, character))
if (character.Inventory.TryPutItem(weapon, 3, true, false, character))
{
weapon.Equip(character);
}
else
{
//couldn't equip the item, escape
Escape(deltaTime);
return;
}
}
//make sure the weapon is loaded
var weaponComponent =
weapon.GetComponent<RangedWeapon>() as ItemComponent ??
weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
weapon.GetComponent<RepairTool>() as ItemComponent;
if (weaponComponent != null && weaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
Item[] containedItems = weapon.ContainedItems;
foreach (RelatedItem requiredItem in weaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
Item containedItem = Array.Find(containedItems, it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (containedItem == null)
{
var newReloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, weapon.GetComponent<ItemContainer>());
if (!newReloadWeaponObjective.IsDuplicate(reloadWeaponObjective))
{
reloadWeaponObjective = newReloadWeaponObjective;
}
}
}
}
if (reloadWeaponObjective != null)
{
if (reloadWeaponObjective.IsCompleted())
{
reloadWeaponObjective = null;
}
else if (!reloadWeaponObjective.CanBeCompleted)
{
Escape(deltaTime);
}
else
{
reloadWeaponObjective.TryComplete(deltaTime);
}
return;
}
character.CursorPosition = enemy.Position;
character.SetInput(InputType.Aim, false, true);
@@ -116,7 +149,7 @@ namespace Barotrauma
//clamp the strength to the health of this character
//(it doesn't make a difference whether the enemy does 200 or 600 damage, it's one hit kill anyway)
float enemyDanger = Math.Min(Math.Max(enemyStrength, MaxEnemyDamage), character.Health) + enemy.Health / 10.0f;
float enemyDanger = Math.Min(Math.Max(CalculateEnemyStrength(), MaxEnemyDamage), character.Health) + enemy.Health / 10.0f;
EnemyAIController enemyAI = enemy.AIController as EnemyAIController;
if (enemyAI != null)
@@ -134,5 +167,19 @@ namespace Barotrauma
return objective.enemy == enemy;
}
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;
}
}
}
@@ -9,7 +9,8 @@ namespace Barotrauma
{
public int MinContainedAmount = 1;
private string[] itemNames;
//can either be a tag or an identifier
private string[] itemIdentifiers;
private ItemContainer container;
@@ -21,16 +22,21 @@ namespace Barotrauma
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
public AIObjectiveContainItem(Character character, string itemName, ItemContainer container)
: this(character, new string[] { itemName }, container)
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container)
: this(character, new string[] { itemIdentifier }, container)
{
}
public AIObjectiveContainItem(Character character, string[] itemNames, ItemContainer container)
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container)
: base (character, "")
{
this.itemNames = itemNames;
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
}
@@ -41,7 +47,7 @@ namespace Barotrauma
int containedItemCount = 0;
foreach (Item item in container.Inventory.Items)
{
if (item != null && itemNames.Any(name => item.Prefab.NameMatches(name) || item.HasTag(name))) containedItemCount++;
if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) containedItemCount++;
}
return containedItemCount >= MinContainedAmount;
@@ -56,7 +62,7 @@ namespace Barotrauma
return goToObjective.CanBeCompleted;
}
return getItemObjective == null || !getItemObjective.CanBeCompleted;
return getItemObjective == null || getItemObjective.CanBeCompleted;
}
}
@@ -75,12 +81,20 @@ namespace Barotrauma
if (isCompleted) return;
//get the item that should be contained
var itemToContain = character.Inventory.FindItem(itemNames);
Item itemToContain = null;
foreach (string identifier in itemIdentifiers)
{
itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
if (itemToContain != null) break;
}
if (itemToContain == null)
{
getItemObjective = new AIObjectiveGetItem(character, itemNames);
getItemObjective.GetItemPriority = GetItemPriority;
getItemObjective.IgnoreContainedItems = IgnoreAlreadyContainedItems;
getItemObjective = new AIObjectiveGetItem(character, itemIdentifiers)
{
GetItemPriority = GetItemPriority,
IgnoreContainedItems = IgnoreAlreadyContainedItems
};
AddSubObjective(getItemObjective);
return;
}
@@ -116,11 +130,11 @@ namespace Barotrauma
AIObjectiveContainItem objective = otherObjective as AIObjectiveContainItem;
if (objective == null) return false;
if (objective.container != container) return false;
if (objective.itemNames.Length != itemNames.Length) return false;
if (objective.itemIdentifiers.Length != itemIdentifiers.Length) return false;
for (int i = 0; i < itemNames.Length; i++)
for (int i = 0; i < itemIdentifiers.Length; i++)
{
if (objective.itemNames[i] != itemNames[i]) return false;
if (objective.itemIdentifiers[i] != itemIdentifiers[i]) return false;
}
return true;
@@ -0,0 +1,106 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
private Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull) :
base(character, "")
{
this.targetHull = targetHull;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
return targetHull.FireSources.Sum(fs => fs.Size.X * 0.1f);
}
public override bool IsCompleted()
{
return targetHull.FireSources.Count == 0;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
var otherExtinguishFire = otherObjective as AIObjectiveExtinguishFire;
return otherExtinguishFire != null && otherExtinguishFire.targetHull == targetHull;
}
public override bool CanBeCompleted
{
get { return getExtinguisherObjective == null || getExtinguisherObjective.CanBeCompleted; }
}
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
if (getExtinguisherObjective == null)
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
getExtinguisherObjective = new AIObjectiveGetItem(character, "extinguisher", true);
}
else
{
getExtinguisherObjective.TryComplete(deltaTime);
}
return;
}
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
if (extinguisher == null)
{
DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
return;
}
foreach (FireSource fs in targetHull.FireSources.ToList())
{
if (fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range)) || useExtinquisherTimer > 0.0f)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f) useExtinquisherTimer = 0.0f;
character.CursorPosition = fs.Position;
character.SetInput(InputType.Aim, false, true);
character.AIController.SteeringManager.Reset();
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.Get("DialogPutOutFire").Replace("[roomname]", targetHull.Name), null, 0, "putoutfire", 10.0f);
}
return;
}
}
foreach (FireSource fs in targetHull.FireSources)
{
//go to the first firesource
if (gotoObjective == null || !gotoObjective.CanBeCompleted || gotoObjective.IsCompleted())
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character);
}
else
{
gotoObjective.TryComplete(deltaTime);
}
break;
}
}
}
}
@@ -0,0 +1,47 @@
using System.Linq;
namespace Barotrauma
{
class AIObjectiveExtinguishFires : AIObjective
{
public AIObjectiveExtinguishFires(Character character) :
base(character, "")
{
if (!Hull.hullList.Any(h => h.FireSources.Count > 0))
{
character?.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (objectiveManager.CurrentObjective == this)
{
return AIObjectiveManager.OrderPriority;
}
return Hull.hullList.Count(h => h.FireSources.Count > 0) * 10;
}
public override bool IsCompleted()
{
return !Hull.hullList.Any(h => h.FireSources.Count > 0);
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveExtinguishFires;
}
protected override void Act(float deltaTime)
{
foreach (Hull hull in Hull.hullList)
{
if (hull.FireSources.Count > 0)
{
AddSubObjective(new AIObjectiveExtinguishFire(character, hull));
}
}
}
}
}
@@ -7,19 +7,19 @@ namespace Barotrauma
{
private AIObjective subObjective;
private string gearName;
private string gearTag;
public override bool IsCompleted()
{
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
if (CharacterInventory.limbSlots[i] == InvSlotType.Any || character.Inventory.Items[i] == null) continue;
if (character.Inventory.Items[i].Prefab.NameMatches(gearName) || character.Inventory.Items[i].HasTag(gearName))
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || character.Inventory.Items[i] == null) continue;
if (character.Inventory.Items[i].HasTag(gearTag))
{
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems == null) continue;
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.NameMatches("Oxygen Tank") || it.HasTag("oxygensource")) && it.Condition > 0.0f);
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
if (oxygenTank != null) return true;
}
}
@@ -30,18 +30,19 @@ namespace Barotrauma
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit)
: base(character, "")
{
gearName = needDivingSuit ? "Diving Suit" : "diving";
gearTag = needDivingSuit ? "divingsuit" : "diving";
}
protected override void Act(float deltaTime)
{
var item = character.Inventory.FindItem(gearName);
if (item == null)
var item = character.Inventory.FindItemByTag(gearTag);
if (item == null || !character.HasEquippedItem(item))
{
//get a diving mask/suit first
if (!(subObjective is AIObjectiveGetItem))
{
subObjective = new AIObjectiveGetItem(character, gearName, true);
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
subObjective = new AIObjectiveGetItem(character, gearTag, true);
}
}
else
@@ -57,7 +58,7 @@ namespace Barotrauma
{
containedItem.Drop();
}
else if (containedItem.Prefab.NameMatches("Oxygen Tank") || containedItem.HasTag("oxygensource"))
else if (containedItem.Prefab.Identifier == "oxygentank" || containedItem.HasTag("oxygensource"))
{
//we've got an oxygen source inside the mask/suit, all good
return;
@@ -66,7 +67,8 @@ namespace Barotrauma
if (!(subObjective is AIObjectiveContainItem) || subObjective.IsCompleted())
{
subObjective = new AIObjectiveContainItem(character, new string[] { "Oxygen Tank", "oxygensource" }, item.GetComponent<ItemContainer>());
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
subObjective = new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>());
}
}
@@ -55,7 +55,10 @@ namespace Barotrauma
var bestHull = FindBestHull();
if (bestHull != null)
{
goToObjective = new AIObjectiveGoTo(bestHull, character);
goToObjective = new AIObjectiveGoTo(bestHull, character)
{
AllowGoingOutside = true
};
}
searchHullTimer = SearchHullInterval;
@@ -63,15 +66,57 @@ namespace Barotrauma
if (goToObjective != null)
{
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering != null && pathSteering.CurrentPath != null &&
goToObjective.TryComplete(deltaTime);
if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
pathSteering.CurrentPath.Unreachable && !unreachable.Contains(goToObjective.Target))
{
unreachable.Add(goToObjective.Target as Hull);
goToObjective = 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
else if (currentHull != null)
{
Vector2 escapeVel = Vector2.Zero;
foreach (FireSource fireSource in currentHull.FireSources)
{
int dir = Math.Sign(character.Position.X - fireSource.Position.X);
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(dir * distMultiplier, 0.0f);
}
foreach (Character enemy in Character.CharacterList)
{
if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(character.Position.X - enemy.Position.X) * distMultiplier, 0.0f);
}
}
goToObjective.TryComplete(deltaTime);
if (escapeVel != Vector2.Zero)
{
//only move if we haven't reached the edge of the room
if ((escapeVel.X < 0 && character.Position.X > currentHull.Rect.X + 50) ||
(escapeVel.X > 0 && character.Position.X < currentHull.Rect.Right - 50))
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
}
else
{
character.AIController.SteeringManager.Reset();
}
}
}
@@ -137,26 +182,30 @@ namespace Barotrauma
return 150.0f - character.Oxygen;
}
if (character.AnimController.CurrentHull == null) return 5.0f;
currenthullSafety = GetHullSafety(character.AnimController.CurrentHull, character);
if (character.CurrentHull == null) return 5.0f;
currenthullSafety = GetHullSafety(character.CurrentHull, character);
priority = 100.0f - currenthullSafety;
var nearbyHulls = character.AnimController.CurrentHull.GetConnectedHulls(3);
//var nearbyHulls = character.CurrentHull.GetConnectedHulls(3);
foreach (Hull hull in nearbyHulls)
//increase priority slightly if there's a fire in the room
//(will increase more heavily if near the damage range of the fire)
if (character.CurrentHull.FireSources.Count > 0)
{
priority += 5.0f;
}
/*foreach (Hull hull in nearbyHulls)
{
foreach (FireSource fireSource in hull.FireSources)
{
//increase priority if almost within damage range of a fire
if (character.Position.X > fireSource.Position.X - fireSource.DamageRange * 2 &&
character.Position.X < fireSource.Position.X + fireSource.Size.X + fireSource.DamageRange * 2 &&
character.Position.Y > hull.Rect.Y - hull.Rect.Height &&
character.Position.Y < hull.Rect.Y)
//heavily increase priority if almost within damage range of a fire
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * 1.25f))
{
priority += Math.Max(fireSource.Size.X, 50.0f);
}
}
}
}*/
if (NeedsDivingGear())
{
@@ -189,25 +238,36 @@ namespace Barotrauma
if (hull.OxygenPercentage < 30.0f) safety -= (30.0f - hull.OxygenPercentage) * 5.0f;
if (safety <= 0.0f) return 0.0f;
bool extinguishFires =
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFires ||
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFire;
float fireAmount = 0.0f;
var nearbyHulls = hull.GetConnectedHulls(3);
foreach (Hull hull2 in nearbyHulls)
{
foreach (FireSource fireSource in hull2.FireSources)
{
//increase priority if almost within damage range of a fire
if (character.Position.X > fireSource.Position.X - fireSource.DamageRange * 2 &&
character.Position.X < fireSource.Position.X + fireSource.Size.X + fireSource.DamageRange * 2 &&
character.Position.Y > hull2.Rect.Y - hull2.Rect.Height &&
character.Position.Y < hull2.Rect.Y)
//increase priority if near the damage range of a fire
//if extinguishing fires, the character can go closer the damage range
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * (extinguishFires ? 1.25f : 5.0f)))
{
fireAmount += Math.Max(fireSource.Size.X, 50.0f);
fireAmount += Math.Max(fireSource.Size.X, AIObjectiveManager.OrderPriority + 1.0f);
}
}
}
safety -= fireAmount;
foreach (Character enemy in Character.CharacterList)
{
if (enemy.CurrentHull == hull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{
safety -= 10.0f;
}
}
return MathHelper.Clamp(safety, 0.0f, 100.0f);
}
}
@@ -8,9 +8,7 @@ namespace Barotrauma
class AIObjectiveFixLeak : AIObjective
{
private readonly Gap leak;
private AIObjectiveGoTo gotoObjective;
public Gap Leak
{
get { return leak; }
@@ -47,11 +45,11 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItem("Welding Tool");
var weldingTool = character.Inventory.FindItemByTag("weldingtool");
if (weldingTool == null)
{
AddSubObjective(new AIObjectiveGetItem(character, "Welding Tool", true));
AddSubObjective(new AIObjectiveGetItem(character, "weldingtool", true));
return;
}
else
@@ -59,11 +57,10 @@ namespace Barotrauma
var containedItems = weldingTool.ContainedItems;
if (containedItems == null) return;
var fuelTank = Array.Find(containedItems, i => i.Prefab.NameMatches("Welding Fuel Tank") && i.Condition > 0.0f);
var fuelTank = Array.Find(containedItems, i => i.HasTag("weldingfueltank") && i.Condition > 0.0f);
if (fuelTank == null)
{
AddSubObjective(new AIObjectiveContainItem(character, "Welding Fuel Tank", weldingTool.GetComponent<ItemContainer>()));
AddSubObjective(new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>()));
return;
}
}
@@ -73,7 +70,15 @@ namespace Barotrauma
Vector2 standPosition = GetStandPosition();
if (Vector2.DistanceSquared(character.WorldPosition, leak.WorldPosition) > 100.0f * 100.0f)
Vector2 gapDiff = leak.WorldPosition - character.WorldPosition;
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(gapDiff.X) < 100.0f && gapDiff.Y < 0.0f && gapDiff.Y > -150.0f)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
}
if (Math.Abs(gapDiff.X) > 100.0f || Math.Abs(gapDiff.Y) > 150.0f)
{
var gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(standPosition), character);
if (!gotoObjective.IsCompleted())
@@ -105,9 +105,20 @@ namespace Barotrauma
{
if (gap.ConnectedWall == null) continue;
if (gap.ConnectedDoor != null || gap.Open <= 0.0f) continue;
if (character.TeamID == 0)
{
if (gap.Submarine == null) continue;
}
else
{
//prevent characters from attempting to fix leaks in the enemy sub
//team 1 plays in sub 0, team 2 in sub 1
Submarine mySub = character.TeamID < 1 || character.TeamID > Submarine.MainSubs.Length ?
Submarine.MainSub : Submarine.MainSubs[character.TeamID - 1];
//TODO: prevent the AI characters from fixing leaks in the enemy sub in sub-vs-sub missions if/when multiplayer bots are implemented
if (gap.Submarine == null) continue;
if (gap.Submarine != mySub) continue;
}
float gapPriority = GetGapFixPriority(gap);
@@ -10,7 +10,8 @@ namespace Barotrauma
{
public Func<Item, float> GetItemPriority;
private string[] itemNames;
//can be either tags or identifiers
private string[] itemIdentifiers;
private Item targetItem, moveToTarget;
@@ -45,29 +46,65 @@ namespace Barotrauma
: base(character, "")
{
canBeCompleted = true;
currSearchIndex = -1;
this.equip = equip;
currSearchIndex = 0;
this.targetItem = targetItem;
}
public AIObjectiveGetItem(Character character, string itemName, bool equip = false)
: this(character, new string[] { itemName }, equip)
public AIObjectiveGetItem(Character character, string itemIdentifier, bool equip = false)
: this(character, new string[] { itemIdentifier }, equip)
{
}
public AIObjectiveGetItem(Character character, string[] itemNames, bool equip = false)
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false)
: base(character, "")
{
canBeCompleted = true;
currSearchIndex = -1;
this.equip = equip;
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
currSearchIndex = 0;
CheckInventory();
}
this.itemNames = itemNames;
private void CheckInventory()
{
if (itemIdentifiers == null)
{
return;
}
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) continue;
if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id)))
{
targetItem = character.Inventory.Items[i];
moveToTarget = targetItem;
currItemPriority = 100.0f;
break;
}
//check items inside items (tool inside a toolbox etc)
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null || containedItem.Condition <= 0.0f) continue;
if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id)))
{
targetItem = containedItem;
moveToTarget = character.Inventory.Items[i];
currItemPriority = 100.0f;
break;
}
}
}
}
}
protected override void Act(float deltaTime)
@@ -99,7 +136,7 @@ namespace Barotrauma
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(CharacterInventory.limbSlots[i])) continue;
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) continue;
targetSlot = i;
@@ -117,7 +154,7 @@ namespace Barotrauma
targetItem.TryInteract(character, false, true);
if (targetSlot > -1 && character.Inventory.IsInLimbSlot(targetItem, InvSlotType.Any))
if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
{
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
}
@@ -127,14 +164,15 @@ namespace Barotrauma
if (goToObjective == null || moveToTarget != goToObjective.Target)
{
//check if we're already looking for a diving gear
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.NameMatches("Diving Gear") || targetItem.HasTag("diving")) ||
(itemNames != null && (itemNames.Contains("diving") || itemNames.Contains("Diving Gear")));
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) ||
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit")));
//don't attempt to get diving gear to reach the destination if the item we're trying to get is diving gear
goToObjective = new AIObjectiveGoTo(moveToTarget, character, false, !gettingDivingGear);
}
goToObjective.TryComplete(deltaTime);
if (!goToObjective.CanBeCompleted) targetItem = null;
}
}
@@ -144,7 +182,7 @@ namespace Barotrauma
/// </summary>
private void FindTargetItem()
{
if (itemNames == null)
if (itemIdentifiers == null)
{
if (targetItem == null) canBeCompleted = false;
return;
@@ -152,7 +190,7 @@ namespace Barotrauma
float currDist = moveToTarget == null ? 0.0f : Vector2.DistanceSquared(moveToTarget.Position, character.Position);
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 2; i++)
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
@@ -160,21 +198,19 @@ namespace Barotrauma
if (item.CurrentHull == null || item.Condition <= 0.0f) continue;
if (IgnoreContainedItems && item.Container != null) continue;
if (!itemNames.Any(name => item.Prefab.NameMatches(name) || item.HasTag(name))) continue;
if (!itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) continue;
//if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory)
{
Character owner = item.ParentInventory.Owner as Character;
if (owner != null && !owner.IsDead) continue;
if (item.ParentInventory.Owner is Character owner && !owner.IsDead) continue;
}
//if the item is inside an item, which is inside a character's inventory, don't steal it
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
Character owner = rootContainer.ParentInventory.Owner as Character;
if (owner != null && !owner.IsDead) continue;
if (rootContainer.ParentInventory.Owner is Character owner && !owner.IsDead) continue;
}
float itemPriority = 0.0f;
@@ -194,10 +230,11 @@ namespace Barotrauma
targetItem = item;
moveToTarget = rootContainer ?? item;
}
//if searched through all the items and a target wasn't found, can't be completed
if (currSearchIndex >= Item.ItemList.Count && targetItem == null) canBeCompleted = false;
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null) canBeCompleted = false;
}
public override bool IsDuplicate(AIObjective otherObjective)
@@ -205,16 +242,16 @@ namespace Barotrauma
AIObjectiveGetItem getItem = otherObjective as AIObjectiveGetItem;
if (getItem == null) return false;
if (getItem.equip != equip) return false;
if (getItem.itemNames != null && itemNames != null)
if (getItem.itemIdentifiers != null && itemIdentifiers != null)
{
if (getItem.itemNames.Length != itemNames.Length) return false;
for (int i = 0; i < getItem.itemNames.Length; i++)
if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) return false;
for (int i = 0; i < getItem.itemIdentifiers.Length; i++)
{
if (getItem.itemNames[i] != itemNames[i]) return false;
if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) return false;
}
return true;
}
else if (getItem.itemNames == null && itemNames == null)
else if (getItem.itemIdentifiers == null && itemIdentifiers == null)
{
return getItem.targetItem == targetItem;
}
@@ -224,11 +261,11 @@ namespace Barotrauma
public override bool IsCompleted()
{
if (itemNames != null)
if (itemIdentifiers != null)
{
foreach (string itemName in itemNames)
foreach (string itemName in itemIdentifiers)
{
var matchingItem = character.Inventory.FindItem(itemName);
var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName);
if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem))) return true;
}
return false;
@@ -7,8 +7,6 @@ namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
private Entity target;
private Vector2 targetPos;
private bool repeat;
@@ -18,8 +16,17 @@ namespace Barotrauma
private bool getDivingGearIfNeeded;
public float CloseEnough = 0.5f;
public bool IgnoreIfTargetDead;
public bool AllowGoingOutside = false;
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Target != null && Target.Removed) return 0.0f;
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) return 0.0f;
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
@@ -32,25 +39,26 @@ namespace Barotrauma
{
get
{
if (Target != null && Target.Removed) return false;
if (repeat || waitUntilPathUnreachable > 0.0f) return true;
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable
if (pathSteering.CurrentPath == null) return true;
if (pathSteering?.CurrentPath == null) return true;
return (!pathSteering.CurrentPath.Unreachable);
if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) return false;
return !pathSteering.CurrentPath.Unreachable;
}
}
public Entity Target
{
get { return target; }
}
public Entity Target { get; private set; }
public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true)
: base (character, "")
{
this.target = target;
this.Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 5.0f;
@@ -70,58 +78,65 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (target == character)
if (Target == character)
{
character.AIController.SteeringManager.Reset();
return;
}
waitUntilPathUnreachable -= deltaTime;
if (character.SelectedConstruction!=null && character.SelectedConstruction.GetComponent<Ladder>()==null)
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
{
character.SelectedConstruction = null;
}
if (target != null) character.AIController.SelectTarget(target.AiTarget);
if (Target != null) character.AIController.SelectTarget(Target.AiTarget);
Vector2 currTargetPos = Vector2.Zero;
if (target == null)
if (Target == null)
{
currTargetPos = targetPos;
}
else
{
currTargetPos = target.SimPosition;
currTargetPos = Target.SimPosition;
//if character is outside the sub and target isn't, transform the position
if (character.Submarine != null && target.Submarine == null)
//if character is inside the sub and target isn't, transform the position
if (character.Submarine != null && Target.Submarine == null)
{
currTargetPos -= character.Submarine.SimPosition;
}
}
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < 0.5f * 0.5f)
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < CloseEnough * CloseEnough)
{
character.AIController.SteeringManager.Reset();
character.AnimController.TargetDir = currTargetPos.X > character.SimPosition.X ? Direction.Right : Direction.Left;
}
else
{
character.AIController.SteeringManager.SteeringSeek(currTargetPos);
var indoorsSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
{
indoorsSteering.SteeringWander();
}
else if (getDivingGearIfNeeded && indoorsSteering.CurrentPath != null && indoorsSteering.CurrentPath.HasOutdoorsNodes)
float normalSpeed = character.AnimController.GetCurrentSpeed(false);
character.AIController.SteeringManager.SteeringSeek(currTargetPos, normalSpeed);
if (getDivingGearIfNeeded && Target?.Submarine == null && AllowGoingOutside)
{
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
}
else if (character.AIController.SteeringManager is IndoorsSteeringManager indoorsSteering)
{
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
{
indoorsSteering.SteeringWander(normalSpeed);
}
else if (AllowGoingOutside &&
getDivingGearIfNeeded &&
indoorsSteering.CurrentPath != null &&
indoorsSteering.CurrentPath.HasOutdoorsNodes)
{
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
}
}
}
}
@@ -132,15 +147,18 @@ namespace Barotrauma
bool completed = false;
float allowedDistance = 0.5f;
var item = target as Item;
if (item != null)
if (Target is Item item)
{
allowedDistance = Math.Max(ConvertUnits.ToSimUnits(item.InteractDistance), allowedDistance);
if (item.IsInsideTrigger(character.WorldPosition)) completed = true;
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter)) completed = true;
}
completed = completed || Vector2.DistanceSquared(target != null ? target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance;
completed = completed || Vector2.DistanceSquared(Target != null ? Target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance;
if (completed) character.AIController.SteeringManager.Reset();
@@ -152,7 +170,7 @@ namespace Barotrauma
AIObjectiveGoTo objective = otherObjective as AIObjectiveGoTo;
if (objective == null) return false;
if (objective.target == target) return true;
if (objective.Target == Target) return true;
return (objective.targetPos == targetPos);
}
@@ -1,4 +1,6 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -12,10 +14,15 @@ namespace Barotrauma
private AITarget currentTarget;
private float newTargetTimer;
private float standStillTimer;
private float walkDuration;
private AIObjectiveFindSafety findSafety;
public AIObjectiveIdle(Character character) : base(character, "")
{
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
}
public override bool IsCompleted()
@@ -33,6 +40,16 @@ namespace Barotrauma
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering == null) return;
//don't keep dragging others when idling
if (character.SelectedCharacter != null)
{
character.DeselectCharacter();
}
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
{
character.SelectedConstruction = null;
}
if (character.AnimController.InWater)
{
//attempt to find a safer place if in water
@@ -48,10 +65,13 @@ namespace Barotrauma
if (currentTarget != null)
{
Vector2 pos = character.SimPosition;
if (character != null && character.Submarine == null) pos -= Submarine.MainSub.SimPosition;
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition);
if (path.Cost > 200.0f && character.AnimController.CurrentHull!=null) return;
if (character != null && character.Submarine == null) { pos -= Submarine.MainSub.SimPosition; }
string errorMsg = "(Character " + character.Name + " idling, target "
+ ((currentTarget.Entity is Hull hull && hull.RoomName != null) ? hull.RoomName : currentTarget.Entity.ToString()) + ")";
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition, errorMsg);
if (path.Cost > 1000.0f && character.AnimController.CurrentHull!=null) return;
pathSteering.SetPath(path);
}
@@ -70,6 +90,19 @@ namespace Barotrauma
if (pathSteering == null || (pathSteering.CurrentPath != null &&
(pathSteering.CurrentPath.NextNode == null || pathSteering.CurrentPath.Unreachable || pathSteering.CurrentPath.HasOutdoorsNodes)))
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(1.0f, 5.0f);
pathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(1.0f, 10.0f);
}
//steer away from edges of the hull
if (character.AnimController.CurrentHull != null)
{
@@ -102,7 +135,7 @@ namespace Barotrauma
}
}
character.AIController.SteeringManager.SteeringWander();
character.AIController.SteeringManager.SteeringWander(character.AnimController.GetCurrentSpeed(false));
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
@@ -115,20 +148,21 @@ namespace Barotrauma
currentTarget = null;
return;
}
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, 2.0f);
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, character.AnimController.GetCurrentSpeed(true));
}
private AITarget FindRandomTarget()
{
if (Rand.Int(5)==1)
//random chance of navigating back to the room where the character spawned
if (Rand.Int(5) == 1)
{
var idCard = character.Inventory.FindItem("ID Card");
if (idCard==null) return null;
var idCard = character.Inventory.FindItemByIdentifier("idcard");
if (idCard == null) return null;
foreach (WayPoint wp in WayPoint.WayPointList)
{
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull==null) continue;
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull == null) continue;
foreach (string tag in wp.IdCardTags)
{
if (idCard.HasTag(tag)) return wp.CurrentHull.AiTarget;
@@ -140,9 +174,31 @@ namespace Barotrauma
List<Hull> targetHulls = new List<Hull>(Hull.hullList);
//ignore all hulls with fires or water in them
targetHulls.RemoveAll(h => h.FireSources.Any() || h.WaterVolume / h.Volume > 0.1f);
if (!targetHulls.Any()) return null;
if (character.Submarine != null)
{
targetHulls.RemoveAll(h => h.Submarine != character.Submarine);
}
return targetHulls[Rand.Range(0, targetHulls.Count)].AiTarget;
//remove ballast hulls
foreach (Item item in Item.ItemList)
{
if (item.HasTag("ballast") && targetHulls.Contains(item.CurrentHull))
{
targetHulls.Remove(item.CurrentHull);
}
}
//ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{
float minHeight = ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value);
targetHulls.RemoveAll(h => h.CeilingHeight < minHeight);
}
if (!targetHulls.Any()) return null;
//prefer larger hulls
var targetHull = ToolBox.SelectWeightedRandom(targetHulls, targetHulls.Select(h => h.Volume).ToList(), Rand.RandSync.Unsynced);
return targetHull?.AiTarget;
}
return null;
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -12,7 +14,12 @@ namespace Barotrauma
private Character character;
private AIObjective currentOrder;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out (assuming they don't a high priority order active)
/// </summary>
public float WaitTimer;
public AIObjective CurrentOrder
{
get { return currentOrder; }
@@ -38,6 +45,23 @@ namespace Barotrauma
objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void AddObjective(AIObjective objective, float delay, Action callback = null)
{
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
{
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
{
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
}, delay);
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective
{
foreach (AIObjective objective in objectives)
@@ -47,15 +71,21 @@ namespace Barotrauma
return null;
}
public float GetCurrentPriority(Character character)
private AIObjective GetCurrentObjective()
{
if (CurrentOrder != null &&
(objectives.Count == 0 || currentOrder.GetPriority(this) > objectives[0].GetPriority(this)))
{
return CurrentOrder.GetPriority(this);
return CurrentOrder;
}
return objectives.Count == 0 ? 0.0f : objectives[0].GetPriority(this);
return objectives.Count == 0 ? null : objectives[0];
}
public float GetCurrentPriority()
{
var currentObjective = GetCurrentObjective();
return currentObjective == null ? 0.0f : currentObjective.GetPriority(this);
}
public void UpdateObjectives()
@@ -67,46 +97,77 @@ namespace Barotrauma
//sort objectives according to priority
objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this)));
GetCurrentObjective()?.SortSubObjectives(this);
}
public void DoCurrentObjective(float deltaTime)
{
if (currentOrder != null && (!objectives.Any() || objectives[0].GetPriority(this) < currentOrder.GetPriority(this)))
CurrentObjective = GetCurrentObjective();
if (CurrentObjective == null || (CurrentObjective.GetPriority(this) < OrderPriority && WaitTimer > 0.0f))
{
CurrentObjective = currentOrder;
currentOrder.TryComplete(deltaTime);
WaitTimer -= deltaTime;
character.AIController.SteeringManager.Reset();
return;
}
if (!objectives.Any()) return;
objectives[0].TryComplete(deltaTime);
CurrentObjective = objectives[0];
CurrentObjective?.TryComplete(deltaTime);
}
public void SetOrder(AIObjective objective)
{
currentOrder = objective;
}
public void SetOrder(Order order, string option)
public void SetOrder(Order order, string option, Character orderGiver)
{
currentOrder = null;
if (order == null) return;
currentOrder = null;
switch (order.Name.ToLowerInvariant())
switch (order.AITag.ToLowerInvariant())
{
case "follow":
currentOrder = new AIObjectiveGoTo(Character.Controlled, character, true);
currentOrder = new AIObjectiveGoTo(orderGiver, character, true)
{
CloseEnough = 1.5f,
AllowGoingOutside = true,
IgnoreIfTargetDead = true
};
break;
case "wait":
currentOrder = new AIObjectiveGoTo(character, character, true);
currentOrder = new AIObjectiveGoTo(character, character, true)
{
AllowGoingOutside = true
};
break;
case "fixleaks":
case "fix leaks":
currentOrder = new AIObjectiveFixLeaks(character);
break;
case "chargebatteries":
currentOrder = new AIObjectiveChargeBatteries(character, option);
break;
case "rescue":
currentOrder = new AIObjectiveRescueAll(character);
break;
case "repairsystems":
currentOrder = new AIObjectiveRepairItems(character) { RequireAdequateSkills = option != "all" };
break;
case "pumpwater":
currentOrder = new AIObjectivePumpWater(character, option);
break;
case "extinguishfires":
currentOrder = new AIObjectiveExtinguishFires(character);
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;
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break;
default:
if (order.TargetItem == null) return;
currentOrder = new AIObjectiveOperateItem(order.TargetItem, character, option, false, null, order.UseController);
if (order.TargetItemComponent == null) return;
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break;
}
}
@@ -17,10 +17,18 @@ namespace Barotrauma
private bool requireEquip;
private bool useController;
private AIObjectiveGoTo gotoObjective;
public override bool CanBeCompleted
{
get
{
if (gotoObjective != null && !gotoObjective.CanBeCompleted) return false;
if (useController && controller == null) return false;
return canBeCompleted;
}
}
@@ -43,23 +51,30 @@ namespace Barotrauma
public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false)
: base (character, option)
{
this.component = item;
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)
{
var controllers = item.Item.GetConnectedComponents<Controller>();
var controllers = component.Item.GetConnectedComponents<Controller>();
if (controllers.Any()) controller = controllers[0];
}
canBeCompleted = true;
}
protected override void Act(float deltaTime)
{
ItemComponent target = controller == null ? component : controller;
ItemComponent target = useController ? controller : component;
if (useController && controller == null)
{
character.Speak(TextManager.Get("DialogCantFindController").Replace("[item]", component.Item.Name), null, 2.0f, "cantfindcontroller", 30.0f);
return;
}
if (target.CanBeSelected)
{
@@ -75,11 +90,17 @@ namespace Barotrauma
return;
}
AddSubObjective(new AIObjectiveGoTo(target.Item, character));
AddSubObjective(gotoObjective = new AIObjectiveGoTo(target.Item, character));
}
else
{
if (!character.Inventory.Items.Contains(component.Item))
if (component.Item.GetComponent<Pickable>() == null)
{
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
canBeCompleted = false;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
{
AddSubObjective(new AIObjectiveGetItem(character, component.Item, true));
}
@@ -95,10 +116,10 @@ namespace Barotrauma
return;
}
for (int i = 0; i < CharacterInventory.limbSlots.Length; i++)
for (int i = 0; i < character.Inventory.Capacity; i++)
{
if (CharacterInventory.limbSlots[i] == InvSlotType.Any ||
!holdable.AllowedSlots.Any(s => s.HasFlag(CharacterInventory.limbSlots[i])))
if (character.Inventory.SlotTypes[i] == InvSlotType.Any ||
!holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
{
continue;
}
@@ -0,0 +1,108 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
class AIObjectivePumpWater : AIObjective
{
private const float FindPumpsInterval = 5.0f;
private string orderOption;
private List<Pump> pumps;
private float lastFindPumpsTime;
public AIObjectivePumpWater(Character character, string option)
: base(character, option)
{
orderOption = option;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Timing.TotalTime >= lastFindPumpsTime + FindPumpsInterval)
{
FindPumps();
}
if (objectiveManager.CurrentOrder == this && pumps.Count > 0)
{
return AIObjectiveManager.OrderPriority;
}
return 0.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectivePumpWater;
}
protected override void Act(float deltaTime)
{
if (Timing.TotalTime < lastFindPumpsTime + FindPumpsInterval)
{
return;
}
FindPumps();
}
private void FindPumps()
{
lastFindPumpsTime = (float)Timing.TotalTime;
pumps = new List<Pump>();
foreach (Item item in Item.ItemList)
{
//don't attempt to use pumps outside the sub
if (item.Submarine == null) { continue; }
var pump = item.GetComponent<Pump>();
if (pump == null) continue;
if (item.HasTag("ballast")) continue;
//if the pump is connected to an item with a steering component, it must be a ballast pump
//(This may not work correctly if the signals are passed through some fancy circuit or a wifi component,
//which is why sub creators are encouraged to tag the ballast pumps)
bool connectedToSteering = false;
foreach (Connection c in item.Connections)
{
if (c.IsPower) continue;
if (item.GetConnectedComponentsRecursive<Steering>(c).Count > 0)
{
connectedToSteering = true;
break;
}
}
if (connectedToSteering) continue;
if (orderOption.ToLowerInvariant() == "stop pumping")
{
if (!pump.IsActive || pump.FlowPercentage == 0.0f) continue;
}
else
{
if (!pump.Item.InWater) continue;
if (pump.IsActive && pump.FlowPercentage <= -90.0f) continue;
}
pumps.Add(pump);
}
foreach (Pump pump in pumps)
{
AddSubObjective(new AIObjectiveOperateItem(pump, character, orderOption, false));
}
}
}
}
@@ -0,0 +1,102 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
private Item item;
public AIObjectiveRepairItem(Character character, Item item)
: base(character, "")
{
this.item = item;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
bool insufficientSkills = true;
bool repairablesFound = false;
foreach (Repairable repairable in item.Repairables)
{
if (item.Condition > repairable.ShowRepairUIThreshold) { continue; }
if (repairable.DegreeOfSuccess(character) >= 0.5f) { insufficientSkills = false; }
repairablesFound = true;
}
if (!repairablesFound) { return 0.0f; }
float priority = 100.0f - item.Condition;
//vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist =
Math.Abs(character.WorldPosition.X - item.WorldPosition.X) +
Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y) * 2.0f;
//heavily increase the priority if the item is already selected
//so characters don't keep switching between nearby damaged items
if (character.SelectedConstruction == item)
{
priority += 50.0f;
}
if (insufficientSkills)
{
return MathHelper.Lerp(0.0f, 50.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
}
else
{
return MathHelper.Lerp(50.0f, 100.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
}
}
public override bool IsCompleted()
{
foreach (Repairable repairable in item.Repairables)
{
if (item.Condition < Math.Max(repairable.ShowRepairUIThreshold, item.Prefab.Health * 0.98f)) return false;
}
character?.Speak(TextManager.Get("DialogItemRepaired").Replace("[itemname]", item.Name), null, 0.0f, "itemrepaired", 10.0f);
return true;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.item == item;
}
protected override void Act(float deltaTime)
{
foreach (Repairable repairable in item.Repairables)
{
//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)
{
if (!character.Inventory.Items.Any(it => it != null && requiredItem.MatchesItem(it)))
{
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true));
return;
}
}
}
}
if (character.CanInteractWith(item))
{
foreach (Repairable repairable in item.Repairables)
{
if (character.SelectedConstruction != item) { item.TryInteract(character, true, true); }
repairable.CurrentFixer = character;
break;
}
}
else
{
AddSubObjective(new AIObjectiveGoTo(item, character));
}
}
}
}
@@ -0,0 +1,68 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjective
{
/// <summary>
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
/// </summary>
public bool RequireAdequateSkills;
public AIObjectiveRepairItems(Character character)
: base(character, "")
{
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
GetBrokenItems();
if (subObjectives.Count > 0 && objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
}
protected override void Act(float deltaTime)
{
GetBrokenItems();
}
private void GetBrokenItems()
{
foreach (Item item in Item.ItemList)
{
//ignore items that are in full condition
if (item.Condition >= 100.0f) continue;
foreach (Repairable repairable in item.Repairables)
{
//ignore ones that are already fixed
if (item.Condition > repairable.ShowRepairUIThreshold) continue;
if (RequireAdequateSkills)
{
if (!repairable.HasRequiredSkills(character)) { continue; }
}
AddSubObjective(new AIObjectiveRepairItem(character, item));
break;
}
}
}
}
}
@@ -1,17 +1,37 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Barotrauma
{
/*class AIObjectiveRescue : AIObjective
class AIObjectiveRescue : AIObjective
{
const float TreatmentDelay = 0.5f;
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private float treatmentTimer;
public override bool CanBeCompleted
{
get
{
if (targetCharacter.Removed) return false;
if (goToObjective != null && !goToObjective.CanBeCompleted) return false;
return true;
}
}
public AIObjectiveRescue(Character character, Character targetCharacter)
: base (character, "")
: base(character, "")
{
Debug.Assert(character != targetCharacter);
this.targetCharacter = targetCharacter;
}
@@ -21,14 +41,189 @@ namespace Barotrauma
return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter;
}
public override float GetPriority(Character character)
protected override void Act(float deltaTime)
{
if (targetCharacter.AnimController.CurrentHull == null) return 0.0f;
//target in water -> move to a dry place first
if (targetCharacter.AnimController.InWater)
{
if (character.SelectedCharacter != targetCharacter)
{
if (!character.CanInteractWith(targetCharacter))
{
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character));
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
AddSubObjective(new AIObjectiveFindSafety(character));
}
return;
}
float distance = Vector2.DistanceSquared(character.WorldPosition, targetCharacter.WorldPosition);
//target not in water -> we can start applying treatment
if (!character.CanInteractWith(targetCharacter))
{
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character));
}
else
{
if (character.SelectedCharacter == null)
{
character?.Speak(TextManager.Get("DialogFoundUnconsciousTarget")
.Replace("[targetname]", targetCharacter.Name).Replace("[roomname]", character.CurrentHull.RoomName),
null, 1.0f,
"foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance;
character.SelectCharacter(targetCharacter);
GiveTreatment(deltaTime);
}
}
}*/
protected override bool ShouldInterruptSubObjective(AIObjective subObjective)
{
if (subObjective is AIObjectiveFindSafety)
{
if (character.SelectedCharacter != targetCharacter) return true;
if (character.AnimController.InWater || targetCharacter.AnimController.InWater) return false;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (!Submarine.RectContains(targetCharacter.CurrentHull.WorldRect, limb.WorldPosition)) return false;
}
return !character.AnimController.InWater && !targetCharacter.AnimController.InWater &&
AIObjectiveFindSafety.GetHullSafety(character.CurrentHull, character) > 50.0f;
}
return false;
}
private void GiveTreatment(float deltaTime)
{
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
}
treatmentTimer = TreatmentDelay;
var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions()
.Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0)
.ToList();
allAfflictions.Sort((a1, a2) =>
{
return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth));
});
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions)
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (treatmentSuitability.Value > 0.0f)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key);
if (matchingItem == null) { continue; }
ApplyTreatment(affliction, matchingItem);
return;
}
}
}
//didn't have any suitable treatments available, try to find some medical items
HashSet<string> suitableItemIdentifiers = new HashSet<string>();
foreach (Affliction affliction in allAfflictions)
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (treatmentSuitability.Value > 0.0f)
{
suitableItemIdentifiers.Add(treatmentSuitability.Key);
}
}
}
if (suitableItemIdentifiers.Count > 0)
{
List<string> itemNameList = new List<string>();
foreach (string itemIdentifier in suitableItemIdentifiers)
{
if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)
{
itemNameList.Add(itemPrefab.Name);
}
//only list the first 4 items
if (itemNameList.Count >= 4) break;
}
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());
}
character?.Speak(TextManager.Get("DialogListRequiredTreatments")
.Replace("[targetname]", targetCharacter.Name)
.Replace("[treatmentlist]", itemListStr),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), true));
}
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(addMessage: false)) continue;
#if CLIENT
ic.PlaySound(ActionType.OnUse, character.WorldPosition, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
if (ic.DeleteOnUse) remove = true;
}
if (remove)
{
Entity.Spawner?.AddToRemoveQueue(item);
}
}
public override bool IsCompleted()
{
bool isCompleted = !targetCharacter.IsUnconscious || targetCharacter.IsDead;
if (isCompleted)
{
character?.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
}
return isCompleted;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (targetCharacter.AnimController.CurrentHull == null) return 0.0f;
float distance = Vector2.DistanceSquared(character.WorldPosition, targetCharacter.WorldPosition);
return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance;
}
}
}
@@ -3,7 +3,7 @@ using System.Linq;
namespace Barotrauma
{
/*class AIObjectiveRescueAll : AIObjective
class AIObjectiveRescueAll : AIObjective
{
private List<Character> rescueTargets;
@@ -18,13 +18,19 @@ namespace Barotrauma
return true;
}
public override float GetPriority(Character character)
public override float GetPriority(AIObjectiveManager objectiveManager)
{
GetRescueTargets();
if (!rescueTargets.Any()) { return 0.0f; }
if (objectiveManager.CurrentObjective == this)
{
return AIObjectiveManager.OrderPriority;
}
//if there are targets to rescue, the priority is slightly less
//than the priority of explicit orders given to the character
return rescueTargets.Any() ? AIObjectiveManager.OrderPriority - 5.0f : 0.0f;
return AIObjectiveManager.OrderPriority - 5.0f;
}
private void GetRescueTargets()
@@ -32,9 +38,7 @@ namespace Barotrauma
rescueTargets = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
c != character &&
(c.IsDead || c.IsUnconscious) &&
c.AnimController.CurrentHull != null &&
AIObjectiveFindSafety.GetHullSafety(c.AnimController.CurrentHull, c) < 50.0f);
c.IsUnconscious);
}
protected override void Act(float deltaTime)
@@ -44,5 +48,11 @@ namespace Barotrauma
AddSubObjective(new AIObjectiveRescue(character, target));
}
}
}*/
public override bool IsCompleted()
{
return false;
}
}
}
@@ -12,6 +12,12 @@ namespace Barotrauma
private static string ConfigFile = Path.Combine("Content", "Orders.xml");
public static List<Order> PrefabList;
public Order Prefab
{
get;
private set;
}
public readonly string Name;
public readonly string DoingText;
@@ -19,15 +25,25 @@ namespace Barotrauma
public readonly Sprite SymbolSprite;
public readonly Type ItemComponentType;
public readonly string ItemName;
public readonly string[] ItemIdentifiers;
public readonly string AITag;
public readonly Color Color;
//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 ItemComponent TargetItem;
public Controller ConnectedController;
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
static Order()
{
@@ -39,98 +55,125 @@ namespace Barotrauma
foreach (XElement orderElement in doc.Root.Elements())
{
if (orderElement.Name.ToString().ToLowerInvariant() != "order") continue;
PrefabList.Add(new Order(orderElement));
var newOrder = new Order(orderElement);
newOrder.Prefab = newOrder;
PrefabList.Add(newOrder);
}
//PrefabList.Add(new Order("Follow", "Following"));
//PrefabList.Add(new Order("Dismiss", "Dismissed"));
//PrefabList.Add(new Order("Wait", "Wait"));
//PrefabList.Add(new Order("Operate Reactor", "Operating reactor", typeof(Reactor), new string[] {"Power up", "Shutdown"}));
//PrefabList.Add(new Order("Operate Railgun", "Operating railgun", typeof(Turret), new string[] { "Fire at will", "Hold fire" }));
}
private Order(XElement orderElement)
{
Name = orderElement.GetAttributeString("name", "Name not found");
DoingText = orderElement.GetAttributeString("doingtext", "");
AITag = orderElement.GetAttributeString("aitag", "");
Name = TextManager.Get("OrderName." + AITag, true) ?? orderElement.GetAttributeString("name", "Name not found");
DoingText = TextManager.Get("OrderNameDoing." + AITag, true) ?? orderElement.GetAttributeString("doingtext", "");
string targetItemName = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemName))
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType))
{
try
{
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemName, true, true);
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemType, true, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemName + " not found", e);
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemType + " not found", e);
}
}
ItemName = orderElement.GetAttributeString("targetitemname", "");
Color = new Color(orderElement.GetAttributeVector4("color", new Vector4(1.0f, 1.0f, 1.0f, 1.0f)));
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
Color = orderElement.GetAttributeColor("color", Color.White);
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]);
string optionStr = orderElement.GetAttributeString("options", "");
if (string.IsNullOrWhiteSpace(optionStr))
string translatedOptionNames = TextManager.Get("OrderOptions." + AITag, true);
if (translatedOptionNames == null)
{
Options = new string[0];
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
}
else
{
Options = optionStr.Split(',');
for (int i = 0; i<Options.Length; i++)
string[] splitOptionNames = translatedOptionNames.Split(',');
OptionNames = new string[Options.Length];
for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++)
{
Options[i] = Options[i].Trim();
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;
}
foreach (XElement subElement in orderElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "sprite") continue;
SymbolSprite = new Sprite(subElement);
break;
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
SymbolSprite = new Sprite(subElement);
break;
}
}
}
private Order(string name, string doingText, Type itemComponentType, string[] parameters = null)
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem)
{
Name = name;
DoingText = doingText;
ItemComponentType = itemComponentType;
Options = parameters == null ? new string[0] : parameters;
}
Prefab = prefab;
public Order(Order prefab, ItemComponent targetItem)
{
Name = prefab.Name;
AITag = prefab.AITag;
DoingText = prefab.DoingText;
ItemComponentType = prefab.ItemComponentType;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
TargetItem = targetItem;
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController)
{
var controllers = targetItem.Item.GetConnectedComponents<Controller>();
if (controllers.Count > 0) ConnectedController = controllers[0];
}
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
}
public bool HasAppropriateJob(Character character)
{
if (AppropriateJobs == null || AppropriateJobs.Length == 0) return true;
if (character.Info == null || character.Info.Job == null) return false;
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) return true;
}
return false;
}
private Order(string name, string doingText, string[] parameters = null)
:this (name,doingText, null, parameters)
public string GetChatMessage(string targetCharacterName, string targetRoomName, string orderOption = "")
{
orderOption = orderOption ?? "";
string messageTag = "OrderDialog." + AITag;
if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption;
string msg = TextManager.Get(messageTag, true);
if (msg == null) return "";
if (targetCharacterName == null) targetCharacterName = "";
if (targetRoomName == null) targetRoomName = "";
return msg.Replace("[name]", targetCharacterName).Replace("[roomname]", targetRoomName);
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -154,24 +155,29 @@ namespace Barotrauma
}
}
public SteeringPath FindPath(Vector2 start, Vector2 end)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr)
{
float closestDist = 0.0f;
PathNode startNode = null;
foreach (PathNode node in nodes)
{
Vector2 nodePos = node.Position;
float dist = System.Math.Abs(start.X - nodePos.X) +
System.Math.Abs(start.Y - nodePos.Y) * 10.0f; //higher cost for vertical movement
float xDiff = System.Math.Abs(start.X - nodePos.X);
float yDiff = System.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 += Vector2.Distance(end, nodePos) / 10.0f;
if (dist<closestDist || startNode==null)
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) dist *= 10.0f;
if (dist < closestDist || startNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (insideSubmarine)
@@ -182,9 +188,9 @@ namespace Barotrauma
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;
}
}
@@ -195,7 +201,7 @@ namespace Barotrauma
if (startNode == null)
{
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node", Color.DarkRed);
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
return new SteeringPath();
}
@@ -207,6 +213,10 @@ namespace Barotrauma
Vector2 nodePos = node.Position;
float dist = Vector2.Distance(end, nodePos);
//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
@@ -223,16 +233,13 @@ namespace Barotrauma
if (endNode == null)
{
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node", Color.DarkRed);
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. "+ errorMsgStr, Color.DarkRed);
return new SteeringPath();
}
var path = FindPath(startNode,endNode);
sw.Stop();
System.Diagnostics.Debug.WriteLine("findpath: " + sw.ElapsedMilliseconds+" ms");
return path;
}
@@ -253,10 +260,9 @@ namespace Barotrauma
}
}
if (startNode == null || endNode == null)
{
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints", Color.DarkRed);
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
return new SteeringPath();
}
@@ -15,9 +15,7 @@ namespace Barotrauma
private Vector2 steering;
//the steering amount when avoiding obstacles
//(needs a separate variable because it's only updated when a raycast is done to detect any nearby obstacles)
private Vector2 avoidSteering;
private Vector2? avoidObstaclePos;
private float rayCastTimer;
private float wanderAngle;
@@ -35,19 +33,19 @@ namespace Barotrauma
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
}
public void SteeringSeek(Vector2 targetSimPos, float speed = 1.0f)
public void SteeringSeek(Vector2 targetSimPos, float speed)
{
steering += DoSteeringSeek(targetSimPos, speed);
}
public void SteeringWander(float speed = 1.0f)
public void SteeringWander(float speed)
{
steering += DoSteeringWander(speed);
}
public void SteeringAvoid(float deltaTime, float speed)
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
{
steering += DoSteeringAvoid(deltaTime, speed);
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, speed);
}
public void SteeringManual(float deltaTime, Vector2 velocity)
@@ -70,7 +68,7 @@ namespace Barotrauma
steering.Y = 0.0f;
}
public virtual void Update(float speed = 1.0f)
public virtual void Update(float speed)
{
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
{
@@ -88,7 +86,7 @@ namespace Barotrauma
host.Steering = steering;
}
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed = 1.0f)
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed)
{
Vector2 targetVel = target - host.SimPosition;
@@ -108,9 +106,9 @@ namespace Barotrauma
return newSteering;
}
protected virtual Vector2 DoSteeringWander(float speed = 1.0f)
protected virtual Vector2 DoSteeringWander(float speed)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? new Vector2(speed, 0.0f) : host.Steering;
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(speed) : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2(
@@ -132,21 +130,19 @@ namespace Barotrauma
return newSteering;
}
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float speed = 1.0f)
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
{
if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero;
float maxDistance = 2.0f;
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
float maxDistance = lookAheadDistance;
if (rayCastTimer <= 0.0f)
{
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
rayCastTimer = RayCastInterval;
Body closestBody = Submarine.CheckVisibility(host.SimPosition, ahead);
if (closestBody == null)
{
avoidSteering = Vector2.Zero;
avoidObstaclePos = null;
return Vector2.Zero;
}
else
@@ -163,19 +159,19 @@ namespace Barotrauma
obstaclePosition.X = closestStructure.SimPosition.X;
}
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
avoidObstaclePos = obstaclePosition;
//avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
}
else if (closestBody.UserData is Item item)
/*else if (closestBody.UserData is Item)
{
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - item.SimPosition);
}
}*/
else
{
avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
avoidObstaclePos = Submarine.LastPickedPosition;
//avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
}
//failed to normalize (the obstacle to avoid is at the same position as the character?)
// -> move to a random direction
if (!MathUtils.IsValid(avoidSteering)) avoidSteering = Rand.Vector(1.0f);
}
}
@@ -184,7 +180,14 @@ namespace Barotrauma
rayCastTimer -= deltaTime;
}
return avoidSteering * speed;
if (!avoidObstaclePos.HasValue) return Vector2.Zero;
Vector2 diff = avoidObstaclePos.Value - host.SimPosition;
float dist = diff.Length();
if (dist > maxDistance) return Vector2.Zero;
return -diff * (1.0f - dist / maxDistance) * speed;
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
@@ -19,16 +20,25 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false)
: base(file, position, characterInfo, isNetworkPlayer)
public AICharacter(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(file, position, seed, characterInfo, isNetworkPlayer, ragdoll)
{
}
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)
@@ -67,7 +77,7 @@ namespace Barotrauma
}
}
if (IsDead || Health <= 0.0f || IsUnconscious || Stun > 0.0f) return;
if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return;
if (Controlled == this || !aiController.Enabled) return;
SoundUpdate(deltaTime);
@@ -78,21 +88,5 @@ namespace Barotrauma
}
}
partial void SoundUpdate(float deltaTime);
public override void AddDamage(CauseOfDeath causeOfDeath, float amount, Character attacker)
{
base.AddDamage(causeOfDeath, amount, attacker);
if (attacker != null) aiController.OnAttacked(attacker, amount);
}
public override AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb limb = null)
{
AttackResult result = base.ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, limb);
aiController.OnAttacked(attacker, result.Damage + result.Bleeding);
return result;
}
}
}
@@ -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;
}
}
}
@@ -1,74 +1,222 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System.Xml.Linq;
using System.Collections.Generic;
using System;
namespace Barotrauma
{
class AnimController : Ragdoll
abstract class AnimController : Ragdoll
{
public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR };
public Animation Anim;
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 LimbType GrabLimb;
protected Character character;
protected float walkSpeed, swimSpeed;
protected float walkPos;
protected readonly Vector2 stepSize;
protected readonly float legTorque;
public float RunSpeedMultiplier
{
get;
private set;
}
public float SwimSpeedMultiplier
{
get;
private set;
}
public Vector2 AimSourcePos
{
get { return ConvertUnits.ToDisplayUnits(AimSourceSimPos); }
}
public virtual Vector2 AimSourceSimPos
public AnimationParams CurrentAnimationParams
{
get
{
return Collider.SimPosition;
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 AnimController(Character character, XElement element)
: base(character, element)
public bool CanWalk => CanEnterSubmarine;
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
{
this.character = character;
stepSize = element.GetAttributeVector2("stepsize", Vector2.One);
stepSize = ConvertUnits.ToSimUnits(stepSize);
walkSpeed = element.GetAttributeFloat("walkspeed", 1.0f);
swimSpeed = element.GetAttributeFloat("swimspeed", 1.0f);
RunSpeedMultiplier = element.GetAttributeFloat("runspeedmultiplier", 2f);
SwimSpeedMultiplier = element.GetAttributeFloat("swimspeedmultiplier", 1.5f);
legTorque = element.GetAttributeFloat("legtorque", 0.0f);
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) { }
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) { }
public virtual void DragCharacter(Character target, float deltaTime) { }
public virtual void UpdateUseItem(bool allowMovement, Vector2 handPos) { }
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;
default:
throw new NotImplementedException(type.ToString());
}
}
}
}
@@ -2,52 +2,130 @@
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class FishAnimController : AnimController
{
//amplitude and wave length of the "sine wave" swimming animation
//if amplitude = 0, sine wave animation isn't used
private float waveAmplitude;
private float waveLength;
public override RagdollParams RagdollParams
{
get { return FishRagdollParams; }
protected set { FishRagdollParams = value as FishRagdollParams; }
}
private float steerTorque;
private FishRagdollParams _ragdollParams;
public FishRagdollParams FishRagdollParams
{
get
{
if (_ragdollParams == null)
{
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
}
return _ragdollParams;
}
protected set
{
_ragdollParams = value;
}
}
private bool rotateTowardsMovement;
private FishWalkParams _fishWalkParams;
public FishWalkParams FishWalkParams
{
get
{
if (_fishWalkParams == null)
{
_fishWalkParams = FishWalkParams.GetDefaultAnimParams(character);
}
return _fishWalkParams;
}
set { _fishWalkParams = value; }
}
private bool mirror, flip;
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;
private float? footRotation;
private float deathAnimTimer, deathAnimDuration = 5.0f;
public FishAnimController(Character character, XElement element)
: base(character, element)
{
waveAmplitude = ConvertUnits.ToSimUnits(element.GetAttributeFloat("waveamplitude", 0.0f));
waveLength = ConvertUnits.ToSimUnits(element.GetAttributeFloat("wavelength", 0.0f));
steerTorque = element.GetAttributeFloat("steertorque", 25.0f);
flip = element.GetAttributeBool("flip", true);
mirror = element.GetAttributeBool("mirror", false);
float footRot = element.GetAttributeFloat("footrotation", float.NaN);
if (float.IsNaN(footRot))
{
footRotation = null;
}
else
{
footRotation = MathHelper.ToRadians(footRot);
}
rotateTowardsMovement = element.GetAttributeBool("rotatetowardsmovement", true);
}
public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public override void UpdateAnim(float deltaTime)
{
@@ -55,38 +133,25 @@ namespace Barotrauma
if (character.IsDead || character.IsUnconscious || character.Stun > 0.0f)
{
Collider.Enabled = false;
Collider.FarseerBody.FixedRotation = false;
if (character.IsRemotePlayer)
{
if (!SimplePhysicsEnabled)
{
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true;
}
}
else
{
Vector2 diff = (MainLimb.SimPosition - Collider.SimPosition);
if (diff.LengthSquared() > 10.0f * 10.0f)
{
Collider.SetTransform(MainLimb.SimPosition, MainLimb.Rotation);
}
else
{
Collider.LinearVelocity = diff * 60.0f;
Collider.SmoothRotate(MainLimb.Rotation);
}
}
//set linear velocity even though the collider is disabled,
//because the character won't be able to switch back from ragdoll mode until the velocity of the collider is low enough
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
if (character.IsDead && deathAnimTimer < deathAnimDuration)
{
deathAnimTimer += deltaTime;
UpdateDying(deltaTime);
UpdateDying(deltaTime);
}
return;
}
else
{
deathAnimTimer = 0.0f;
}
//re-enable collider
if (!Collider.Enabled)
@@ -109,18 +174,18 @@ namespace Barotrauma
strongestImpact = 0.0f;
}
if (inWater)
if (inWater && !forceStanding)
{
Collider.FarseerBody.FixedRotation = false;
UpdateSineAnim(deltaTime);
}
else if (currentHull != null && CanEnterSubmarine)
else if (CanEnterSubmarine && (currentHull != null || forceStanding) && CurrentGroundedParams != null)
{
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, 0.0f)) > 0.001f)
//rotate collider back upright
float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians;
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f)
{
//rotate collider back upright
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, 0.0f) * 60.0f;
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f;
Collider.FarseerBody.FixedRotation = false;
}
else
@@ -134,26 +199,37 @@ namespace Barotrauma
//don't flip or drag when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotePlayer)
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
{
if (mirror || !inWater)
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
if (targetMovement.X > 0.1f && targetMovement.X > Math.Abs(targetMovement.Y) * 0.5f)
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.5f)
else if (targetMovement.X < -0.1f && targetMovement.X < -Math.Abs(targetMovement.Y) * 0.2f)
{
TargetDir = Direction.Left;
}
}
else
{
Limb head = GetLimb(LimbType.Head);
if (head == null) head = GetLimb(LimbType.Torso);
float refAngle = 0.0f;
Limb refLimb = GetLimb(LimbType.Head);
if (refLimb == null)
{
refAngle = CurrentAnimationParams.TorsoAngleInRadians;
refLimb = GetLimb(LimbType.Torso);
}
else
{
refAngle = CurrentAnimationParams.HeadAngleInRadians;
}
float rotation = MathUtils.WrapAngleTwoPi(head.Rotation);
rotation = MathHelper.ToDegrees(rotation);
float rotation = refLimb.Rotation;
if (!float.IsNaN(refAngle)) { rotation -= refAngle * Dir; }
rotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(rotation));
if (rotation < 0.0f) rotation += 360;
@@ -168,9 +244,10 @@ namespace Barotrauma
}
}
if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter);
if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter, deltaTime);
if (!flip) return;
if (!CurrentFishAnimation.Flip || IsStuck) return;
if (character.AIController != null && !character.AIController.CanFlip) return;
flipTimer += deltaTime;
@@ -179,7 +256,10 @@ namespace Barotrauma
if (flipTimer > 1.0f || character.IsRemotePlayer)
{
Flip();
if (mirror || !inWater) Mirror();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror();
}
flipTimer = 0.0f;
}
}
@@ -187,7 +267,7 @@ namespace Barotrauma
private float eatTimer = 0.0f;
public override void DragCharacter(Character target)
public override void DragCharacter(Character target, float deltaTime)
{
if (target == null) return;
@@ -202,20 +282,9 @@ namespace Barotrauma
Character targetCharacter = target;
float eatSpeed = character.Mass / targetCharacter.Mass * 0.1f;
eatTimer += deltaTime * eatSpeed;
eatTimer += (float)Timing.Step * eatSpeed;
Vector2 mouthPos = mouthLimb.SimPosition;
if (mouthLimb.MouthPos.HasValue)
{
float cos = (float)Math.Cos(mouthLimb.Rotation);
float sin = (float)Math.Sin(mouthLimb.Rotation);
mouthPos += new Vector2(
mouthLimb.MouthPos.Value.X * cos - mouthLimb.MouthPos.Value.Y * sin,
mouthLimb.MouthPos.Value.X * sin + mouthLimb.MouthPos.Value.Y * cos);
}
Vector2 mouthPos = GetMouthPosition().Value;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
@@ -232,19 +301,21 @@ namespace Barotrauma
float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f));
mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength);
if (eatTimer % 1.0f < 0.5f && (eatTimer - (float)Timing.Step * eatSpeed) % 1.0f > 0.5f)
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
{
//apply damage to the target character to get some blood particles flying
targetCharacter.AnimController.MainLimb.AddDamage(targetCharacter.SimPosition, DamageType.None, Rand.Range(10.0f, 25.0f), 10.0f, false);
targetCharacter.AnimController.MainLimb.AddDamage(targetCharacter.SimPosition, 0.0f, 20.0f, 0.0f, false);
//keep severing joints until there is only one limb left
LimbJoint[] nonSeveredJoints = Array.FindAll(targetCharacter.AnimController.LimbJoints, l => !l.IsSevered && l.CanBeSevered);
LimbJoint[] nonSeveredJoints = Array.FindAll(targetCharacter.AnimController.LimbJoints,
l => !l.IsSevered && l.CanBeSevered && l.LimbA != null && !l.LimbA.IsSevered && l.LimbB != null && !l.LimbB.IsSevered);
if (nonSeveredJoints.Length == 0)
{
//only one limb left, the character is now full eaten
Entity.Spawner.AddToRemoveQueue(targetCharacter);
character.SelectedCharacter = null;
character.Health += 10.0f;
}
else //sever a random joint
{
@@ -260,42 +331,124 @@ namespace Barotrauma
void UpdateSineAnim(float deltaTime)
{
movement = TargetMovement * swimSpeed;
if (CurrentSwimParams == null) { return; }
movement = TargetMovement;
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, 0.5f);
if (movement.LengthSquared() > 0.00001f)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, 0.5f);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
MainLimb.PullJointEnabled = true;
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (movement.LengthSquared() < 0.00001f) return;
//MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (movement.LengthSquared() < 0.00001f)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
MainLimb.PullJointWorldAnchorB = Vector2.Lerp(MainLimb.PullJointWorldAnchorB, Collider.SimPosition, 0.5f);
return;
}
float movementAngle = MathUtils.VectorToAngle(movement) - MathHelper.PiOver2;
if (rotateTowardsMovement)
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle.Value : HeadAngle.Value) * Dir;
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
Collider.SmoothRotate(movementAngle, 25.0f);
MainLimb.body.SmoothRotate(movementAngle, steerTorque);
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);
//tail?.body.SmoothRotate(movementAngle + TailAngle.Value * Dir, TailTorque);
if (tail != null)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque);
}
}
}
else
{
Collider.SmoothRotate(HeadAngle * Dir, 25.0f);
MainLimb.body.SmoothRotate(HeadAngle * Dir, steerTorque);
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
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);
}
}
Limb tail = GetLimb(LimbType.Tail);
if (tail != null && waveAmplitude > 0.0f)
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude);
if (waveLength > 0 && waveAmplitude > 0)
{
walkPos -= movement.Length();
float waveRotation = (float)Math.Sin(walkPos / waveLength);
tail.body.ApplyTorque(waveRotation * tail.Mass * 100.0f * waveAmplitude);
WalkPos -= movement.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.limbParams.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.limbParams.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 * CurrentSwimParams.TailTorque * waveAmplitude);
}
break;
}
}
for (int i = 0; i < Limbs.Length; i++)
{
@@ -304,13 +457,24 @@ namespace Barotrauma
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos);
}
if (CurrentSwimParams.UseSineMovement)
{
MainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(MainLimb.PullJointWorldAnchorB, Collider.SimPosition, (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointWorldAnchorB = Vector2.Lerp(MainLimb.PullJointWorldAnchorB, Collider.SimPosition, 0.5f);
}
floorY = Limbs[0].SimPosition.Y;
}
void UpdateWalkAnim(float deltaTime)
{
movement = MathUtils.SmoothStep(movement, TargetMovement * walkSpeed, 0.2f);
if (CurrentGroundedParams == null) { return; }
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
Collider.LinearVelocity = new Vector2(
movement.X,
@@ -319,30 +483,83 @@ namespace Barotrauma
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
float mainLimbHeight, mainLimbAngle;
if (MainLimb.type == LimbType.Torso)
float mainLimbHeight = ColliderHeightFromFloor;
Vector2 colliderBottom = GetColliderBottom();
float movementAngle = 0.0f;
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle.Value : HeadAngle.Value) * Dir;
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
mainLimbHeight = TorsoPosition;
mainLimbAngle = torsoAngle;
movementAngle += MathHelper.TwoPi;
}
else
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
mainLimbHeight = HeadPosition;
mainLimbAngle = headAngle;
movementAngle -= MathHelper.TwoPi;
}
MainLimb.body.SmoothRotate(mainLimbAngle * Dir, 50.0f);
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 + Vector2.UnitY * TorsoPosition.Value;
MainLimb.MoveToPos(GetColliderBottom() + Vector2.UnitY * mainLimbHeight, 10.0f);
MainLimb.PullJointEnabled = true;
MainLimb.PullJointWorldAnchorB = GetColliderBottom() + Vector2.UnitY * mainLimbHeight;
if (torso != MainLimb)
pos.X = torso.SimPosition.X;
else
mainLimbHeight = TorsoPosition.Value;
walkPos -= MainLimb.LinearVelocity.X * 0.05f;
torso.MoveToPos(pos, TorsoMoveForce);
torso.PullJointEnabled = true;
torso.PullJointWorldAnchorB = pos;
}
}
Vector2 transformedStepSize = new Vector2(
(float)Math.Cos(walkPos) * stepSize.X * 3.0f,
(float)Math.Sin(walkPos) * stepSize.Y * 2.0f);
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 + Vector2.UnitY * HeadPosition.Value;
if (head != MainLimb)
pos.X = head.SimPosition.X;
else
mainLimbHeight = HeadPosition.Value;
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);
}
}
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)
{
@@ -350,56 +567,78 @@ namespace Barotrauma
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
Vector2 footPos = new Vector2(limb.SimPosition.X, MainLimb.SimPosition.Y - mainLimbHeight);
Vector2 footPos = new Vector2(limb.SimPosition.X, colliderBottom.Y);
if (limb.RefJointIndex>-1)
if (limb.RefJointIndex > -1)
{
RevoluteJoint refJoint = LimbJoints[limb.RefJointIndex];
footPos.X = refJoint.WorldAnchorA.X;
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;
if (limb.type == LimbType.LeftFoot)
{
limb.MoveToPos(footPos +new Vector2(
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),
8.0f);
(transformedStepSize.Y > 0.0f) ? transformedStepSize.Y : 0.0f);
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
}
else if (limb.type == LimbType.RightFoot)
{
limb.MoveToPos(footPos + new Vector2(
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),
8.0f);
(-transformedStepSize.Y > 0.0f) ? -transformedStepSize.Y : 0.0f);
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
}
if (footRotation != null) limb.body.SmoothRotate((float)footRotation * Dir, 50.0f);
if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID))
{
SmoothRotateWithoutWrapping(limb,
movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.limbParams.ID] * Dir,
MainLimb, FootTorque);
}
break;
case LimbType.LeftLeg:
case LimbType.RightLeg:
if (legTorque != 0.0f) limb.body.ApplyTorque(limb.Mass * legTorque * Dir);
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 animStrength = (1.0f - deathAnimTimer / deathAnimDuration);
Limb head = GetLimb(LimbType.Head);
Limb tail = GetLimb(LimbType.Tail);
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * Math.Sin(walkPos)) * 10.0f);
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (float)Math.Sin(walkPos)) * 10.0f);
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * Math.Sin(WalkPos)) * 30.0f * animStrength);
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * Math.Sin(WalkPos)) * 30.0f * animStrength);
walkPos += deltaTime * 5.0f;
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);
}
#endif
if (limb.type == LimbType.Head || limb.type == LimbType.Tail || limb.IsSevered || !limb.body.Enabled) continue;
if (limb.Mass <= 0.0f)
{
@@ -420,20 +659,33 @@ namespace Barotrauma
return;
}
limb.body.ApplyForce(diff * (float)(Math.Sin(walkPos) * Math.Sqrt(limb.Mass)) * 10.0f);
limb.body.ApplyForce(diff * (float)(Math.Sin(WalkPos) * Math.Sqrt(limb.Mass)) * 30.0f * animStrength);
}
}
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;
l.body.SetTransform(l.SimPosition,
-l.body.Rotation);
l.body.SetTransform(l.SimPosition, -l.body.Rotation);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,398 @@
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), Editable(DecimalCount = 2, ToolTip = "How big steps the character takes.")]
public Vector2 StepSize
{
get;
set;
}
[Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's head is positioned.")]
public float HeadPosition { get; set; }
[Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's torso is positioned.")]
public float TorsoPosition { get; set; }
[Serialize(0.75f, true), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2, ToolTip = "The character's movement speed is multiplied with this value when moving backwards.")]
public float BackwardsMovementMultiplier { get; set; }
}
abstract class SwimParams : AnimationParams
{
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerTorque { get; set; }
}
abstract class AnimationParams : EditableParams
{
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)]
public float MovementSpeed { get; set; }
[Serialize(1.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2,
ToolTip = "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)")]
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 GetDefaultFolder(string speciesName) => $"Content/Characters/{speciesName.CapitaliseFirstInvariant()}/Animations/";
public static string GetDefaultFile(string speciesName, AnimationType animType) => $"{GetDefaultFolder(speciesName)}{GetDefaultFileName(speciesName, animType)}.xml";
protected static string GetFolder(string speciesName)
{
var folder = XMLExtensions.TryLoadXml(Character.GetConfigFile(speciesName))?.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
{
folder = GetDefaultFolder(speciesName);
}
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).ToLowerInvariant() == fileName.ToLowerInvariant());
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))
{
if (!anims.ContainsKey(a.Name))
{
anims.Add(a.Name, 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 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(instance.Name, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance as T;
}
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
protected void CreateSnapshot<T>() where T : AnimationParams, new()
{
Serialize();
var copy = new T
{
IsLoaded = true,
doc = new XDocument(doc)
};
copy.Deserialize();
copy.Serialize();
memento.Store(copy);
}
public override void Undo() => Deserialize(memento.Undo().MainElement);
public override void Redo() => Deserialize(memento.Redo().MainElement);
#endregion
}
}
@@ -0,0 +1,215 @@
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 CreateSnapshot() => CreateSnapshot<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 CreateSnapshot() => CreateSnapshot<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 CreateSnapshot() => CreateSnapshot<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 CreateSnapshot() => CreateSnapshot<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;
}
[Serialize(true, true), Editable(ToolTip = "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), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the head to the correct position.")]
public float HeadMoveForce { get; set; }
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the torso to the correct position.")]
public float TorsoMoveForce { get; set; }
[Serialize(8.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")]
public float FootMoveForce { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")]
public float HeadTorque { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")]
public float TorsoTorque { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")]
public float TailTorque { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
public float FootTorque { get; set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "Optional torque that's constantly applied to legs.")]
public float LegTorque { get; set; }
/// <summary>
/// The angle of the collider when standing (i.e. out of water).
/// In degrees.
/// </summary>
[Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "The angle of the character's collider when standing.")]
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), Editable(ToolTip = "TODO")]
public bool UseSineMovement { get; set; }
[Serialize(true, true), Editable(ToolTip = "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(true, true), Editable(ToolTip = "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(1f, true), Editable]
public float WaveAmplitude { get; set; }
[Serialize(10.0f, true), Editable]
public float WaveLength { get; set; }
[Serialize(true, true), Editable(ToolTip = "Should the character face towards the direction it's heading.")]
public bool RotateTowardsMovement { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")]
public float HeadTorque { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")]
public float TailTorque { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
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 CreateSnapshot() => CreateSnapshot<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 CreateSnapshot() => CreateSnapshot<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 CreateSnapshot() => CreateSnapshot<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 CreateSnapshot() => CreateSnapshot<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), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
public float FootRotateStrength { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
{
[Serialize(0.3f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2, ToolTip = "How much force is used to force the character upright.")]
public float GetUpForce { get; set; }
// -- TODO: use a separate clip for crawling -> replace these when implemented.
[Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the torso when crouching.")]
public float CrouchingTorsoPos { get; set; }
[Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the head when crouching.")]
public float CrouchingHeadPos { get; set; }
/// <summary>
/// In degrees
/// </summary>
[Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the torso when crouching.")]
public float CrouchingTorsoAngle { get; set; }
/// <summary>
/// In degrees
/// </summary>
[Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the head when crouching.")]
public float CrouchingHeadAngle { get; set; }
// --
[Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's head leans forwards when moving.")]
public float HeadLeanAmount { get; set; }
[Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's torso leans forwards when moving.")]
public float TorsoLeanAmount { get; set; }
[Serialize(15.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")]
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), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
public float FootRotateStrength { get; set; }
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2, ToolTip = "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.")]
public Vector2 FootMoveOffset { get; set; }
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2, ToolTip = "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.")]
public Vector2 CrouchingFootMoveOffset { get; set; }
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to bend the characters legs when taking a step.")]
public float LegBendTorque { get; set; }
[Serialize("0.4, 0.15", true), Editable(DecimalCount = 2, ToolTip = "How much the hands move along each axis.")]
public Vector2 HandMoveAmount { get; set; }
[Serialize("-0.15, 0.0", true), Editable(DecimalCount = 2, ToolTip = "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.")]
public Vector2 HandMoveOffset { get; set; }
[Serialize(0.7f, true), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2, ToolTip = "How much force is used to move the hands.")]
public float HandMoveStrength { get; set; }
[Serialize(-1.0f, true), Editable(DecimalCount = 2, ToolTip = "The position of the hands is clamped below this (relative to the position of the character's torso).")]
public float HandClampY { get; set; }
}
public interface IHumanAnimation
{
float FootAngle { get; set; }
float FootAngleInRadians { get; }
float FootRotateStrength { get; set; }
}
}
@@ -0,0 +1,129 @@
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Linq;
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 XElement MainElement => doc.Root;
public XElement OriginalElement { get; protected set; }
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;
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 = Path.GetFileNameWithoutExtension(FullPath);
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)
{
if (!IsLoaded)
{
DebugConsole.ThrowError("[Params] Not loaded!");
return;
}
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true);
}
#endif
#region Memento
public readonly Memento<EditableParams> memento = new Memento<EditableParams>();
public abstract void CreateSnapshot();
public abstract void Undo();
public abstract void Redo();
public void ClearHistory() => memento.Clear();
#endregion
}
}
@@ -0,0 +1,649 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using System.IO;
using Barotrauma.Extensions;
using System.Xml;
namespace Barotrauma
{
class HumanRagdollParams : RagdollParams
{
public static HumanRagdollParams GetRagdollParams(string speciesName, string fileName = null) => GetRagdollParams<HumanRagdollParams>(speciesName, fileName);
public static HumanRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams<HumanRagdollParams>(speciesName);
}
class FishRagdollParams : RagdollParams
{
public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams<FishRagdollParams>(speciesName);
}
class RagdollParams : EditableParams
{
public const float MIN_SCALE = 0.1f;
public const float MAX_SCALE = 2;
public string SpeciesName { get; private set; }
[Serialize(0f, true), Editable(-360, 360, ToolTip = "Rotation offset (in degrees) used for animations and widgets. If the sprites in the sheet are in different orientations, use the orientation of the torso for the final version of your character (while editing the character in the editor, you can change the orientation freely).")]
public float SpritesheetOrientation { get; set; }
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float LimbScale { get; set; }
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float JointScale { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2)]
public float TextureScale { get; set; }
[Serialize(45f, true), Editable(0f, 1000f)]
public float ColliderHeightFromFloor { get; set; }
[Serialize(50f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float ImpactTolerance { get; set; }
[Serialize(true, true), Editable]
public bool CanEnterSubmarine { get; set; }
[Serialize(true, true), Editable]
public bool Draggable { get; set; }
private static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
public List<ColliderParams> ColliderParams { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
public List<JointParams> Joints { get; private set; } = new List<JointParams>();
protected IEnumerable<RagdollSubParams> GetAllSubParams() =>
ColliderParams.Select(c => c as RagdollSubParams)
.Concat(Limbs.Select(j => j as RagdollSubParams)
.Concat(Joints.Select(j => j as RagdollSubParams)));
public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll";
public static string GetDefaultFolder(string speciesName) => $"Content/Characters/{speciesName.CapitaliseFirstInvariant()}/Ragdolls/";
public static string GetDefaultFile(string speciesName) => $"{GetDefaultFolder(speciesName)}{GetDefaultFileName(speciesName)}.xml";
private static readonly object[] dummyParams = new object[]
{
new XAttribute("type", "Dummy"),
new XElement("collider", new XAttribute("radius", 1)),
new XElement("limb",
new XAttribute("id", 0),
new XAttribute("type", LimbType.Head.ToString()),
new XAttribute("width", 1),
new XAttribute("height", 1),
new XElement("sprite",
new XAttribute("sourcerect", $"0, 0, 1, 1")))
};
protected static string GetFolder(string speciesName)
{
var folder = XMLExtensions.TryLoadXml(Character.GetConfigFile(speciesName))?.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
{
folder = GetDefaultFolder(speciesName);
}
return folder;
}
public static T GetDefaultRagdollParams<T>(string speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName, GetDefaultFileName(speciesName));
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetRagdollParams<T>(string speciesName, string fileName = null) where T : RagdollParams, new()
{
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
{
string selectedFile = null;
string folder = GetFolder(speciesName);
if (Directory.Exists(folder))
{
var files = Directory.GetFiles(folder);
if (files.None())
{
DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(speciesName);
}
else
{
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
if (selectedFile == null)
{
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
}
}
else
{
DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
}
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
T r = new T();
if (r.Load(selectedFile, speciesName))
{
if (!ragdolls.ContainsKey(r.Name))
{
ragdolls.Add(r.Name, r);
}
return r;
}
else
{
DebugConsole.ThrowError($"[RagdollParams] Failed to load ragdoll {r} at {selectedFile} for the character {speciesName}. Creating a dummy file.");
var defaultFile = GetDefaultFile(speciesName);
if (File.Exists(defaultFile))
{
DebugConsole.ThrowError($"[RagdollParams] Renaming the invalid file as {selectedFile}.invalid");
// Rename the old file so that it's not lost.
File.Move(defaultFile, defaultFile + ".invalid");
}
return CreateDefault<T>(defaultFile, speciesName, dummyParams);
}
}
return (T)ragdoll;
}
/// <summary>
/// Creates a default ragdoll for the species using a predefined configuration.
/// Note: Use only to create ragdolls for new characters, because this overrides the old ragdoll!
/// </summary>
public static T CreateDefault<T>(string fullPath, string speciesName, params object[] ragdollConfig) where T : RagdollParams, new()
{
// Remove the old ragdolls, if found.
if (allRagdolls.ContainsKey(speciesName))
{
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
allRagdolls.Remove(speciesName);
}
var ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
var instance = new T();
XElement ragdollElement = new XElement("Ragdoll", ragdollConfig);
instance.doc = new XDocument(ragdollElement);
instance.UpdatePath(fullPath);
instance.IsLoaded = instance.Deserialize(ragdollElement);
instance.Save();
instance.Load(fullPath, speciesName);
ragdolls.Add(instance.Name, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance as T;
}
protected override void UpdatePath(string fullPath)
{
if (SpeciesName == null)
{
base.UpdatePath(fullPath);
}
else
{
// Update the key by removing and re-adding the ragdoll.
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls.Remove(Name);
}
base.UpdatePath(fullPath);
if (ragdolls != null)
{
if (!ragdolls.ContainsKey(Name))
{
ragdolls.Add(Name, this);
}
}
}
}
public bool Save(string fileNameWithoutExtension = null)
{
OriginalElement = MainElement;
GetAllSubParams().ForEach(p => p.SetCurrentElementAsOriginalElement());
Serialize();
return base.Save(fileNameWithoutExtension, new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineOnAttributes = false
});
}
protected bool Load(string file, string speciesName)
{
if (Load(file))
{
SpeciesName = speciesName;
CreateColliders();
CreateLimbs();
CreateJoints();
return true;
}
return false;
}
public override bool Reset(bool forceReload = false)
{
if (forceReload)
{
return Load(FullPath, SpeciesName);
}
Deserialize(OriginalElement, recursive: true);
GetAllSubParams().ForEach(sp => sp.Reset());
return true;
}
protected void CreateColliders()
{
ColliderParams.Clear();
for (int i = 0; i < MainElement.Elements("collider").Count(); i++)
{
var element = MainElement.Elements("collider").ElementAt(i);
string name = i > 0 ? "Secondary Collider" : "Main Collider";
ColliderParams.Add(new ColliderParams(element, this, name));
}
}
protected void CreateLimbs()
{
Limbs.Clear();
foreach (var element in MainElement.Elements("limb"))
{
Limbs.Add(new LimbParams(element, this));
}
Limbs = Limbs.OrderBy(l => l.ID).ToList();
}
protected void CreateJoints()
{
Joints.Clear();
foreach (var element in MainElement.Elements("joint"))
{
Joints.Add(new JointParams(element, this));
}
}
protected bool Deserialize(XElement element = null, bool recursive = true)
{
if (base.Deserialize(element))
{
if (recursive)
{
GetAllSubParams().ForEach(p => p.Deserialize());
}
return true;
}
return false;
}
protected bool Serialize(XElement element = null, bool recursive = true)
{
if (base.Serialize(element))
{
if (recursive)
{
GetAllSubParams().ForEach(p => p.Serialize());
}
return true;
}
return false;
}
#region Memento
public override void CreateSnapshot()
{
Serialize();
var copy = new RagdollParams
{
IsLoaded = true,
doc = new XDocument(doc)
};
copy.CreateColliders();
copy.CreateLimbs();
copy.CreateJoints();
copy.Deserialize();
copy.Serialize();
memento.Store(copy);
}
public override void Undo() => RevertTo(memento.Undo() as RagdollParams);
public override void Redo() => RevertTo(memento.Redo() as RagdollParams);
private void RevertTo(RagdollParams source)
{
Deserialize(source.MainElement, recursive: false);
var sourceSubParams = source.GetAllSubParams().ToList();
var subParams = GetAllSubParams().ToList();
for (int i = 0; i < subParams.Count; i++)
{
subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false);
var subSubParams = subParams[i].SubParams;
for (int j = 0; j < subSubParams.Count; j++)
{
subSubParams[j].Deserialize(sourceSubParams[i].SubParams[j].Element, recursive: false);
// Since we cannot use recursion here, we have to go deeper manually, if necessary.
}
}
}
#endregion
#if CLIENT
public override void AddToEditor(ParamsEditor editor)
{
base.AddToEditor(editor);
var subParams = GetAllSubParams();
foreach (var subParam in subParams)
{
subParam.AddToEditor(editor);
//TODO: divider sprite
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, 10), editor.EditorBox.Content.RectTransform),
style: "ConnectionPanelWire");
}
}
#endif
}
class JointParams : RagdollSubParams
{
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
private string name;
[Serialize("", true), Editable]
public override string Name
{
get
{
if (string.IsNullOrWhiteSpace(name))
{
name = GenerateName();
}
return name;
}
set
{
name = value;
}
}
public override string GenerateName() => $"Joint {Limb1} - {Limb2}";
[Serialize(-1, true), Editable]
public int Limb1 { get; set; }
[Serialize(-1, true), Editable]
public int Limb2 { get; set; }
/// <summary>
/// Should be converted to sim units.
/// </summary>
[Serialize("1.0, 1.0", true), Editable]
public Vector2 Limb1Anchor { get; set; }
/// <summary>
/// Should be converted to sim units.
/// </summary>
[Serialize("1.0, 1.0", true), Editable]
public Vector2 Limb2Anchor { get; set; }
[Serialize(true, true), Editable]
public bool CanBeSevered { get; set; }
[Serialize(true, true), Editable]
public bool LimitEnabled { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0f, true), Editable]
public float UpperLimit { get; set; }
/// <summary>
/// In degrees.
/// </summary>
[Serialize(0f, true), Editable]
public float LowerLimit { get; set; }
[Serialize(0.25f, true), Editable]
public float Stiffness { get; set; }
}
class LimbParams : RagdollSubParams
{
public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
var spriteElement = element.Element("sprite");
if (spriteElement != null)
{
normalSpriteParams = new SpriteParams(spriteElement, ragdoll);
SubParams.Add(normalSpriteParams);
}
var damagedElement = element.Element("damagedsprite");
if (damagedElement != null)
{
damagedSpriteParams = new SpriteParams(damagedElement, ragdoll);
// Hide the damaged sprite params in the editor for now.
//SubParams.Add(damagedSpriteParams);
}
var deformElement = element.Element("deformablesprite");
if (deformElement != null)
{
deformSpriteParams = new SpriteParams(deformElement, ragdoll);
SubParams.Add(deformSpriteParams);
}
}
public readonly SpriteParams normalSpriteParams;
public readonly SpriteParams damagedSpriteParams;
public readonly SpriteParams deformSpriteParams;
private string name;
[Serialize("", true), Editable]
public override string Name
{
get
{
if (string.IsNullOrWhiteSpace(name))
{
name = GenerateName();
}
return name;
}
set
{
name = value;
}
}
public override string GenerateName() => $"Limb {ID}";
/// <summary>
/// Note that editing this in-game doesn't currently have any effect (unless the ragdoll is recreated). It should be visible, but readonly in the editor.
/// </summary>
[Serialize(-1, true), Editable]
public int ID { get; set; }
[Serialize(LimbType.None, true), Editable]
public LimbType Type { get; set; }
[Serialize(true, true), Editable]
public bool Flip { get; set; }
[Serialize(0, true), Editable]
public int HealthIndex { get; set; }
[Serialize(0f, true), Editable(ToolTip = "Higher values make AI characters prefer attacking this limb.")]
public float AttackPriority { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
[Serialize("0, 0", true), Editable(ToolTip = "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint.")]
public Vector2 StepOffset { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Radius { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Height { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10000)]
public float Mass { get; set; }
[Serialize(10f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float Density { get; set; }
[Serialize("0, 0", true), Editable(ToolTip = "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot.")]
public Vector2 PullPos { get; set; }
[Serialize(-1, true), Editable(ToolTip = "Only applicable if this limb is a foot. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter.")]
public int RefJoint { get; set; }
[Serialize(false, true), Editable]
public bool IgnoreCollisions { get; set; }
[Serialize("", true), Editable]
public string Notes { get; set; }
// Non-editable ->
[Serialize(0.3f, true)]
public float Friction { get; set; }
[Serialize(0.05f, true)]
public float Restitution { get; set; }
}
class SpriteParams : RagdollSubParams
{
public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
[Serialize("0, 0, 0, 0", true), Editable]
public Rectangle SourceRect { get; set; }
[Serialize("0.5, 0.5", true), Editable(DecimalCount = 2, ToolTip = "Relative to the collider.")]
public Vector2 Origin { get; set; }
[Serialize(0f, true), Editable(DecimalCount = 3)]
public float Depth { get; set; }
[Serialize("", true)]
public string Texture { get; set; }
}
class ColliderParams : RagdollSubParams
{
public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
{
Name = name;
}
private string name;
[Serialize("", true), Editable]
public override string Name
{
get
{
if (string.IsNullOrWhiteSpace(name))
{
name = GenerateName();
}
return name;
}
set
{
name = value;
}
}
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Radius { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Height { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
}
abstract class RagdollSubParams : ISerializableEntity
{
public virtual string Name { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public XElement Element { get; set; }
public XElement OriginalElement { get; protected set; }
public List<RagdollSubParams> SubParams { get; set; } = new List<RagdollSubParams>();
public RagdollParams Ragdoll { get; private set; }
public virtual string GenerateName() => Element.Name.ToString();
public RagdollSubParams(XElement element, RagdollParams ragdoll)
{
Element = element;
OriginalElement = new XElement(element);
Ragdoll = ragdoll;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public virtual bool Deserialize(XElement element = null, bool recursive = true)
{
element = element ?? Element;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (recursive)
{
SubParams.ForEach(sp => sp.Deserialize());
}
return SerializableProperties != null;
}
public virtual bool Serialize(XElement element = null, bool recursive = true)
{
element = element ?? Element;
SerializableProperty.SerializeProperties(this, element, true);
if (recursive)
{
SubParams.ForEach(sp => sp.Serialize());
}
return true;
}
public void SetCurrentElementAsOriginalElement()
{
OriginalElement = Element;
SubParams.ForEach(sp => sp.SetCurrentElementAsOriginalElement());
}
public void Reset()
{
Deserialize(OriginalElement, false);
SubParams.ForEach(sp => sp.Reset());
}
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true);
SubParams.ForEach(sp => sp.AddToEditor(editor));
}
#endif
}
}
File diff suppressed because it is too large Load Diff
@@ -1,109 +1,210 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
enum CauseOfDeath
{
Damage, Bloodloss, Pressure, Suffocation, Drowning, Burn, Husk, Disconnected
}
[Flags]
public enum DamageType
{
None = 0,
Blunt = 1,
Slash = 2,
Burn = 4,
Any = Blunt | Slash | Burn
}
{
public enum HitDetection
{
Distance,
Contact
}
public enum AttackContext
{
NotDefined,
Water,
Ground
}
public enum AttackTarget
{
Any,
Character,
Structure // Including hulls etc. Evaluated as anything but a character.
}
public enum AIBehaviorAfterAttack
{
FallBack,
PursueIfCanAttack,
Pursue
}
struct AttackResult
{
public readonly float Damage;
public readonly float Bleeding;
public readonly List<Affliction> Afflictions;
public readonly Limb HitLimb;
public readonly List<DamageModifier> AppliedDamageModifiers;
public AttackResult(float damage, float bleeding, List<DamageModifier> appliedDamageModifiers = null)
public AttackResult(List<Affliction> afflictions, Limb hitLimb, List<DamageModifier> appliedDamageModifiers = null)
{
this.Damage = damage;
this.Bleeding = bleeding;
HitLimb = hitLimb;
Afflictions = new List<Affliction>();
this.AppliedDamageModifiers = appliedDamageModifiers;
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
partial class Attack : ISerializableEntity
{
[Serialize(HitDetection.Distance, false)]
public readonly XElement SourceElement;
[Serialize(AttackContext.NotDefined, true), Editable]
public AttackContext Context { get; private set; }
[Serialize(AttackTarget.Any, true), Editable]
public AttackTarget TargetType { get; private set; }
[Serialize(HitDetection.Distance, true), Editable]
public HitDetection HitDetectionType { get; private set; }
[Serialize(0.0f, false)]
[Serialize(AIBehaviorAfterAttack.FallBack, true), Editable(ToolTip = "The preferred AI behavior after the attack.")]
public AIBehaviorAfterAttack AfterAttack { get; private set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f, ToolTip = "Min distance from the attack limb to the target before the AI tries to attack.")]
public float Range { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f, ToolTip = "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.")]
public float DamageRange { get; set; }
[Serialize(0.0f, false)]
[Serialize(0.25f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ToolTip = "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.")]
public float Duration { get; private set; }
[Serialize(DamageType.None, false)]
public DamageType DamageType { get; private set; }
[Serialize(5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "How long the AI waits between the attacks.")]
public float CoolDown { get; private set; } = 5;
[Serialize(0.0f, false)]
[Serialize(0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0.")]
public float SecondaryCoolDown { get; private set; } = 0;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float StructureDamage { get; private set; }
[Serialize(0.0f, false)]
public float Damage { get; private set; }
[Serialize(0.0f, false)]
public float BleedingDamage { get; private set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage { get; private set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
[Serialize(0.0f, false)]
public float Stun { get; private set; }
[Serialize(false, false)]
[Serialize(false, true), Editable]
public bool OnlyHumans { get; private set; }
[Serialize(0.0f, false)]
[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), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked.")]
public float Force { get; private set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb.")]
public float Torque { get; private set; }
[Serialize(0.0f, false)]
[Serialize(false, true), Editable]
public bool ApplyForcesOnlyOnce { get; private set; }
[Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "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).")]
public float TargetImpulse { get; private set; }
[Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")]
public Vector2 TargetImpulseWorld { get; private set; }
[Serialize(0.0f, true), Editable(-1000.0f, 1000.0f, ToolTip = "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).")]
public float TargetForce { get; private set; }
[Serialize(0.0f, false)]
[Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float SeverLimbsProbability { get; set; }
[Serialize(0.0f, false)]
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float StickChance { get; set; }
[Serialize(0.0f, true), 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> ApplyForceOnLimbs;
public readonly List<int> ForceOnLimbIndices = new List<int>();
public readonly List<Affliction> Afflictions = new List<Affliction>();
/// <summary>
/// Only affects ai decision making.
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
private readonly List<StatusEffect> statusEffects;
public float GetDamage(float deltaTime)
public void SetUser(Character user)
{
return (Duration == 0.0f) ? Damage : Damage * deltaTime;
if (statusEffects == null) { return; }
foreach (StatusEffect statusEffect in statusEffects)
{
statusEffect.SetUser(user);
}
}
public float GetBleedingDamage(float deltaTime)
public List<Affliction> GetMultipliedAfflictions(float multiplier)
{
return (Duration == 0.0f) ? BleedingDamage : BleedingDamage * deltaTime;
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions)
{
multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source));
}
return multipliedAfflictions;
}
public float GetStructureDamage(float deltaTime)
@@ -111,36 +212,48 @@ namespace Barotrauma
return (Duration == 0.0f) ? StructureDamage : StructureDamage * deltaTime;
}
public Attack(float damage, float structureDamage, float bleedingDamage, float range = 0.0f)
public float GetItemDamage(float deltaTime)
{
Range = range;
DamageRange = range;
this.Damage = damage;
this.StructureDamage = structureDamage;
this.BleedingDamage = bleedingDamage;
return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime;
}
public Attack(XElement element)
public float GetTotalDamage(bool includeStructureDamage = false)
{
SerializableProperty.DeserializeProperties(this, element);
DamageRange = element.GetAttributeFloat("damagerange", Range);
InitProjSpecific(element);
string limbIndicesStr = element.GetAttributeString("applyforceonlimbs", "");
if (!string.IsNullOrWhiteSpace(limbIndicesStr))
float totalDamage = includeStructureDamage ? StructureDamage : 0.0f;
foreach (Affliction affliction in Afflictions)
{
ApplyForceOnLimbs = new List<int>();
foreach (string limbIndexStr in limbIndicesStr.Split(','))
{
int limbIndex;
if (int.TryParse(limbIndexStr, out limbIndex))
{
ApplyForceOnLimbs.Add(limbIndex);
}
}
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));
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage));
if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage));
Range = range;
DamageRange = range;
StructureDamage = structureDamage;
}
public Attack(XElement element, string parentDebugName)
{
SourceElement = element;
Deserialize();
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\" />).");
}
DamageRange = element.GetAttributeFloat("damagerange", 0f);
InitProjSpecific(element);
foreach (XElement subElement in element.Elements())
{
@@ -151,13 +264,56 @@ namespace Barotrauma
{
statusEffects = new List<StatusEffect>();
}
statusEffects.Add(StatusEffect.Load(subElement));
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.Find(ap => ap.Name.ToLowerInvariant() == afflictionName);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
}
}
else
{
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
}
}
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
Afflictions.Add(afflictionPrefab.Instantiate(afflictionStrength));
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
{
Conditionals.Add(new PropertyConditional(attribute));
}
break;
}
}
}
partial void InitProjSpecific(XElement element);
public void Serialize()
{
if (SourceElement == null) { return; }
SerializableProperty.SerializeProperties(this, SourceElement, true);
}
public void Deserialize()
{
if (SourceElement == null) { return; }
SerializableProperties = SerializableProperty.DeserializeProperties(this, SourceElement);
}
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true)
{
@@ -167,6 +323,8 @@ namespace Barotrauma
if (targetCharacter != null && targetCharacter.ConfigPath != Character.HumanConfigFile) return new AttackResult();
}
SetUser(attacker);
DamageParticles(deltaTime, worldPosition);
var attackResult = target.AddDamage(attacker, worldPosition, this, deltaTime, playSound);
@@ -179,13 +337,24 @@ namespace Barotrauma
foreach (StatusEffect effect in statusEffects)
{
if (effect.Targets.HasFlag(StatusEffect.TargetType.This))
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (effect.Targets.HasFlag(StatusEffect.TargetType.Character) && target is Character)
if (target is Character)
{
effect.Apply(effectType, deltaTime, (Character)target, (Character)target);
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, (Character)target, (Character)target);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effectType, deltaTime, (Character)target, attackResult.HitLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
effect.Apply(effectType, deltaTime, (Character)target, ((Character)target).AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
}
}
@@ -201,6 +370,8 @@ namespace Barotrauma
if (targetLimb.character != null && targetLimb.character.ConfigPath != Character.HumanConfigFile) return new AttackResult();
}
SetUser(attacker);
DamageParticles(deltaTime, worldPosition);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb);
@@ -209,19 +380,88 @@ namespace Barotrauma
foreach (StatusEffect effect in statusEffects)
{
if (effect.Targets.HasFlag(StatusEffect.TargetType.This))
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (effect.Targets.HasFlag(StatusEffect.TargetType.Character))
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()
{
CoolDownTimer = CoolDown;
SecondaryCoolDownTimer = SecondaryCoolDown;
}
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 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
@@ -1,4 +1,8 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
@@ -7,10 +11,15 @@ using System.Xml.Linq;
namespace Barotrauma
{
public enum Gender { None, Male, Female };
public enum Gender { None, Male, Female };
public enum Race { None, White, Black, Asian };
partial class CharacterInfo
{
private static Dictionary<string, XDocument> cachedConfigs = new Dictionary<string, XDocument>();
private static ushort idCounter;
public string Name;
public string DisplayName
{
@@ -49,24 +58,107 @@ namespace Barotrauma
return disguiseName;
}
}
/// <summary>
/// Note: Can be null.
/// </summary>
public Character Character;
public readonly string File;
public Job Job;
private List<ushort> pickedItems;
public ushort? HullID = null;
private Vector2[] headSpriteRange;
private Gender gender;
public int Salary;
private int headSpriteId;
private Vector2 headSpriteRange;
private Sprite headSprite;
public Sprite HeadSprite
{
get
{
if (headSprite == null)
{
LoadHeadSprite();
}
return headSprite;
}
}
private Sprite portrait;
public Sprite Portrait
{
get
{
if (portrait == null)
{
LoadHeadSprite();
}
return portrait;
}
}
private Sprite clothingSprite;
public Sprite ClothingSprite
{
get
{
if (clothingSprite == null)
{
if (Job != null && Job.Prefab.ClothingElement != null)
{
clothingSprite = new Sprite(Job.Prefab.ClothingElement.Element("sprite"));
}
}
return clothingSprite;
}
}
private Sprite portraitBackground;
public Sprite PortraitBackground
{
get
{
if (portraitBackground == null)
{
var portraitBackgroundElement = SourceElement.Element("portraitbackground");
if (portraitBackgroundElement != null)
{
portraitBackground = new Sprite(portraitBackgroundElement.Element("sprite"));
}
}
return portraitBackground;
}
}
private List<WearableSprite> attachmentSprites;
public List<WearableSprite> AttachmentsSprites
{
get
{
if (attachmentSprites == null)
{
LoadAttachmentSprites();
}
return attachmentSprites;
}
}
public XElement SourceElement { get; set; }
public XElement HairElement { get; private set; }
public XElement BeardElement { get; private set; }
public XElement MoustacheElement { get; private set; }
public XElement FaceAttachment { get; private set; }
public int HairIndex { get; set; } = -1;
public int BeardIndex { get; set; } = -1;
public int MoustacheIndex { get; set; } = -1;
public int FaceAttachmentIndex { get; set; } = -1;
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
public readonly string ragdollFileName = string.Empty;
public bool StartItemsGiven;
@@ -74,19 +166,14 @@ namespace Barotrauma
public byte TeamID;
public List<ushort> PickedItemIDs
{
get { return pickedItems; }
}
public Sprite HeadSprite
{
get
{
if (headSprite == null) LoadHeadSprite();
return headSprite;
}
}
private NPCPersonalityTrait personalityTrait;
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
public XElement InventoryData;
public List<string> SpriteTags
{
@@ -94,6 +181,12 @@ namespace Barotrauma
private set;
}
public NPCPersonalityTrait PersonalityTrait
{
get { return personalityTrait; }
}
private int headSpriteId;
public int HeadSpriteId
{
get { return headSpriteId; }
@@ -102,15 +195,21 @@ namespace Barotrauma
int oldId = headSpriteId;
headSpriteId = value;
Vector2 spriteRange = headSpriteRange[gender == Gender.Male ? 0 : 1];
Vector2 spriteRange = headSpriteRange;
if (headSpriteId < (int)spriteRange.X) headSpriteId = (int)(spriteRange.Y);
if (headSpriteId > (int)spriteRange.Y) headSpriteId = (int)(spriteRange.X);
if (headSpriteId != oldId) headSprite = null;
if (headSpriteId != oldId)
{
headSprite = null;
attachmentSprites = null;
ResetHeadAttachments();
}
}
}
private Gender gender;
public Gender Gender
{
get { return gender; }
@@ -118,101 +217,151 @@ namespace Barotrauma
{
if (gender == value) return;
gender = value;
int genderIndex = (this.gender == Gender.Female) ? 1 : 0;
if (headSpriteRange[genderIndex] != Vector2.Zero)
{
HeadSpriteId = Rand.Range((int)headSpriteRange[genderIndex].X, (int)headSpriteRange[genderIndex].Y + 1);
}
else
{
HeadSpriteId = 0;
}
LoadHeadSprite();
}
}
public CharacterInfo(string file, string name = "", Gender gender = Gender.None, JobPrefab jobPrefab = null)
{
this.File = file;
headSpriteRange = new Vector2[2];
pickedItems = new List<ushort>();
SpriteTags = new List<string>();
//ID = -1;
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null) return;
if (doc.Root.GetAttributeBool("genders", false))
{
if (gender == Gender.None)
{
float femaleRatio = doc.Root.GetAttributeFloat("femaleratio", 0.5f);
this.gender = (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < femaleRatio) ? Gender.Female : Gender.Male;
}
else
{
this.gender = gender;
Gender = Gender.Male;
//SetRandomGender();
}
CalculateHeadSpriteRange();
ResetHeadAttachments();
headSprite = null;
attachmentSprites = null;
//SetRandomHead();
//LoadHeadSprite();
}
headSpriteRange[0] = doc.Root.GetAttributeVector2("headid", Vector2.Zero);
headSpriteRange[1] = headSpriteRange[0];
if (headSpriteRange[0] == Vector2.Zero)
{
headSpriteRange[0] = doc.Root.GetAttributeVector2("maleheadid", Vector2.Zero);
headSpriteRange[1] = doc.Root.GetAttributeVector2("femaleheadid", Vector2.Zero);
}
int genderIndex = (this.gender == Gender.Female) ? 1 : 0;
if (headSpriteRange[genderIndex] != Vector2.Zero)
{
HeadSpriteId = Rand.Range((int)headSpriteRange[genderIndex].X, (int)headSpriteRange[genderIndex].Y + 1);
}
this.Job = (jobPrefab == null) ? Job.Random() : new Job(jobPrefab);
if (!string.IsNullOrEmpty(name))
{
this.Name = name;
return;
}
name = "";
if (doc.Root.Element("name") != null)
{
string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", "");
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "f" : "");
this.Name = ToolBox.GetRandomLine(firstNamePath);
}
string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "f" : "");
if (this.Name != "") this.Name += " ";
this.Name += ToolBox.GetRandomLine(lastNamePath);
}
}
Salary = CalculateSalary();
}
private void LoadHeadSprite()
private Race race;
public Race Race
{
XDocument doc = XMLExtensions.TryLoadXml(File);
if (doc == null) return;
get { return race; }
set
{
if (race == value) { return; }
race = value;
if (race == Race.None)
{
race = Race.White;
//SetRandomRace();
}
CalculateHeadSpriteRange();
ResetHeadAttachments();
headSprite = null;
attachmentSprites = null;
//SetRandomHead();
//LoadHeadSprite();
}
}
XElement ragdollElement = doc.Root.Element("ragdoll");
foreach (XElement limbElement in ragdollElement.Elements())
private RagdollParams ragdoll;
public RagdollParams Ragdoll
{
get
{
if (ragdoll == null)
{
string speciesName = SourceElement.GetAttributeString("name", string.Empty);
bool isHumanoid = SourceElement.GetAttributeBool("humanoid", false);
ragdoll = isHumanoid
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
: RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
}
return ragdoll;
}
set { ragdoll = value; }
}
// Used for creating the data
public CharacterInfo(string file, string name = "", Gender gender = Gender.None, JobPrefab jobPrefab = null, string ragdollFileName = null)
{
ID = idCounter;
idCounter++;
File = file;
SpriteTags = new List<string>();
XDocument doc = GetConfig(file);
SourceElement = doc.Root;
if (doc.Root.GetAttributeBool("genders", false))
{
this.gender = gender == Gender.None ? SetRandomGender() : gender;
}
Enum.TryParse(doc.Root.GetAttributeString("race", "None"), true, out race);
if (race == Race.None)
{
SetRandomRace();
}
CalculateHeadSpriteRange();
SetRandomHeadID();
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab);
if (!string.IsNullOrEmpty(name))
{
Name = name;
}
else
{
name = "";
if (doc.Root.Element("name") != null)
{
string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", "");
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "female" : "male");
Name = ToolBox.GetRandomLine(firstNamePath);
}
string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "female" : "male");
if (Name != "") Name += " ";
Name += ToolBox.GetRandomLine(lastNamePath);
}
}
}
personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId);
Salary = CalculateSalary();
if (ragdollFileName != null)
{
this.ragdollFileName = ragdollFileName;
}
LoadHeadAttachments();
}
// Used for loading the data
public CharacterInfo(XElement element)
{
ID = idCounter;
idCounter++;
Name = element.GetAttributeString("name", "unnamed");
string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant();
gender = (genderStr == "male") ? Gender.Male : Gender.Female;
Enum.TryParse(element.GetAttributeString("race", "white"), true, out race);
File = element.GetAttributeString("file", "");
SourceElement = GetConfig(File).Root;
Salary = element.GetAttributeInt("salary", 1000);
headSpriteId = element.GetAttributeInt("headspriteid", 1);
HairIndex = element.GetAttributeInt("hairindex", -1);
BeardIndex = element.GetAttributeInt("beardindex", -1);
MoustacheIndex = element.GetAttributeInt("moustacheindex", -1);
FaceAttachmentIndex = element.GetAttributeInt("faceattachmentindex", -1);
StartItemsGiven = element.GetAttributeBool("startitemsgiven", false);
string personalityName = element.GetAttributeString("personality", "");
ragdollFileName = element.GetAttributeString("ragdoll", string.Empty);
if (!string.IsNullOrEmpty(personalityName))
{
personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName);
}
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
Job = new Job(subElement);
break;
}
LoadHeadAttachments();
}
public void LoadHeadSprite()
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue;
@@ -220,21 +369,25 @@ namespace Barotrauma
string spritePath = spriteElement.Attribute("texture").Value;
spritePath = spritePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "f" : "");
spritePath = spritePath.Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male");
spritePath = spritePath.Replace("[RACE]", race.ToString().ToLowerInvariant());
spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString());
string fileName = Path.GetFileNameWithoutExtension(spritePath);
//go through the files in the directory to find a matching sprite
var files = Directory.GetFiles(Path.GetDirectoryName(spritePath)).ToList();
foreach (string file in files)
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
if (fileWithoutTags != fileName) continue;
headSprite = new Sprite(spriteElement, "", file);
portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).ToList();
@@ -249,52 +402,215 @@ namespace Barotrauma
break;
}
}
public void UpdateCharacterItems()
public Gender SetRandomGender() => gender = (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
public Race SetRandomRace() => race = new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server);
public int SetRandomHead() => HeadSpriteId = SetRandomHeadID();
private XDocument GetConfig(string file)
{
pickedItems.Clear();
foreach (Item item in Character.Inventory.Items)
if (!cachedConfigs.TryGetValue(file, out XDocument doc))
{
pickedItems.Add(item == null ? (ushort)0 : item.ID);
doc = XMLExtensions.TryLoadXml(file);
if (doc == null) { return null; }
cachedConfigs.Add(file, doc);
}
return doc;
}
private int SetRandomHeadID()
{
if (headSpriteRange != Vector2.Zero)
{
headSpriteId = Rand.Range((int)headSpriteRange.X, (int)headSpriteRange.Y + 1, Rand.RandSync.Server);
}
else
{
headSpriteId = 0;
}
return headSpriteId;
}
private List<XElement> hairs;
private List<XElement> beards;
private List<XElement> moustaches;
private List<XElement> faceAttachments;
private IEnumerable<XElement> wearables;
private IEnumerable<XElement> Wearables
{
get
{
if (wearables == null)
{
var attachments = SourceElement.Element("HeadAttachments");
if (attachments != null)
{
wearables = attachments.Elements("Wearable");
}
}
return wearables;
}
}
public CharacterInfo(XElement element)
private IEnumerable<XElement> FilterElementsByGenderAndRace(IEnumerable<XElement> elements)
{
Name = element.GetAttributeString("name", "unnamed");
if (elements == null) { return elements; }
return elements.Where(w =>
Enum.TryParse(w.GetAttributeString("gender", "male"), true, out Gender g) && g == gender &&
Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == race);
}
string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant();
gender = (genderStr == "m") ? Gender.Male : Gender.Female;
File = element.GetAttributeString("file", "");
Salary = element.GetAttributeInt("salary", 1000);
headSpriteId = element.GetAttributeInt("headspriteid", 1);
StartItemsGiven = element.GetAttributeBool("startitemsgiven", false);
int hullId = element.GetAttributeInt("hull", -1);
if (hullId > 0 && hullId <= ushort.MaxValue) this.HullID = (ushort)hullId;
pickedItems = new List<ushort>();
string pickedItemString = element.GetAttributeString("items", "");
if (!string.IsNullOrEmpty(pickedItemString))
private void CalculateHeadSpriteRange()
{
if (SourceElement == null) { return; }
headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero);
if (headSpriteRange == Vector2.Zero)
{
string[] itemIds = pickedItemString.Split(',');
foreach (string s in itemIds)
// If range is defined, we use it as it is
// Else we calculate the range from the wearables.
var wearables = FilterElementsByGenderAndRace(Wearables);
if (wearables == null)
{
pickedItems.Add((ushort)int.Parse(s));
headSpriteRange = Vector2.Zero;
return;
}
if (wearables.None())
{
DebugConsole.ThrowError($"[CharacterInfo] No headidrange defined and no wearables matching the gender {gender} and the race {race} could be found. Total wearables found: {Wearables.Count()}.");
return;
}
else
{
// Ignore head ids that are less than 1, because they are not supported.
var ids = wearables.Select(w => w.GetAttributeInt("headid", -1)).Where(id => id > 0);
if (ids.None())
{
DebugConsole.ThrowError($"[CharacterInfo] Wearables with matching gender and race were found but none with a valid headid! Total wearables found: {Wearables.Count()}.");
return;
}
ids = ids.OrderBy(id => id);
headSpriteRange = new Vector2(ids.First(), ids.Last());
}
}
}
foreach (XElement subElement in element.Elements())
/// <summary>
/// Loads only the elements according to the indices, not the sprites.
/// </summary>
public void LoadHeadAttachments()
{
if (Wearables != null)
{
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
if (hairs == null)
{
hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair);
}
if (beards == null)
{
beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Beard), WearableType.Beard);
}
if (moustaches == null)
{
moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Moustache), WearableType.Moustache);
}
if (faceAttachments == null)
{
faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.FaceAttachment), WearableType.FaceAttachment);
}
Job = new Job(subElement);
break;
if (IsValidIndex(HairIndex, hairs))
{
HairElement = hairs[HairIndex];
}
else
{
HairElement = GetRandomElement(hairs);
HairIndex = hairs.IndexOf(HairElement);
}
if (IsValidIndex(BeardIndex, beards))
{
BeardElement = beards[BeardIndex];
}
else
{
BeardElement = GetRandomElement(beards);
BeardIndex = beards.IndexOf(BeardElement);
}
if (IsValidIndex(MoustacheIndex, moustaches))
{
MoustacheElement = moustaches[MoustacheIndex];
}
else
{
MoustacheElement = GetRandomElement(moustaches);
MoustacheIndex = moustaches.IndexOf(MoustacheElement);
}
if (IsValidIndex(FaceAttachmentIndex, faceAttachments))
{
FaceAttachment = faceAttachments[FaceAttachmentIndex];
}
else
{
FaceAttachment = GetRandomElement(faceAttachments);
FaceAttachmentIndex = faceAttachments.IndexOf(FaceAttachment);
}
List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type)
{
// Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
var emptyElement = new XElement("EmptyWearable", type.ToString());
var list = new List<XElement>() { emptyElement };
list.AddRange(elements);
return list;
}
XElement GetRandomElement(IEnumerable<XElement> elements)
{
var filtered = elements.Where(e => IsWearableAllowed(e)).ToList();
if (filtered.Count == 0) { return null; }
var weights = GetWeights(filtered).ToList();
var element = ToolBox.SelectWeightedRandom(filtered, weights, Rand.RandSync.Server);
return element == null || element.Name == "Empty" ? null : element;
}
IEnumerable<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> elements, WearableType targetType)
{
return elements.Where(e =>
{
if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; }
int headId = e.GetAttributeInt("headid", -1);
// if the head id is less than 1, the id is not valid and the condition is ignored.
return headId < 1 || headId == headSpriteId;
});
}
bool IsWearableAllowed(XElement element)
{
string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty);
return IsAllowed(HairElement, spriteName) && IsAllowed(BeardElement, spriteName) && IsAllowed(MoustacheElement, spriteName) && IsAllowed(FaceAttachment, spriteName);
}
bool IsAllowed(XElement element, string spriteName)
{
if (element != null)
{
var disallowed = element.GetAttributeStringArray("disallow", new string[0]);
if (disallowed.Any(s => spriteName.Contains(s)))
{
return false;
}
}
return true;
}
bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
}
}
partial void LoadAttachmentSprites();
private int CalculateSalary()
{
if (Name == null || Job == null) return 0;
@@ -303,12 +619,49 @@ namespace Barotrauma
foreach (Skill skill in Job.Skills)
{
salary += skill.Level * 10;
salary += (int)skill.Level * 50;
}
return salary;
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos)
{
if (Job == null || GameMain.Client != null) return;
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase);
float newLevel = Job.GetSkillLevel(skillIdentifier);
OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos);
if (GameMain.Server != null && (int)newLevel != (int)prevLevel)
{
GameMain.Server.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
}
}
public void SetSkillLevel(string skillIdentifier, float level, Vector2 worldPos)
{
if (Job == null) return;
var skill = Job.Skills.Find(s => s.Identifier == skillIdentifier);
if (skill == null)
{
Job.Skills.Add(new Skill(skillIdentifier, level));
OnSkillChanged(skillIdentifier, 0.0f, skill.Level, worldPos);
}
else
{
float prevLevel = skill.Level;
skill.Level = level;
OnSkillChanged(skillIdentifier, prevLevel, skill.Level, worldPos);
}
}
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos);
public virtual XElement Save(XElement parentElement)
{
XElement charElement = new XElement("Character");
@@ -316,44 +669,204 @@ namespace Barotrauma
charElement.Add(
new XAttribute("name", Name),
new XAttribute("file", File),
new XAttribute("gender", gender == Gender.Male ? "m" : "f"),
new XAttribute("gender", gender == Gender.Male ? "male" : "female"),
new XAttribute("race", race.ToString()),
new XAttribute("salary", Salary),
new XAttribute("headspriteid", HeadSpriteId),
new XAttribute("startitemsgiven", StartItemsGiven));
new XAttribute("hairindex", HairIndex),
new XAttribute("beardindex", BeardIndex),
new XAttribute("moustacheindex", MoustacheIndex),
new XAttribute("faceattachmentindex", FaceAttachmentIndex),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("ragdoll", ragdollFileName),
new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name));
// TODO: animations?
if (Character != null)
{
if (Character.Inventory != null)
{
UpdateCharacterItems();
}
if (Character.AnimController.CurrentHull != null)
{
HullID = Character.AnimController.CurrentHull.ID;
charElement.Add(new XAttribute("hull", Character.AnimController.CurrentHull.ID));
}
}
if (pickedItems.Count > 0)
{
charElement.Add(new XAttribute("items", string.Join(",", pickedItems)));
}
Job.Save(charElement);
parentElement.Add(charElement);
return charElement;
}
public void SpawnInventoryItems(Inventory inventory, XElement itemData)
{
SpawnInventoryItemsRecursive(inventory, itemData);
}
private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element)
{
foreach (XElement itemElement in element.Elements())
{
var newItem = Item.Load(itemElement, inventory.Owner.Submarine);
int slotIndex = itemElement.GetAttributeInt("i", 0);
if (newItem == null) continue;
Entity.Spawner.CreateNetworkEvent(newItem, false);
inventory.TryPutItem(newItem, slotIndex, false, false, null);
int itemContainerIndex = 0;
var itemContainers = newItem.GetComponents<ItemContainer>().ToList();
foreach (XElement childInvElement in itemElement.Elements())
{
if (itemContainerIndex >= itemContainers.Count) break;
if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue;
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement);
itemContainerIndex++;
}
}
}
public void ServerWrite(NetBuffer msg)
{
msg.Write(ID);
msg.Write(Name);
msg.Write(Gender == Gender.Female);
msg.Write((byte)Race);
msg.Write((byte)HeadSpriteId);
msg.Write((byte)HairIndex);
msg.Write((byte)BeardIndex);
msg.Write((byte)MoustacheIndex);
msg.Write((byte)FaceAttachmentIndex);
msg.Write(ragdollFileName);
if (Job != null)
{
msg.Write(Job.Prefab.Identifier);
msg.Write((byte)Job.Skills.Count);
foreach (Skill skill in Job.Skills)
{
msg.Write(skill.Identifier);
msg.Write(skill.Level);
}
}
else
{
msg.Write("");
}
// TODO: animations
}
public static CharacterInfo ClientRead(string configPath, NetBuffer inc)
{
ushort infoID = inc.ReadUInt16();
string newName = inc.ReadString();
bool isFemale = inc.ReadBoolean();
int race = inc.ReadByte();
int headSpriteID = inc.ReadByte();
int hairIndex = inc.ReadByte();
int beardIndex = inc.ReadByte();
int moustacheIndex = inc.ReadByte();
int faceAttachmentIndex = inc.ReadByte();
string ragdollFile = inc.ReadString();
string jobIdentifier = inc.ReadString();
JobPrefab jobPrefab = null;
Dictionary<string, float> skillLevels = new Dictionary<string, float>();
if (!string.IsNullOrEmpty(jobIdentifier))
{
jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier);
int skillCount = inc.ReadByte();
for (int i = 0; i < skillCount; i++)
{
string skillIdentifier = inc.ReadString();
float skillLevel = inc.ReadSingle();
skillLevels.Add(skillIdentifier, skillLevel);
}
}
// TODO: animations
CharacterInfo ch = new CharacterInfo(configPath, newName, isFemale ? Gender.Female : Gender.Male, jobPrefab, ragdollFile)
{
ID = infoID,
race = (Race)race,
headSpriteId = headSpriteID,
HairIndex = hairIndex,
BeardIndex = beardIndex,
MoustacheIndex = moustacheIndex,
FaceAttachmentIndex = faceAttachmentIndex
};
ch.CalculateHeadSpriteRange();
ch.ReloadHeadAttachments();
System.Diagnostics.Debug.Assert(skillLevels.Count == ch.Job.Skills.Count);
if (ch.Job != null)
{
foreach (KeyValuePair<string, float> skill in skillLevels)
{
Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key);
if (matchingSkill == null)
{
DebugConsole.ThrowError("Skill \"" + skill.Key + "\" not found in character \"" + newName + "\"");
continue;
}
matchingSkill.Level = skill.Value;
}
}
return ch;
}
public void ReloadHeadAttachments()
{
ResetLoadedAttachments();
LoadHeadAttachments();
}
public void ResetHeadAttachments()
{
ResetAttachmentIndices();
ResetLoadedAttachments();
}
private void ResetAttachmentIndices()
{
HairIndex = -1;
BeardIndex = -1;
MoustacheIndex = -1;
FaceAttachmentIndex = -1;
}
private void ResetLoadedAttachments()
{
hairs = null;
beards = null;
moustaches = null;
faceAttachments = null;
}
public void Remove()
{
Character = null;
//if (headSprite != null)
//{
// headSprite.Remove();
// headSprite = null;
//}
if (headSprite != null)
{
headSprite.Remove();
headSprite = null;
}
if (portrait != null)
{
portrait.Remove();
portrait = null;
}
if (portraitBackground != null)
{
portraitBackground.Remove();
portraitBackground = null;
}
if (attachmentSprites != null)
{
attachmentSprites.ForEach(a => a.Sprite.Remove());
attachmentSprites = null;
}
}
}
}
@@ -53,8 +53,10 @@ namespace Barotrauma
Aim = 0x200,
Attack = 0x400,
Ragdoll = 0x800,
Health = 0x1000,
Grab = 0x2000,
MaxVal = 0xFFF
MaxVal = 0x3FFF
}
private InputNetFlags dequeuedInput = 0;
private InputNetFlags prevDequeuedInput = 0;
@@ -108,13 +110,14 @@ namespace Barotrauma
LastNetworkUpdateID = 0;
LastProcessedID = 0;
}
private void UpdateNetInput()
{
if (this != Character.Controlled)
if (this != Controlled)
{
if (GameMain.Client != null)
{
#if CLIENT
//freeze AI characters if more than 1 seconds have passed since last update from the server
if (lastRecvPositionUpdateTime < NetTime.Now - 1.0f)
{
@@ -127,6 +130,7 @@ namespace Barotrauma
return;
}
}
#endif
}
else if (GameMain.Server != null && (!(this is AICharacter) || IsRemotePlayer))
{
@@ -153,12 +157,13 @@ namespace Barotrauma
dequeuedInput = memInput[memInput.Count - 1].states;
double aimAngle = ((double)memInput[memInput.Count - 1].intAim / 65535.0) * 2.0 * Math.PI;
cursorPosition = (ViewTarget == null ? AnimController.AimSourcePos : ViewTarget.Position)
+ new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
//reset focus when attempting to use/select something
if (memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Use) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select))
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Health) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Grab))
{
focusedItem = null;
focusedCharacter = null;
@@ -205,7 +210,7 @@ namespace Barotrauma
AnimController.Collider.Rotation,
LastNetworkUpdateID,
AnimController.TargetDir,
SelectedCharacter == null ? (Entity)selectedConstruction : (Entity)SelectedCharacter,
SelectedCharacter == null ? (Entity)SelectedConstruction : (Entity)SelectedCharacter,
AnimController.Anim);
memLocalState.Add(posInfo);
@@ -218,6 +223,8 @@ namespace Barotrauma
if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run;
if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch;
if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered
if (IsKeyHit(InputType.Health)) newInput |= InputNetFlags.Health;
if (IsKeyHit(InputType.Grab)) newInput |= InputNetFlags.Grab;
if (IsKeyDown(InputType.Use)) newInput |= InputNetFlags.Use;
if (IsKeyDown(InputType.Aim)) newInput |= InputNetFlags.Aim;
if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack;
@@ -225,14 +232,16 @@ namespace Barotrauma
if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft;
Vector2 relativeCursorPos = cursorPosition - (ViewTarget == null ? AnimController.AimSourcePos : ViewTarget.Position);
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
relativeCursorPos.Normalize();
UInt16 intAngle = (UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI));
NetInputMem newMem = new NetInputMem();
newMem.states = newInput;
newMem.intAim = intAngle;
if (focusedItem != null)
NetInputMem newMem = new NetInputMem
{
states = newInput,
intAim = intAngle
};
if (focusedItem != null && (!newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health)))
{
newMem.interact = focusedItem.ID;
}
@@ -292,13 +301,26 @@ namespace Barotrauma
UInt16 newAim = 0;
UInt16 newInteract = 0;
if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft)
{
c.KickAFKTimer = 0.0f;
}
else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft))
{
//character changed the direction they're facing
c.KickAFKTimer = 0.0f;
}
if (newInput.HasFlag(InputNetFlags.Aim))
{
newAim = msg.ReadUInt16();
}
if (newInput.HasFlag(InputNetFlags.Select) || newInput.HasFlag(InputNetFlags.Use))
if (newInput.HasFlag(InputNetFlags.Select) ||
newInput.HasFlag(InputNetFlags.Use) ||
newInput.HasFlag(InputNetFlags.Health) ||
newInput.HasFlag(InputNetFlags.Grab))
{
newInteract = msg.ReadUInt16();
newInteract = msg.ReadUInt16();
}
//if (AllowInput)
@@ -334,7 +356,7 @@ namespace Barotrauma
switch (eventType)
{
case 0:
inventory.ServerRead(type, msg, c);
Inventory.ServerRead(type, msg, c);
break;
case 1:
bool doingCPR = msg.ReadBoolean();
@@ -359,20 +381,10 @@ namespace Barotrauma
if (IsUnconscious)
{
Kill(lastAttackCauseOfDeath);
var causeOfDeath = CharacterHealth.GetCauseOfDeath();
Kill(causeOfDeath.First, causeOfDeath.Second);
}
break;
case 3:
LimbType grabLimb = (LimbType)msg.ReadByte();
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
AnimController.GrabLimb = grabLimb;
break;
}
break;
}
@@ -388,18 +400,34 @@ namespace Barotrauma
switch ((NetEntityEvent.Type)extraData[0])
{
case NetEntityEvent.Type.InventoryState:
msg.WriteRangedInteger(0, 2, 0);
inventory.ClientWrite(msg, extraData);
msg.WriteRangedInteger(0, 3, 0);
Inventory.ClientWrite(msg, extraData);
break;
case NetEntityEvent.Type.Control:
msg.WriteRangedInteger(0, 2, 1);
msg.WriteRangedInteger(0, 3, 1);
Client owner = ((Client)extraData[1]);
msg.Write(owner == null ? (byte)0 : owner.ID);
break;
case NetEntityEvent.Type.Status:
msg.WriteRangedInteger(0, 2, 2);
msg.WriteRangedInteger(0, 3, 2);
WriteStatus(msg);
break;
case NetEntityEvent.Type.UpdateSkills:
msg.WriteRangedInteger(0, 3, 3);
if (Info?.Job == null)
{
msg.Write((byte)0);
}
else
{
msg.Write((byte)Info.Job.Skills.Count);
foreach (Skill skill in Info.Job.Skills)
{
msg.Write(skill.Identifier);
msg.Write(skill.Level);
}
}
break;
default:
DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")");
break;
@@ -454,16 +482,17 @@ namespace Barotrauma
if (AnimController is HumanoidAnimController)
{
tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching);
tempBuffer.Write((byte)AnimController.GrabLimb);
}
bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null);
AttackContext currentContext = GetAttackContext();
// TODO: do we need to filter the attack target here?
bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null && l.attack.IsValidContext(currentContext));
tempBuffer.Write(hasAttackLimb);
if (hasAttackLimb) tempBuffer.Write(attack);
if (aiming)
{
Vector2 relativeCursorPos = cursorPosition - (ViewTarget == null ? AnimController.AimSourcePos : ViewTarget.Position);
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI)));
}
tempBuffer.Write(IsRagdolled);
@@ -471,10 +500,10 @@ namespace Barotrauma
tempBuffer.Write(AnimController.TargetDir == Direction.Right);
}
if (SelectedCharacter != null || selectedConstruction != null)
if (SelectedCharacter != null || SelectedConstruction != null)
{
tempBuffer.Write(true);
tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : selectedConstruction.ID);
tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : SelectedConstruction.ID);
if (SelectedCharacter != null)
{
tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR);
@@ -505,11 +534,15 @@ namespace Barotrauma
DebugConsole.ThrowError("Client attempted to write character status to a networked message");
return;
}
msg.Write(isDead);
if (isDead)
msg.Write(IsDead);
if (IsDead)
{
msg.Write((byte)causeOfDeath);
msg.WriteRangedInteger(0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1, (int)CauseOfDeath.Type);
if (CauseOfDeath.Type == CauseOfDeathType.Affliction)
{
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(CauseOfDeath.Affliction));
}
if (AnimController?.LimbJoints == null)
{
@@ -535,29 +568,8 @@ namespace Barotrauma
}
else
{
msg.WriteRangedSingle(health, minHealth, maxHealth, 8);
msg.Write(oxygen < 100.0f);
if (oxygen < 100.0f)
{
msg.WriteRangedSingle(oxygen, -100.0f, 100.0f, 8);
}
msg.Write(bleeding > 0.0f);
if (bleeding > 0.0f)
{
msg.WriteRangedSingle(bleeding, 0.0f, 5.0f, 8);
}
msg.Write(Stun > 0.0f);
if (Stun > 0.0f)
{
msg.WriteRangedSingle(MathHelper.Clamp(Stun, 0.0f, MaxStun), 0.0f, MaxStun, 8);
}
CharacterHealth.ServerWrite(msg);
msg.Write(IsRagdolled);
msg.Write(HuskInfectionState > 0.0f);
}
}
@@ -568,6 +580,7 @@ namespace Barotrauma
msg.Write(Info == null);
msg.Write(ID);
msg.Write(ConfigPath);
msg.Write(seed);
msg.Write(WorldPosition.X);
msg.Write(WorldPosition.Y);
@@ -576,7 +589,7 @@ namespace Barotrauma
//character with no characterinfo (e.g. some monster)
if (Info == null) return;
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (ownerClient != null)
{
@@ -593,27 +606,9 @@ namespace Barotrauma
msg.Write(false);
}
msg.Write(Info.Name);
msg.Write(TeamID);
msg.Write(this is AICharacter);
msg.Write(Info.Gender == Gender.Female);
msg.Write((byte)Info.HeadSpriteId);
if (info.Job != null)
{
msg.Write(Info.Job.Name);
msg.Write((byte)info.Job.Skills.Count);
foreach (Skill skill in info.Job.Skills)
{
msg.Write(skill.Name);
msg.WriteRangedInteger(0, 100, MathHelper.Clamp(skill.Level, 0, 100));
}
}
else
{
msg.Write("");
}
}
info.ServerWrite(msg);
}
}
}
@@ -1,66 +0,0 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma
{
class DamageModifier
{
[Serialize(DamageType.None, false)]
public DamageType DamageType
{
get;
private set;
}
[Serialize(1.0f, false)]
public float DamageMultiplier
{
get;
private set;
}
[Serialize(1.0f, false)]
public float BleedingMultiplier
{
get;
private set;
}
[Serialize("0.0,360", false)]
public Vector2 ArmorSector
{
get;
private set;
}
[Serialize(true, false)]
public bool IsArmor
{
get;
private set;
}
[Serialize(true, false)]
public bool DeflectProjectiles
{
get;
private set;
}
#if CLIENT
[Serialize("", false)]
public string DamageSound
{
get;
private set;
}
#endif
public DamageModifier(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
ArmorSector = new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
}
}
}
@@ -0,0 +1,141 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class Affliction
{
public readonly AfflictionPrefab Prefab;
public float Strength;
public float DamagePerSecond;
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
/// <summary>
/// Which character gave this affliction
/// </summary>
public Character Source;
public Affliction(AfflictionPrefab prefab, float strength)
{
Prefab = prefab;
Strength = strength;
}
public Affliction CreateMultiplied(float multiplier)
{
return Prefab.Instantiate(Strength * multiplier, Source);
}
public override string ToString()
{
return "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 virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return;
Strength += currentEffect.StrengthChange * deltaTime;
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
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());
}
}
}
}
}
@@ -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;
}
}
}
@@ -1,67 +1,88 @@
using Microsoft.Xna.Framework;
#if CLIENT
using Microsoft.Xna.Framework;
#endif
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class HuskInfection
partial class AfflictionHusk : Affliction
{
public enum InfectionState
{
Dormant, Transition, Active
}
const float IncubationDuration = 300.0f;
private bool subscribedToDeathEvent;
private InfectionState state;
private Limb huskAppendage;
private float incubationTimer;
public float IncubationTimer
{
get { return incubationTimer; }
set
{
incubationTimer = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
public InfectionState State
{
get { return state; }
}
public bool CanSpeak
public AfflictionHusk(AfflictionPrefab prefab, float strength) :
base(prefab, strength)
{
get { return IncubationTimer < 0.5f; }
}
public HuskInfection(Character character)
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
character.OnDeath += CharacterDead;
}
float prevStrength = Strength;
base.Update(characterHealth, targetLimb, deltaTime);
public void Update(float deltaTime, Character character)
{
float prevTimer = IncubationTimer;
UpdateProjSpecific(prevTimer,character);
if (IncubationTimer < 0.5f)
if (!subscribedToDeathEvent)
{
UpdateDormantState(deltaTime, character);
characterHealth.Character.OnDeath += CharacterDead;
subscribedToDeathEvent = true;
}
else if (IncubationTimer < 1.0f)
if (characterHealth.Character == Character.Controlled) UpdateMessages(prevStrength, characterHealth.Character);
if (Strength < Prefab.MaxStrength * 0.5f)
{
UpdateTransitionState(deltaTime, character);
UpdateDormantState(deltaTime, characterHealth.Character);
}
else if (Strength < Prefab.MaxStrength)
{
characterHealth.Character.SpeechImpediment = 100.0f;
UpdateTransitionState(deltaTime, characterHealth.Character);
}
else
{
UpdateActiveState(deltaTime, character);
characterHealth.Character.SpeechImpediment = 100.0f;
UpdateActiveState(deltaTime, characterHealth.Character);
}
}
partial void UpdateProjSpecific(float prevTimer, Character character);
private void UpdateMessages(float prevStrength, Character character)
{
#if CLIENT
if (Strength < Prefab.MaxStrength * 0.5f)
{
if (prevStrength % 10.0f > 0.05f && Strength % 10.0f < 0.05f)
{
GUI.AddMessage(TextManager.Get("HuskDormant"), Color.Red);
}
}
else if (Strength < Prefab.MaxStrength)
{
if (state == InfectionState.Dormant && Character.Controlled == character)
{
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), Color.Red);
}
}
else if (state != InfectionState.Active && Character.Controlled == character)
{
GUI.AddMessage(TextManager.Get("HuskActivate").Replace("[Attack]", GameMain.Config.KeyBind(InputType.Attack).ToString()),
Color.Red);
}
#endif
}
private void UpdateDormantState(float deltaTime, Character character)
{
@@ -69,10 +90,8 @@ namespace Barotrauma
{
DeactivateHusk(character);
}
float prevTimer = IncubationTimer;
state = InfectionState.Dormant;
IncubationTimer += deltaTime / IncubationDuration;
}
private void UpdateTransitionState(float deltaTime, Character character)
@@ -82,7 +101,6 @@ namespace Barotrauma
DeactivateHusk(character);
}
IncubationTimer += deltaTime / IncubationDuration;
state = InfectionState.Transition;
}
@@ -94,7 +112,14 @@ namespace Barotrauma
state = InfectionState.Active;
}
character.AddDamage(CauseOfDeath.Husk, 0.5f * deltaTime, null);
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);
}
}
private void ActivateHusk(Character character)
@@ -108,7 +133,7 @@ namespace Barotrauma
//husk appendage already created, don't do anything
if (huskAppendage != null) return;
XDocument doc = XMLExtensions.TryLoadXml(Path.Combine("Content", "Characters", "Human", "huskappendage.xml"));
XDocument doc = XMLExtensions.TryLoadXml(Path.Combine("Content", "Characters", "Human", "Huskappendage.xml"));
if (doc == null || doc.Root == null) return;
var limbElement = doc.Root.Element("limb");
@@ -132,11 +157,11 @@ namespace Barotrauma
}
var torso = character.AnimController.GetLimb(LimbType.Torso);
huskAppendage = new Limb(character, limbElement);
huskAppendage = new Limb(character.AnimController, character, new LimbParams(limbElement, character.AnimController.RagdollParams));
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(torso.SimPosition, torso.Rotation);
character.AnimController.AddLimb(huskAppendage);
character.AnimController.AddJoint(jointElement);
}
@@ -158,13 +183,14 @@ namespace Barotrauma
public void Remove(Character character)
{
DeactivateHusk(character);
if (character != null) character.OnDeath -= CharacterDead;
subscribedToDeathEvent = false;
}
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.Client != null) return;
if (GameMain.Client != null) { 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)
@@ -174,7 +200,7 @@ namespace Barotrauma
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));
}
@@ -184,8 +210,8 @@ namespace Barotrauma
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
var characterFiles = GameMain.SelectedPackage.GetFilesOfType(ContentType.Character);
var configFile = characterFiles.Find(f => Path.GetFileNameWithoutExtension(f) == "humanhusk");
var characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character);
var configFile = characterFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f) == "humanhusk");
if (string.IsNullOrEmpty(configFile))
{
@@ -193,7 +219,16 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
var husk = Character.Create(configFile, character.WorldPosition, character.Info, false, true);
XDocument doc = XMLExtensions.TryLoadXml(configFile);
if (doc?.Root == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - humanhusk config file ("+configFile+") could not be read.");
yield return CoroutineStatus.Success;
}
character.Info.Ragdoll = null;
character.Info.SourceElement = doc.Root;
var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, false, true);
foreach (Limb limb in husk.AnimController.Limbs)
{
@@ -0,0 +1,336 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
public static class CPRSettings
{
public static float ReviveChancePerSkill { get; private set; }
public static float ReviveChanceExponent { get; private set; }
public static float ReviveChanceMin { get; private set; }
public static float ReviveChanceMax { get; private set; }
public static float StabilizationPerSkill { get; private set; }
public static float StabilizationMin { get; private set; }
public static float StabilizationMax { get; private set; }
public static float DamageSkillThreshold { get; private set; }
public static float DamageSkillMultiplier { get; private set; }
public static void Load(XElement element)
{
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);
}
}
class AfflictionPrefab
{
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 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);
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 AfflictionPrefab Husk;
public static List<AfflictionPrefab> List = new List<AfflictionPrefab>();
//Arbitrary string that is used to identify the type of the affliction.
//Afflictions with the same type stack up, and items may be configured to cure specific types of afflictions.
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 readonly string Identifier;
public readonly string Name, Description;
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.0f;
public readonly float MaxStrength = 100.0f;
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 IconColor;
private List<Effect> effects = new List<Effect>();
private Dictionary<string, float> treatmentSuitability = new Dictionary<string, float>();
private readonly string typeName;
private readonly ConstructorInfo constructor;
public Dictionary<string, float> TreatmentSuitability
{
get { return treatmentSuitability; }
}
public static void LoadAll(IEnumerable<string> filePaths)
{
foreach (string filePath in filePaths)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
foreach (XElement element in doc.Root.Elements())
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "internaldamage":
List.Add(InternalDamage = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "bleeding":
List.Add(Bleeding = new AfflictionPrefab(element, typeof(AfflictionBleeding)));
break;
case "burn":
List.Add(Burn = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "oxygenlow":
List.Add(OxygenLow = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "bloodloss":
List.Add(Bloodloss = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "pressure":
List.Add(Pressure = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "stun":
List.Add(Stun = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "husk":
case "afflictionhusk":
List.Add(Husk = new AfflictionPrefab(element, typeof(AfflictionHusk)));
break;
case "cprsettings":
CPRSettings.Load(element);
break;
default:
List.Add(new AfflictionPrefab(element));
break;
}
}
}
if (InternalDamage == null) DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs.");
if (Bleeding == null) DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs.");
if (Burn == null) DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs.");
if (OxygenLow == null) DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs.");
if (Bloodloss == null) DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs.");
if (Pressure == null) DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs.");
if (Stun == null) DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs.");
if (Husk == null) DebugConsole.ThrowError("Affliction \"Husk\" not defined in the affliction prefabs.");
}
public AfflictionPrefab(XElement element, Type type = null)
{
typeName = type == null ? element.Name.ToString() : type.Name;
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", "");
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", ActivationThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
IconColor = subElement.GetAttributeColor("color", Color.White);
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 + "\".");
return;
}
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 || !treatmentSuitability.ContainsKey(item.Prefab.Identifier.ToLowerInvariant()))
{
return 0.0f;
}
return treatmentSuitability[item.Prefab.Identifier.ToLowerInvariant()];
}
}
}
@@ -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,716 @@
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class CharacterHealth
{
class LimbHealth
{
public Sprite IndicatorSprite;
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 "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);
}
}
const float InsufficientOxygenThreshold = 30.0f;
const float LowOxygenThreshold = 50.0f;
protected float minVitality, maxVitality;
public bool Unkillable;
//bleeding settings
public bool DoesBleed { get; private set; }
public bool UseHealthWindow { get; set; }
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 CrushDepth { get; private set; }
public float Vitality { get; private set; }
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;
maxVitality = 100.0f;
DoesBleed = true;
UseHealthWindow = false;
InitIrremovableAfflictions();
limbHealths.Add(new LimbHealth());
InitProjSpecific(null, character);
}
public CharacterHealth(XElement element, Character character)
{
this.Character = character;
InitIrremovableAfflictions();
CrushDepth = element.GetAttributeFloat("crushdepth", float.NegativeInfinity);
maxVitality = element.GetAttributeFloat("vitality", 100.0f);
Vitality = maxVitality;
DoesBleed = element.GetAttributeBool("doesbleed", true);
UseHealthWindow = element.GetAttributeBool("usehealthwindow", false);
minVitality = (character.ConfigPath == Character.HumanConfigFile) ? -100.0f : 0.0f;
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "limb") 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()
{
return afflictions.Concat(limbHealths.SelectMany(lh => lh.Afflictions).ToList());
}
public Affliction GetAffliction(string afflictionType, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
}
if (!allowLimbAfflictions) return null;
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
}
}
return null;
}
public Affliction GetAffliction(string afflictionType, Limb limb)
{
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
{
if (affliction.Prefab.AfflictionType == afflictionType) 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 void ReduceAffliction(Limb targetLimb, string affliction, float amount)
{
affliction = affliction.ToLowerInvariant();
List<Affliction> matchingAfflictions = new List<Affliction>(afflictions);
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.ToLowerInvariant() != affliction &&
a.Prefab.AfflictionType.ToLowerInvariant() != affliction);
if (matchingAfflictions.Count == 0) return;
do
{
float reduceAmount = amount / matchingAfflictions.Count;
for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
{
var matchingAffliction = matchingAfflictions[i];
if (matchingAffliction.Strength < reduceAmount)
{
amount -= matchingAffliction.Strength;
matchingAffliction.Strength = 0.0f;
matchingAfflictions.RemoveAt(i);
SteamAchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
}
else
{
matchingAffliction.Strength -= reduceAmount;
amount -= reduceAmount;
}
}
} while (matchingAfflictions.Count > 0 && amount > 0.0f);
}
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;
}
}
private void AddLimbAffliction(Limb limb, Affliction newAffliction)
{
if (!newAffliction.Prefab.LimbSpecific) 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));
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)),
newAffliction.Source);
limbHealth.Afflictions.Add(copyAffliction);
CalculateVitality();
if (Vitality <= MinVitality) Kill();
}
private void AddAffliction(Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) 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));
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)),
source: newAffliction.Source));
CalculateVitality();
if (Vitality <= MinVitality) Kill();
}
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);
}
}
foreach (Affliction affliction in limbHealths[i].Afflictions)
{
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);
}
}
}
for (int i = afflictions.Count - 1; i >= 0; i--)
{
if (irremovableAfflictions.Contains(afflictions[i])) continue;
if (afflictions[i].Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(afflictions[i], Character);
afflictions.RemoveAt(i);
}
}
for (int i = 0; i < afflictions.Count; i++)
{
afflictions[i].Update(this, null, deltaTime);
afflictions[i].DamagePerSecondTimer += deltaTime;
}
#if CLIENT
foreach (Limb limb in Character.AnimController.Limbs)
{
limb.BurnOverlayStrength = 0.0f;
limb.DamageOverlayStrength = 0.0f;
if (limbHealths[limb.HealthIndex].Afflictions.Count == 0) continue;
foreach (Affliction a in limbHealths[limb.HealthIndex].Afflictions)
{
limb.BurnOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.BurnOverlayAlpha;
limb.DamageOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.DamageOverlayAlpha;
}
limb.BurnOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count;
limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count;
}
#endif
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 CalculateVitality()
{
Vitality = MaxVitality;
if (Unkillable) { return; }
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant()))
{
vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()];
}
if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant()))
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()];
}
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
}
foreach (Affliction affliction in afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
}
private void Kill()
{
if (Unkillable) { return; }
var causeOfDeath = GetCauseOfDeath();
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
}
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);
newAffliction.DamagePerSecond = affliction.DamagePerSecond;
newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
mergedAfflictions.Add(newAffliction);
}
else
{
existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
existingAffliction.Strength += affliction.Strength;
}
}
return mergedAfflictions;
}
return allAfflictions;
}
public void ServerWrite(NetBuffer 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.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(affliction.Prefab));
msg.Write(affliction.Strength);
}
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(0, limbHealths.Count - 1, limbHealths.IndexOf(limbAffliction.First));
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(limbAffliction.Second.Prefab));
msg.Write(limbAffliction.Second.Strength);
}
}
public void Remove()
{
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
}
}
@@ -0,0 +1,94 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma
{
class DamageModifier
{
[Serialize(1.0f, false)]
public float DamageMultiplier
{
get;
private set;
}
[Serialize("0.0,360", false)]
public Vector2 ArmorSector
{
get;
private set;
}
[Serialize(true, false)]
public bool IsArmor
{
get;
private set;
}
[Serialize(false, false)]
public bool DeflectProjectiles
{
get;
private set;
}
public string[] AfflictionIdentifiers
{
get;
private set;
}
public string[] AfflictionTypes
{
get;
private set;
}
#if CLIENT
[Serialize("", false)]
public string DamageSound
{
get;
private set;
}
#endif
public DamageModifier(XElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
ArmorSector = new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
if (element.Attribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
}
AfflictionIdentifiers = element.GetAttributeStringArray("afflictionidentifiers", new string[0]);
for (int i = 0; i < AfflictionIdentifiers.Length; i++)
{
AfflictionIdentifiers[i] = AfflictionIdentifiers[i].ToLowerInvariant();
}
AfflictionTypes = element.GetAttributeStringArray("afflictiontypes", new string[0]);
for (int i = 0; i < AfflictionTypes.Length; i++)
{
AfflictionTypes[i] = AfflictionTypes[i].ToLowerInvariant();
}
}
public bool MatchesAffliction(Affliction affliction)
{
foreach (string afflictionName in AfflictionIdentifiers)
{
if (affliction.Prefab.Identifier.ToLowerInvariant() == afflictionName) return true;
}
foreach (string afflictionType in AfflictionTypes)
{
if (affliction.Prefab.AfflictionType.ToLowerInvariant() == afflictionType) return true;
}
return false;
}
}
}
@@ -7,7 +7,6 @@ namespace Barotrauma
{
class Job
{
private readonly JobPrefab prefab;
private Dictionary<string, Skill> skills;
@@ -31,12 +30,7 @@ namespace Barotrauma
{
get { return prefab.Items; }
}
//public List<bool> EquipSpawnItem
//{
// get { return prefab.EquipItem; }
//}
public List<Skill> Skills
{
get { return skills.Values.ToList(); }
@@ -49,61 +43,94 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
skills.Add(skillPrefab.Name, new Skill(skillPrefab));
skills.Add(skillPrefab.Identifier, new Skill(skillPrefab));
}
}
public Job(XElement element)
{
string name = element.GetAttributeString("name", "").ToLowerInvariant();
prefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == name);
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
prefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == identifier);
string name = "";
if (prefab == null)
{
name = element.GetAttributeString("name", "").ToLowerInvariant();
prefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == name);
}
if (prefab == null)
{
DebugConsole.ThrowError("Could not find the job \"" + name + "\" (identifier " + identifier + "). Giving the character a random job.");
prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count)];
}
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "skill") continue;
string skillName = subElement.GetAttributeString("name", "");
if (string.IsNullOrEmpty(name)) continue;
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) continue;
skills.Add(
skillName,
new Skill(skillName, subElement.GetAttributeInt("level", 0)));
skillIdentifier,
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
}
}
public static Job Random()
public static Job Random(Rand.RandSync randSync)
{
JobPrefab prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count - 1, Rand.RandSync.Server)];
JobPrefab prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count - 1, randSync)];
return new Job(prefab);
}
public int GetSkillLevel(string skillName)
public float GetSkillLevel(string skillIdentifier)
{
Skill skill = null;
skills.TryGetValue(skillName, out skill);
skills.TryGetValue(skillIdentifier, out Skill skill);
return (skill==null) ? 0 : skill.Level;
return (skill == null) ? 0.0f : skill.Level;
}
public void GiveJobItems(Character character, WayPoint spawnPoint)
public void IncreaseSkillLevel(string skillIdentifier, float increase)
{
if (skills.TryGetValue(skillIdentifier, out Skill skill))
{
skill.Level += increase;
}
}
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
{
if (SpawnItems == null) return;
foreach (XElement itemElement in SpawnItems.Elements())
{
InitializeJobItem(character, spawnPoint, itemElement);
InitializeJobItem(character, itemElement, spawnPoint);
}
}
private void InitializeJobItem(Character character, WayPoint spawnPoint, XElement itemElement, Item parentItem = null)
private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null)
{
string itemName = itemElement.GetAttributeString("name", "");
ItemPrefab itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
ItemPrefab itemPrefab;
if (itemElement.Attribute("name") != null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
return;
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);
@@ -125,7 +152,7 @@ namespace Barotrauma
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
if (item.Prefab.NameMatches("ID Card") && spawnPoint != null)
if (item.Prefab.Identifier == "idcard" && spawnPoint != null)
{
foreach (string s in spawnPoint.IdCardTags)
{
@@ -146,7 +173,7 @@ namespace Barotrauma
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeJobItem(character, spawnPoint, childItemElement, item);
InitializeJobItem(character, childItemElement, spawnPoint, item);
}
}
@@ -158,7 +185,7 @@ namespace Barotrauma
foreach (KeyValuePair<string, Skill> skill in skills)
{
jobElement.Add(new XElement("skill", new XAttribute("name", skill.Value.Name), new XAttribute("level", skill.Value.Level)));
jobElement.Add(new XElement("skill", new XAttribute("identifier", skill.Value.Identifier), new XAttribute("level", skill.Value.Level)));
}
parentElement.Add(jobElement);
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
@@ -11,31 +12,53 @@ namespace Barotrauma
public readonly List<string> ItemNames;
public List<SkillPrefab> Skills;
[Serialize("1,1,1,1", false)]
public Color UIColor
{
get;
private set;
}
//the number of these characters in the crew the player starts with
public readonly int InitialCount;
[Serialize("notfound", false)]
public string Identifier
{
get;
private set;
}
[Serialize("notfound", false)]
public string Name
{
get;
private set;
}
[Serialize("", false)]
public string Description
{
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)
//how many crew members can have the job (only one captain etc)
[Serialize(100, false)]
public int MaxNumber
{
get;
@@ -44,39 +67,46 @@ namespace Barotrauma
//how many crew members are REQUIRED to have the job
//(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference)
[Serialize(0, false)]
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 XElement ClothingElement { get; private set; }
public JobPrefab(XElement element)
{
Name = element.GetAttributeString("name", "name not found");
SerializableProperty.DeserializeProperties(this, element);
Description = element.GetAttributeString("description", "");
string translatedName = TextManager.Get("JobName." + Identifier, true);
if (!string.IsNullOrEmpty(translatedName)) Name = translatedName;
MinNumber = element.GetAttributeInt("minnumber", 0);
MaxNumber = element.GetAttributeInt("maxnumber", 10);
MinKarma = element.GetAttributeFloat("minkarma", 0.0f);
InitialCount = element.GetAttributeInt("initialcount", 0);
Commonness = element.GetAttributeInt("commonness", 10);
AllowAlways = element.GetAttributeBool("allowalways", false);
string translatedDescription = TextManager.Get("JobDescription." + Identifier, true);
if (!string.IsNullOrEmpty(translatedDescription)) Description = translatedDescription;
ItemNames = new List<string>();
@@ -90,8 +120,32 @@ namespace Barotrauma
Items = subElement;
foreach (XElement itemElement in subElement.Elements())
{
string itemName = itemElement.GetAttributeString("name", "");
if (!string.IsNullOrWhiteSpace(itemName)) ItemNames.Add(itemName);
if (itemElement.Element("name") != null)
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.");
ItemNames.Add(itemElement.GetAttributeString("name", ""));
continue;
}
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
if (string.IsNullOrWhiteSpace(itemIdentifier))
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier.");
ItemNames.Add("");
}
else
{
var prefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (prefab == null)
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item prefab \""+itemIdentifier+"\" not found.");
ItemNames.Add("");
}
else
{
ItemNames.Add(prefab.Name);
}
}
}
break;
case "skills":
@@ -104,6 +158,12 @@ namespace Barotrauma
}
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
ClothingElement = element.Element("PortraitClothing");
if (ClothingElement == null)
{
ClothingElement = element.Element("portraitclothing");
}
}
public static JobPrefab Random()
@@ -111,7 +171,7 @@ namespace Barotrauma
return List[Rand.Int(List.Count)];
}
public static void LoadAll(List<string> filePaths)
public static void LoadAll(IEnumerable<string> filePaths)
{
List = new List<JobPrefab>();
@@ -5,48 +5,47 @@ namespace Barotrauma
{
class Skill
{
SkillPrefab prefab;
string name;
int level;
private SkillPrefab prefab;
private float level;
static string[] levelNames = new string[] {
"Untrained", "Incompetent", "Novice",
"Adequate", "Competent", "Proficient",
"Professional", "Master", "Legendary" };
public string Name
string identifier;
public string Identifier
{
get { return name; }
get { return identifier; }
}
public int Level
public float Level
{
get { return level; }
set { level = MathHelper.Clamp(value, 0, 100); }
set { level = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public Skill(SkillPrefab prefab)
{
this.prefab = prefab;
this.name = prefab.Name;
this.identifier = prefab.Identifier;
this.level = (int)Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y);
this.level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
}
public Skill(string name, int level)
public Skill(string identifier, float level)
{
this.name = name;
this.identifier = identifier;
this.level = level;
}
/// <summary>
/// returns the "name" of some skill level (0-10 -> untrained, etc)
/// </summary>
public static string GetLevelName(int level)
public static string GetLevelName(float level)
{
level = MathHelper.Clamp(level, 0, 100);
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)];
@@ -5,17 +5,12 @@ namespace Barotrauma
{
class SkillPrefab
{
private string name;
private string description;
private Vector2 levelRange;
public string Name
{
get { return name; }
}
public readonly string Identifier;
public string Description
{
get { return description; }
@@ -28,7 +23,7 @@ namespace Barotrauma
public SkillPrefab(XElement element)
{
name = element.GetAttributeString("name", "");
Identifier = element.GetAttributeString("identifier", "");
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
@@ -41,7 +36,5 @@ namespace Barotrauma
levelRange = new Vector2(skillLevel, skillLevel);
}
}
}
}
@@ -1,13 +1,11 @@
//using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Items.Components;
using FarseerPhysics;
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.IO;
using System.Linq;
using System.Xml.Linq;
@@ -15,31 +13,89 @@ namespace Barotrauma
{
public enum LimbType
{
None, LeftHand, RightHand, LeftArm, RightArm,
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist
};
class LimbJoint : RevoluteJoint
partial class LimbJoint : RevoluteJoint
{
public bool IsSevered;
public bool CanBeSevered;
public bool CanBeSevered => jointParams.CanBeSevered;
public readonly JointParams jointParams;
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)
{
this.jointParams = 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 SaveParams()
{
// Saving to the params is handled only in the params level.
return;
jointParams.Stiffness = MaxMotorTorque;
if (ragdoll.IsFlipped)
{
jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorA.X, LocalAnchorA.Y) / jointParams.Ragdoll.JointScale);
jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorB.X, LocalAnchorB.Y) / jointParams.Ragdoll.JointScale);
jointParams.UpperLimit = MathHelper.ToDegrees(-LowerLimit);
jointParams.LowerLimit = MathHelper.ToDegrees(-UpperLimit);
}
else
{
jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorA / jointParams.Ragdoll.JointScale);
jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorB / jointParams.Ragdoll.JointScale);
jointParams.UpperLimit = MathHelper.ToDegrees(UpperLimit);
jointParams.LowerLimit = MathHelper.ToDegrees(LowerLimit);
}
}
public void LoadParams()
{
MaxMotorTorque = jointParams.Stiffness;
LimitEnabled = jointParams.LimitEnabled;
if (float.IsNaN(jointParams.LowerLimit))
{
jointParams.LowerLimit = 0;
}
if (float.IsNaN(jointParams.UpperLimit))
{
jointParams.UpperLimit = 0;
}
if (ragdoll.IsFlipped)
{
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb1Anchor.X, jointParams.Limb1Anchor.Y) * jointParams.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb2Anchor.X, jointParams.Limb2Anchor.Y) * jointParams.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(-jointParams.LowerLimit);
LowerLimit = MathHelper.ToRadians(-jointParams.UpperLimit);
}
else
{
LocalAnchorA = ConvertUnits.ToSimUnits(jointParams.Limb1Anchor * jointParams.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(jointParams.Limb2Anchor * jointParams.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(jointParams.UpperLimit);
LowerLimit = MathHelper.ToRadians(jointParams.LowerLimit);
}
}
}
partial class Limb
partial class Limb : ISerializableEntity
{
// Note: not used
private const float LimbDensity = 15;
private const float LimbAngularDamping = 7;
@@ -47,13 +103,16 @@ namespace Barotrauma
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 limbParams;
//the physics body of the limb
public PhysicsBody body;
protected readonly Vector2 stepOffset;
public Sprite sprite, damagedSprite;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(limbParams.StepOffset) * ragdoll.RagdollParams.JointScale;
public bool inWater;
@@ -68,16 +127,19 @@ namespace Barotrauma
public Vector2? MouthPos;
//a timer for delaying when a hitsound/attacksound can be played again
public float SoundTimer;
public const float SoundInterval = 0.4f;
public readonly Attack attack;
private List<DamageModifier> damageModifiers;
private Direction dir;
public int HealthIndex => limbParams.HealthIndex;
public float Scale => limbParams.Ragdoll.LimbScale;
public float AttackPriority => limbParams.AttackPriority;
public bool DoesFlip => limbParams.Flip;
public float SteerForce => limbParams.SteerForce;
public float AttackTimer;
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
public bool IsSevered
{
@@ -87,13 +149,11 @@ namespace Barotrauma
isSevered = value;
if (!isSevered) severedFadeOutTimer = 0.0f;
#if CLIENT
if (isSevered) damage = 100.0f;
if (isSevered) damageOverlayStrength = 100.0f;
#endif
}
}
public bool DoesFlip { get; private set; }
public Vector2 WorldPosition
{
get { return character.Submarine == null ? Position : Position + character.Submarine.Position; }
@@ -114,13 +174,9 @@ namespace Barotrauma
get { return body.Rotation; }
}
public float Scale { get; private set; }
//where an animcontroller is trying to pull the limb, only used for debug visualization
public Vector2 AnimTargetPos { get; private set; }
public float SteerForce { get; private set; }
public float Mass
{
get { return body.Mass; }
@@ -136,16 +192,19 @@ namespace Barotrauma
public float Dir
{
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
set { dir = (value==-1.0f) ? Direction.Left : Direction.Right; }
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
}
public int RefJointIndex { get; private set; }
public int RefJointIndex => limbParams.RefJoint;
public Vector2 StepOffset
private List<WearableSprite> wearingItems;
public List<WearableSprite> WearingItems
{
get { return stepOffset; }
get { return wearingItems; }
}
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>();
public bool PullJointEnabled
{
get { return pullJoint.Enabled; }
@@ -161,100 +220,105 @@ namespace Barotrauma
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 of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
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);
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchor:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, 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 of a limb's pull joint extremely far from the limb (diff: " + diff +
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);
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchor:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#endif
return;
}
pullJoint.WorldAnchorB = value;
}
}
public List<WearableSprite> WearingItems { get; private set; }
public Limb (Character character, XElement element, float scale = 1.0f)
public Vector2 PullJointLocalAnchorA
{
get { return pullJoint.LocalAnchorA; }
}
public string Name => limbParams.Name;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
this.character = character;
WearingItems = new List<WearableSprite>();
this.limbParams = limbParams;
wearingItems = new List<WearableSprite>();
dir = Direction.Right;
DoesFlip = element.GetAttributeBool("flip", false);
Scale = scale;
body = new PhysicsBody(element, scale);
if (element.GetAttributeBool("ignorecollisions", false))
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;
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
}
body.UserData = this;
RefJointIndex = -1;
Vector2 pullJointPos = Vector2.Zero;
if (element.Attribute("type") != null)
{
try
{
type = (LimbType)Enum.Parse(typeof(LimbType), element.Attribute("type").Value, true);
}
catch
{
type = LimbType.None;
DebugConsole.ThrowError("Error in "+element+"! \""+element.Attribute("type").Value+"\" is not a valid limb type");
}
pullJointPos = element.GetAttributeVector2("pullpos", Vector2.Zero) * scale;
pullJointPos = ConvertUnits.ToSimUnits(pullJointPos);
stepOffset = element.GetAttributeVector2("stepoffset", Vector2.Zero) * scale;
stepOffset = ConvertUnits.ToSimUnits(stepOffset);
RefJointIndex = element.GetAttributeInt("refjoint", -1);
}
else
{
type = LimbType.None;
}
pullJoint = new FixedMouseJoint(body.FarseerBody, pullJointPos)
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
@@ -262,8 +326,7 @@ namespace Barotrauma
GameMain.World.AddJoint(pullJoint);
SteerForce = element.GetAttributeFloat("steerforce", 0.0f);
var element = limbParams.Element;
if (element.Attribute("mouthpos") != null)
{
MouthPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("mouthpos", Vector2.Zero));
@@ -278,58 +341,33 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
string spritePath = subElement.Attribute("texture").Value;
string spritePathWithTags = spritePath;
if (character.Info != null)
{
spritePath = spritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "f" : "");
spritePath = spritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString());
if (character.Info.HeadSprite != null && character.Info.SpriteTags.Any())
{
string tags = "";
character.Info.SpriteTags.ForEach(tag => tags += "[" + tag + "]");
spritePathWithTags = Path.Combine(
Path.GetDirectoryName(spritePath),
Path.GetFileNameWithoutExtension(spritePath) + tags + Path.GetExtension(spritePath));
}
}
if (File.Exists(spritePathWithTags))
{
sprite = new Sprite(subElement, "", spritePathWithTags);
}
else
{
sprite = new Sprite(subElement, "", spritePath);
}
break;
case "damagedsprite":
string damagedSpritePath = subElement.Attribute("texture").Value;
if (character.Info != null)
{
damagedSpritePath = damagedSpritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "f" : "");
damagedSpritePath = damagedSpritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString());
}
damagedSprite = new Sprite(subElement, "", damagedSpritePath);
break;
case "attack":
attack = new Attack(subElement);
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));
damageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
}
}
SerializableProperties = SerializableProperty.GetProperties(this);
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
@@ -352,43 +390,53 @@ namespace Barotrauma
pullJoint.LocalAnchorA = new Vector2(-pullJoint.LocalAnchorA.X, pullJoint.LocalAnchorA.Y);
}
public AttackResult AddDamage(Vector2 position, DamageType damageType, float amount, float bleedingAmount, bool playSound)
public AttackResult AddDamage(Vector2 position, 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(position, afflictions, playSound);
}
public AttackResult AddDamage(Vector2 position, List<Affliction> afflictions, bool playSound)
{
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
foreach (DamageModifier damageModifier in damageModifiers)
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
afflictions = new List<Affliction>(afflictions);
for (int i = 0; i < afflictions.Count; i++)
{
if (damageModifier.DamageType == DamageType.None) continue;
if (damageModifier.DamageType.HasFlag(damageType) && SectorHit(damageModifier.ArmorSector, position))
foreach (DamageModifier damageModifier in damageModifiers)
{
appliedDamageModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in WearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (damageModifier.DamageType == DamageType.None) continue;
if (damageModifier.DamageType.HasFlag(damageType) && SectorHit(damageModifier.ArmorSector, position))
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
if (SectorHit(damageModifier.ArmorSector, position))
{
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in wearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
if (SectorHit(damageModifier.ArmorSector, position))
{
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
}
}
foreach (DamageModifier damageModifier in appliedDamageModifiers)
{
amount *= damageModifier.DamageMultiplier;
bleedingAmount *= damageModifier.BleedingMultiplier;
}
AddDamageProjSpecific(position, afflictions, playSound, appliedDamageModifiers);
AddDamageProjSpecific(position, damageType, amount, bleedingAmount, playSound, appliedDamageModifiers);
return new AttackResult(amount, bleedingAmount, appliedDamageModifiers);
return new AttackResult(afflictions, this, appliedDamageModifiers);
}
partial void AddDamageProjSpecific(Vector2 position, DamageType damageType, float amount, float bleedingAmount, bool playSound, List<DamageModifier> appliedDamageModifiers);
partial void AddDamageProjSpecific(Vector2 position, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
@@ -408,7 +456,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
UpdateProjSpecific(deltaTime);
if (LinearVelocity.X > 500.0f)
{
//DebugConsole.ThrowError("CHARACTER EXPLODED");
@@ -430,22 +478,26 @@ namespace Barotrauma
}
}
if (character.IsDead) return;
SoundTimer -= deltaTime;
if (attack != null)
{
attack.UpdateCoolDown(deltaTime);
}
}
partial void UpdateProjSpecific(float deltaTime);
public void UpdateAttack(float deltaTime, Vector2 attackPosition, IDamageable damageTarget)
/// <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 attackPosition, IDamageable damageTarget, out AttackResult attackResult, float distance = -1)
{
float dist = ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackPosition));
AttackTimer += deltaTime;
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
attackResult = default(AttackResult);
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackPosition));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime);
bool wasHit = false;
Body structureBody = null;
if (damageTarget != null)
{
switch (attack.HitDetectionType)
@@ -456,15 +508,29 @@ namespace Barotrauma
List<Body> ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
structureBody = Submarine.PickBody(
SimPosition, attackPosition,
ignoredBodies, Physics.CollisionWall);
wasHit = body == null;
if (damageTarget is Item && structureBody?.UserData is Item)
{
// If the attack is aimed to an item and hits an item, it's successful
wasHit = true;
}
else if (damageTarget is Structure && structureBody?.UserData is Structure)
{
// If the attack is aimed to a structure and hits a structure, it's successful
wasHit = true;
}
else
{
// If the attack is aimed to a character but hits a structure, the hit is blocked.
wasHit = structureBody == null;
}
}
break;
case HitDetection.Contact:
List<Body> targetBodies = new List<Body>();
var targetBodies = new List<Body>();
if (damageTarget is Character targetCharacter)
{
foreach (Limb limb in targetCharacter.AnimController.Limbs)
@@ -498,10 +564,10 @@ namespace Barotrauma
contactEdge.Contact.IsTouching &&
targetBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body))
{
structureBody = targetBodies.LastOrDefault();
wasHit = true;
break;
}
contactEdge = contactEdge.Next;
}
}
@@ -511,63 +577,134 @@ namespace Barotrauma
if (wasHit)
{
if (AttackTimer >= attack.Duration && damageTarget != null)
wasHit = damageTarget != null;
}
if (wasHit)
{
bool playSound = false;
#if CLIENT
playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval;
if (playSound)
{
attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, (SoundTimer <= 0.0f));
SoundTimer = SoundInterval;
LastAttackSoundTime = SoundInterval;
}
#endif
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(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation));
var from = body.FarseerBody.GetWorldPoint(localFront);
var to = from;
var drawPos = body.DrawPosition;
StickTo(structureBody, from, to);
}
attack.ResetAttackTimer();
attack.SetCoolDown();
}
Vector2 diff = attackPosition - SimPosition;
if (diff.LengthSquared() < 0.00001f) return;
if (attack.ApplyForceOnLimbs != null)
bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f;
if (applyForces)
{
foreach (int limbIndex in attack.ApplyForceOnLimbs)
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
Limb limb = character.AnimController.Limbs[limbIndex];
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
limb.body.ApplyLinearImpulse(
limb.Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos);
Limb limb = character.AnimController.Limbs[limbIndex];
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos);
}
}
else
{
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
body.ApplyLinearImpulse(Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos);
}
}
else
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)
{
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
body.ApplyLinearImpulse(Mass * attack.Force *
Vector2.Normalize(attackPosition - SimPosition), forcePos);
// 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(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation));
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.AddJoint(colliderJoint);
}
attachJoint = new WeldJoint(body.FarseerBody, target, from, to, true)
{
FrequencyHz = 1,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.AddJoint(attachJoint);
}
public void Release()
{
if (!IsStuck) { return; }
GameMain.World.RemoveJoint(attachJoint);
attachJoint = null;
if (colliderJoint != null)
{
GameMain.World.RemoveJoint(colliderJoint);
colliderJoint = null;
}
}
public void Remove()
{
if (sprite != null)
{
sprite.Remove();
sprite = null;
}
if (damagedSprite != null)
{
damagedSprite.Remove();
damagedSprite = null;
}
if (body != null)
{
body.Remove();
body = null;
}
#if CLIENT
if (LightSource != null)
{
LightSource.Remove();
}
#endif
body?.Remove();
body = null;
Release();
RemoveProjSpecific();
}
partial void RemoveProjSpecific();
public void LoadParams()
{
attack?.Deserialize();
pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(limbParams.PullPos * Scale);
LoadParamsProjSpecific();
}
partial void LoadParamsProjSpecific();
}
}
@@ -0,0 +1,39 @@
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 Name;
public readonly List<string> AllowedDialogTags;
private float commonness;
public NPCPersonalityTrait(XElement element)
{
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);
}
}
}