(d9829ac) v0.9.4.0

This commit is contained in:
Regalis
2019-10-24 18:05:42 +02:00
parent 9aa12bcac2
commit b39922a074
319 changed files with 12516 additions and 6815 deletions
@@ -2,10 +2,10 @@
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat }
abstract partial class AIController : ISteerable
{
public enum AIState { Idle, Attack, GoTo, Escape, Eat }
public bool Enabled;
public readonly Character Character;
@@ -21,7 +21,9 @@ namespace Barotrauma
/// <summary>
/// How long does it take for the ai target to fade out if not kept alive.
/// </summary>
public float FadeOutTime { get; private set; } = 3;
public float FadeOutTime { get; private set; }
public bool Static { get; private set; }
public float SoundRange
{
@@ -128,6 +130,19 @@ namespace Barotrauma
MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange);
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime);
Static = element.GetAttributeBool("static", Static);
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = MinSightRange;
SoundRange = MinSoundRange;
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
string typeString = element.GetAttributeString("type", "Any");
@@ -143,6 +158,16 @@ namespace Barotrauma
List.Add(this);
}
public void Update(float deltaTime)
{
if (!Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
SightRange -= deltaTime * (MaxSightRange / FadeOutTime);
SoundRange -= deltaTime * (MaxSoundRange / FadeOutTime);
}
}
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) return true;
File diff suppressed because it is too large Load Diff
@@ -69,6 +69,10 @@ namespace Barotrauma
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
{
throw new System.Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!");
}
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
@@ -324,7 +328,7 @@ namespace Barotrauma
AddTargets<AIObjectiveFightIntruders, Character>(Character, c);
if (newOrder == null)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders");
var orderPrefab = Order.GetPrefab("reportintruders");
newOrder = new Order(orderPrefab, c.CurrentHull, null, orderGiver: Character);
}
}
@@ -334,7 +338,7 @@ namespace Barotrauma
AddTargets<AIObjectiveExtinguishFires, Hull>(Character, hull);
if (newOrder == null)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire");
var orderPrefab = Order.GetPrefab("reportfire");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
}
}
@@ -347,7 +351,7 @@ namespace Barotrauma
{
if (newOrder == null)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid");
var orderPrefab = Order.GetPrefab("requestfirstaid");
newOrder = new Order(orderPrefab, c.CurrentHull, null, orderGiver: Character);
}
}
@@ -360,7 +364,7 @@ namespace Barotrauma
AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap);
if (newOrder == null && !gap.IsRoomToRoom)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach");
var orderPrefab = Order.GetPrefab("reportbreach");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
}
}
@@ -374,7 +378,7 @@ namespace Barotrauma
AddTargets<AIObjectiveRepairItems, Item>(Character, item);
if (newOrder == null)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbrokendevices");
var orderPrefab = Order.GetPrefab("reportbrokendevices");
newOrder = new Order(orderPrefab, item.CurrentHull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
}
}
@@ -518,11 +522,7 @@ namespace Barotrauma
}
else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
{
//TODO: re-enable on all languages after DialogNoRescueTargets has been translated
if (TextManager.Language == "English")
{
Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
}
Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
}
else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
{
@@ -620,7 +620,7 @@ namespace Barotrauma
public static void RefreshTargets(Character character, Order order, Hull hull)
{
switch (order.AITag)
switch (order.Identifier)
{
case "reportfire":
AddTargets<AIObjectiveExtinguishFires, Hull>(character, hull);
@@ -667,7 +667,7 @@ namespace Barotrauma
break;
default:
#if DEBUG
DebugConsole.ThrowError(order.AITag + " not implemented!");
DebugConsole.ThrowError(order.Identifier + " not implemented!");
#endif
break;
}
@@ -765,6 +765,9 @@ namespace Barotrauma
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public static bool IsFriendly(Character me, Character other) => (other.TeamID == me.TeamID || other.TeamID == Character.TeamType.FriendlyNPC || me.TeamID == Character.TeamType.FriendlyNPC) && other.SpeciesName == me.SpeciesName;
public static bool IsFriendly(Character me, Character other) =>
(other.TeamID == me.TeamID ||
other.TeamID == Character.TeamType.FriendlyNPC ||
me.TeamID == Character.TeamType.FriendlyNPC) && (other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group));
}
}
@@ -130,7 +130,7 @@ namespace Barotrauma
switch (enemyAI.State)
{
case AIController.AIState.Idle:
case AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
{
raycastTimer -= deltaTime;
@@ -187,7 +187,7 @@ namespace Barotrauma
}
}
break;
case AIController.AIState.Attack:
case AIState.Attack:
if (enemyAI.AttackingLimb != null)
{
if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null)
@@ -33,7 +33,7 @@ namespace Barotrauma
{
if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
if (doc == 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));
@@ -44,7 +44,7 @@ namespace Barotrauma
{
if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
if (doc == 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));
@@ -73,7 +73,7 @@ namespace Barotrauma
private static void Load(string file)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null || doc.Root == null) return;
if (doc == null) { return; }
string language = doc.Root.GetAttributeString("Language", "English");
if (language != TextManager.Language) return;
@@ -102,8 +102,10 @@ namespace Barotrauma
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);
if (JobPrefab.List.TryGetValue(allowedJobIdentifier.ToLowerInvariant(), out JobPrefab jobPrefab))
{
AllowedJobs.Add(jobPrefab);
}
}
Flags = new List<string>(element.GetAttributeStringArray("flags", new string[0]));
@@ -115,7 +115,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
return;
}
container.Combine(itemToContain);
container.Combine(itemToContain, character);
}
}
@@ -68,13 +68,13 @@ namespace Barotrauma
public void CreateAutonomousObjectives()
{
Objectives.Clear();
AddObjective(new AIObjectiveFindSafety(character, this), delay: Rand.Value() / 2);
AddObjective(new AIObjectiveIdle(character, this), delay: Rand.Value() / 2);
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
int objectiveCount = Objectives.Count;
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == automaticOrder.aiTag);
if (orderPrefab == null) { throw new Exception("Could not find a matching prefab by ai tag: " + automaticOrder.aiTag); }
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
// TODO: Similar code is used in CrewManager:815-> DRY
var matchingItems = orderPrefab.ItemIdentifiers.Any() ?
Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) :
@@ -144,7 +144,7 @@ namespace Barotrauma
if (previousObjective != CurrentObjective)
{
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>()?.SetRandom();
GetObjective<AIObjectiveIdle>().SetRandom();
}
return CurrentObjective;
}
@@ -231,7 +231,7 @@ namespace Barotrauma
{
if (order == null) { return null; }
AIObjective newObjective;
switch (order.AITag.ToLowerInvariant())
switch (order.Identifier.ToLowerInvariant())
{
case "follow":
if (orderGiver == null) { return null; }
@@ -53,7 +53,7 @@ namespace Barotrauma
{
if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > vitalityThreshold) { return false; }
}
if (target.Submarine == null) { return false; }
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
@@ -10,9 +10,16 @@ namespace Barotrauma
{
class Order
{
private static string ConfigFile = Path.Combine("Content", "Orders.xml");
public static List<Order> PrefabList;
public static Dictionary<string, Order> Prefabs { get; private set; }
public static List<Order> PrefabList { get; private set; }
public static Order GetPrefab(string identifier)
{
if (!Prefabs.TryGetValue(identifier, out Order order))
{
DebugConsole.ThrowError($"Cannot find an order with the identifier '{identifier}'!");
}
return order;
}
public Order Prefab
{
@@ -27,7 +34,7 @@ namespace Barotrauma
public readonly Type ItemComponentType;
public readonly string[] ItemIdentifiers;
public readonly string AITag;
public readonly string Identifier;
public readonly Color Color;
@@ -43,30 +50,64 @@ namespace Barotrauma
public Character OrderGiver;
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
static Order()
{
PrefabList = new List<Order>();
Prefabs = new Dictionary<string, Order>();
XDocument doc = XMLExtensions.TryLoadXml(ConfigFile);
if (doc == null || doc.Root == null) return;
foreach (XElement orderElement in doc.Root.Elements())
foreach (string file in GameMain.Instance.GetFilesOfType(ContentType.Orders))
{
if (orderElement.Name.ToString().ToLowerInvariant() != "order") continue;
var newOrder = new Order(orderElement);
newOrder.Prefab = newOrder;
PrefabList.Add(newOrder);
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null) { continue; }
var mainElement = doc.Root;
bool allowOverriding = false;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement sourceElement in mainElement.Elements())
{
var orderElement = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = orderElement.Name.ToString();
if (name.Equals("order", StringComparison.OrdinalIgnoreCase))
{
string identifier = orderElement.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Error in file {file}: The order element '{name}' does not have an identifier! All orders must have a unique identifier.");
continue;
}
if (Prefabs.TryGetValue(identifier, out Order duplicate))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file}'", Color.Yellow);
Prefabs.Remove(identifier);
}
else
{
DebugConsole.ThrowError($"Error in file {file}: Duplicate element with the idenfitier '{identifier}' found in '{file}'! All orders must have a unique identifier. Use <override></override> tags to override an order with the same identifier.");
continue;
}
}
var newOrder = new Order(orderElement);
newOrder.Prefab = newOrder;
Prefabs.Add(identifier, newOrder);
}
}
}
PrefabList = new List<Order>(Prefabs.Values);
}
private Order(XElement orderElement)
{
AITag = orderElement.GetAttributeString("aitag", "");
Name = TextManager.Get("OrderName." + AITag, true) ?? "Name not found";
Identifier = orderElement.GetAttributeString("identifier", "");
Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found";
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType))
@@ -78,7 +119,7 @@ namespace Barotrauma
catch (Exception e)
{
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemType + " not found", e);
DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e);
}
}
@@ -90,7 +131,7 @@ namespace Barotrauma
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
string translatedOptionNames = TextManager.Get("OrderOptions." + AITag, true);
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
{
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
@@ -116,7 +157,7 @@ namespace Barotrauma
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
SymbolSprite = new Sprite(subElement);
SymbolSprite = new Sprite(subElement, lazyLoad: true);
break;
}
}
@@ -127,7 +168,7 @@ namespace Barotrauma
Prefab = prefab;
Name = prefab.Name;
AITag = prefab.AITag;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
@@ -155,8 +196,14 @@ namespace Barotrauma
public bool HasAppropriateJob(Character character)
{
if (AppropriateJobs == null || AppropriateJobs.Length == 0) return true;
if (character.Info == null || character.Info.Job == null) return false;
if (character.Info == null || character.Info.Job == null) { return false; }
if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; }
if (!JobPrefab.List.Values.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
(AppropriateJobs == null || AppropriateJobs.Length == 0))
{
return true;
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) return true;
@@ -168,7 +215,7 @@ namespace Barotrauma
{
orderOption = orderOption ?? "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + AITag;
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption;
if (targetCharacterName == null) targetCharacterName = "";
@@ -20,8 +20,8 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(file, position, seed, characterInfo, isNetworkPlayer, ragdoll)
public AICharacter(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(speciesName, position, seed, characterInfo, isNetworkPlayer, ragdoll)
{
InitProjSpecific();
}
@@ -37,7 +37,7 @@ namespace Barotrauma
}
if (!CanWalk)
{
DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!");
//DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!");
return null;
}
else
@@ -214,6 +214,8 @@ namespace Barotrauma
return SwimSlowParams;
case AnimationType.SwimFast:
return SwimFastParams;
case AnimationType.NotDefined:
return null;
default:
throw new NotImplementedException(type.ToString());
}
@@ -285,16 +285,9 @@ namespace Barotrauma
public override void DragCharacter(Character target, float deltaTime)
{
if (target == null) return;
Limb mouthLimb = Array.Find(Limbs, l => l != null && l.MouthPos.HasValue);
if (mouthLimb == null) mouthLimb = GetLimb(LimbType.Head);
if (mouthLimb == null)
{
DebugConsole.ThrowError("Character \"" + character.SpeciesName + "\" failed to eat a target (a head or a limb with a mouthpos required)");
return;
}
if (target == null) { return; }
Limb mouthLimb = GetLimb(LimbType.Head);
if (mouthLimb == null) { return; }
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -489,9 +482,9 @@ namespace Barotrauma
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID))
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.limbParams.ID] * Dir, MainLimb, FootTorque);
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, MainLimb, FootTorque);
}
break;
case LimbType.Tail:
@@ -557,6 +550,9 @@ namespace Barotrauma
movementAngle -= MathHelper.TwoPi;
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
@@ -566,7 +562,7 @@ namespace Barotrauma
}
if (TorsoPosition.HasValue)
{
Vector2 pos = colliderBottom + Vector2.UnitY * TorsoPosition.Value;
Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift);
if (torso != MainLimb)
{
@@ -588,7 +584,7 @@ namespace Barotrauma
}
if (HeadPosition.HasValue)
{
Vector2 pos = colliderBottom + Vector2.UnitY * HeadPosition.Value;
Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
if (head != MainLimb)
{
@@ -673,10 +669,10 @@ namespace Barotrauma
#if CLIENT
if (playFootstepSound) { PlayImpactSound(limb); }
#endif
if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID))
if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb,
movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.limbParams.ID] * Dir,
movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.Params.ID] * Dir,
MainLimb, FootTorque);
}
break;
@@ -637,9 +637,15 @@ namespace Barotrauma
}
else
{
if (!onGround) movement = Vector2.Zero;
if (!onGround)
{
movement = Vector2.Zero;
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
float y = colliderPos.Y;
float y = colliderPos.Y + stepLift;
if (TorsoPosition.HasValue)
{
y += TorsoPosition.Value;
@@ -648,7 +654,7 @@ namespace Barotrauma
MathUtils.SmoothStep(torso.SimPosition,
new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce);
y = colliderPos.Y;
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue)
{
y += HeadPosition.Value;
@@ -809,10 +815,11 @@ namespace Barotrauma
//get the elbow to a neutral rotation
if (Math.Abs(hand.body.AngularVelocity) < 10.0f)
{
LimbJoint elbow =
GetJointBetweenLimbs(armType, hand.type) ??
GetJointBetweenLimbs(armType, foreArmType);
hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f);
LimbJoint elbow = GetJointBetweenLimbs(armType, hand.type) ?? GetJointBetweenLimbs(armType, foreArmType);
if (elbow != null)
{
hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f);
}
}
}
}
@@ -1848,7 +1855,11 @@ namespace Barotrauma
}
var torso = GetLimb(LimbType.Torso);
var waist = GetJointBetweenLimbs(LimbType.Waist, upperLeg.type);
Vector2 waistPos = waist.LimbA == upperLeg ? waist.WorldAnchorA : waist.WorldAnchorB;
Vector2 waistPos = Vector2.Zero;
if (waist != null)
{
waistPos = waist.LimbA == upperLeg ? waist.WorldAnchorA : waist.WorldAnchorB;
}
//distance from waist joint to the target position
float c = Vector2.Distance(pos, waistPos);
@@ -1,676 +0,0 @@
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; }
private float limbScale;
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
private float jointScale;
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize.
[Serialize(1f, false)]
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 GetDefaultFile(string speciesName, ContentPackage contentPackage = null)
=> Path.Combine(GetFolder(speciesName, contentPackage), $"{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")))
};
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
string configFilePath = Character.GetConfigFile(speciesName, contentPackage);
var folder = XMLExtensions.TryLoadXml(configFilePath)?.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
{
folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Ragdolls") + Path.DirectorySeparatorChar;
}
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();
if (doc == null)
{
DebugConsole.ThrowError("[RagdollParams] The source XML Document is null!");
return;
}
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)
{
if (source.MainElement == null)
{
DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!");
return;
}
Deserialize(source.MainElement, recursive: false);
var sourceSubParams = source.GetAllSubParams().ToList();
var subParams = GetAllSubParams().ToList();
// TODO: cannot currently undo joint/limb deletion.
if (sourceSubParams.Count != subParams.Count)
{
DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.");
return;
}
for (int i = 0; i < subParams.Count; i++)
{
var subSubParams = subParams[i].SubParams;
if (subSubParams.Count != sourceSubParams[i].SubParams.Count)
{
DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.");
return;
}
subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false);
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 void AddToEditor(ParamsEditor editor, bool alsoChildren = true)
{
base.AddToEditor(editor);
if (alsoChildren)
{
var subParams = GetAllSubParams();
foreach (var subParam in subParams)
{
subParam.AddToEditor(editor);
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, 10), editor.EditorBox.Content.RectTransform),
style: null, color: Color.Black);
}
}
}
#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, inGame: false, showName: true);
SubParams.ForEach(sp => sp.AddToEditor(editor));
}
#endif
}
}
@@ -10,6 +10,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
using JointParams = Barotrauma.RagdollParams.JointParams;
namespace Barotrauma
{
@@ -223,7 +225,7 @@ namespace Barotrauma
{
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered || !limb.body.PhysEnabled) { continue; }
limb.body.SetTransform(Collider.SimPosition, Collider.Rotation);
//reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated)
limb.PullJointEnabled = false;
@@ -233,8 +235,7 @@ namespace Barotrauma
}
}
// Currently the camera cannot handle greater speeds. It starts to lag behind.
public const float MAX_SPEED = 9;
public const float MAX_SPEED = 15;
public Vector2 TargetMovement
{
@@ -258,6 +259,7 @@ namespace Barotrauma
public float ImpactTolerance => RagdollParams.ImpactTolerance;
public bool Draggable => RagdollParams.Draggable;
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure));
public float Dir
{
@@ -317,7 +319,7 @@ namespace Barotrauma
}
else
{
items = limbs?.ToDictionary(l => l.limbParams, l => l.WearingItems);
items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems);
}
foreach (var limbParams in RagdollParams.Limbs)
{
@@ -327,7 +329,7 @@ namespace Barotrauma
limbParams.Radius = 10;
}
}
foreach (var colliderParams in RagdollParams.ColliderParams)
foreach (var colliderParams in RagdollParams.Colliders)
{
if (!PhysicsBody.IsValidShape(colliderParams.Radius, colliderParams.Height, colliderParams.Width))
{
@@ -352,11 +354,16 @@ namespace Barotrauma
limb.WearingItems.AddRange(itemList);
}
}
if (character.SpeciesName.ToLowerInvariant() == "humanhusk")
if (character.IsHusk)
{
if (Limbs.None(l => l.Name.ToLowerInvariant() == "huskappendage"))
if (Character.TryGetConfigFile(character.ConfigPath, out XDocument configFile))
{
AfflictionHusk.AttachHuskAppendage(character, this);
var mainElement = configFile.Root.IsOverride() ? configFile.Root.FirstElement() : configFile.Root;
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
{
AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this);
}
}
}
}
@@ -376,7 +383,7 @@ namespace Barotrauma
}
DebugConsole.Log($"Creating colliders from {RagdollParams.Name}.");
collider = new List<PhysicsBody>();
foreach (ColliderParams cParams in RagdollParams.ColliderParams)
foreach (var cParams in RagdollParams.Colliders)
{
if (!PhysicsBody.IsValidShape(cParams.Radius, cParams.Height, cParams.Width))
{
@@ -456,14 +463,12 @@ namespace Barotrauma
/// </summary>
public void SaveRagdoll(string fileNameWithoutExtension = null)
{
SaveJoints();
SaveLimbs();
RagdollParams.Save(fileNameWithoutExtension);
}
/// <summary>
/// Resets the serializable data to the currently selected ragdoll params.
/// Force reloading always loads the xml stored in the disk.
/// Force reloading always loads the xml stored on the disk.
/// </summary>
public void ResetRagdoll(bool forceReload = false)
{
@@ -472,24 +477,6 @@ namespace Barotrauma
ResetLimbs();
}
/// <summary>
/// Saves the current joint values to the serializable joint params. This method should properly handle character flipping.
/// NOTE: Currently all the params are handled stored as SubRagdollParams and handled in the RagdollParams Save method. This method does nothing.
/// </summary>
public void SaveJoints()
{
LimbJoints.ForEach(j => j.SaveParams());
}
/// <summary>
/// Handles custom serialization per limb. Currently only the attacks need to be serialized, since they cannot be stored as SubRagdollParams (because they shouldn't be decoupled with ragdolls).
/// Note: Saving to file is not handled by this method. Calling RagdollParams.Save() after this method should work.
/// </summary>
public void SaveLimbs()
{
Limbs.ForEach(l => l.attack?.Serialize());
}
/// <summary>
/// Resets the current joint values to the serialized joint params.
/// </summary>
@@ -792,17 +779,9 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered) continue;
if (limb == null || limb.IsSevered) { continue; }
limb.Dir = Dir;
if (limb.MouthPos.HasValue)
{
limb.MouthPos = new Vector2(
-limb.MouthPos.Value.X,
limb.MouthPos.Value.Y);
}
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint();
}
@@ -1052,6 +1031,21 @@ namespace Barotrauma
/// </summary>
private float bodyInRestTimer;
private float BodyInRestDelay = 1.0f;
public bool BodyInRest
{
get { return bodyInRestTimer > BodyInRestDelay; }
set
{
foreach (Limb limb in Limbs)
{
limb.body.PhysEnabled = !value;
}
bodyInRestTimer = value ? BodyInRestDelay : 0.0f;
}
}
public bool forceStanding;
public void Update(float deltaTime, Camera cam)
@@ -1335,7 +1329,7 @@ namespace Barotrauma
else if (Limbs.All(l => l != null && !l.body.Enabled || l.LinearVelocity.LengthSquared() < 0.001f))
{
bodyInRestTimer += deltaTime;
if (bodyInRestTimer > 1.0f)
if (bodyInRestTimer > BodyInRestDelay)
{
foreach (Limb limb in Limbs)
{
@@ -1615,24 +1609,27 @@ namespace Barotrauma
{
if (GameMain.NetworkMember == null) return;
float lowestSubPos = ConvertUnits.ToSimUnits(Submarine.Loaded.Min(s => s.HiddenSubPosition.Y - s.Borders.Height - 128.0f));
for (int i = 0; i < character.MemState.Count; i++ )
float lowestSubPos = float.MaxValue;
if (Submarine.Loaded.Any())
{
if (character.Submarine == null)
lowestSubPos = ConvertUnits.ToSimUnits(Submarine.Loaded.Min(s => s.HiddenSubPosition.Y - s.Borders.Height - 128.0f));
for (int i = 0; i < character.MemState.Count; i++)
{
//transform in-sub coordinates to outside coordinates
if (character.MemState[i].Position.Y > lowestSubPos)
character.MemState[i].TransformInToOutside();
}
else if (currentHull?.Submarine != null)
{
//transform outside coordinates to in-sub coordinates
if (character.MemState[i].Position.Y < lowestSubPos)
character.MemState[i].TransformOutToInside(currentHull.Submarine);
if (character.Submarine == null)
{
//transform in-sub coordinates to outside coordinates
if (character.MemState[i].Position.Y > lowestSubPos)
character.MemState[i].TransformInToOutside();
}
else if (currentHull?.Submarine != null)
{
//transform outside coordinates to in-sub coordinates
if (character.MemState[i].Position.Y < lowestSubPos)
character.MemState[i].TransformOutToInside(currentHull.Submarine);
}
}
}
UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos);
}
@@ -1663,23 +1660,15 @@ namespace Barotrauma
public Vector2? GetMouthPosition()
{
Limb mouthLimb = Array.Find(Limbs, l => l != null && l.MouthPos.HasValue);
if (mouthLimb == null) mouthLimb = GetLimb(LimbType.Head);
if (mouthLimb == null) return null;
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) * RagdollParams.LimbScale;
}
return mouthPos;
Limb mouthLimb = GetLimb(LimbType.Head);
if (mouthLimb == null) { return null; }
float cos = (float)Math.Cos(mouthLimb.Rotation);
float sin = (float)Math.Sin(mouthLimb.Rotation);
Vector2 bodySize = mouthLimb.body.GetSize();
Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2);
return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * RagdollParams.LimbScale;
}
public Vector2 GetColliderBottom()
{
float offset = 0.0f;
@@ -30,7 +30,9 @@ namespace Barotrauma
FallBack,
FallBackUntilCanAttack,
PursueIfCanAttack,
Pursue
Pursue,
FollowThrough,
FollowThroughUntilCanAttack
}
struct AttackResult
@@ -65,42 +67,43 @@ namespace Barotrauma
AppliedDamageModifiers = appliedDamageModifiers;
}
}
partial class Attack : ISerializableEntity
{
public readonly XElement SourceElement;
[Serialize(AttackContext.NotDefined, true), Editable]
[Serialize(AttackContext.NotDefined, true, description: "Is the attack used only in a specific condition?"), Editable]
public AttackContext Context { get; private set; }
[Serialize(AttackTarget.Any, true), Editable]
[Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable]
public AttackTarget TargetType { get; private set; }
[Serialize(HitDetection.Distance, true), Editable]
[Serialize(LimbType.None, true, description: "If not defined or set to none, the closest limb is used (default)."), Editable]
public LimbType TargetLimbType { get; private set; }
[Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable]
public HitDetection HitDetectionType { get; private set; }
[Serialize(AIBehaviorAfterAttack.FallBack, true), Editable(ToolTip = "The preferred AI behavior after the attack.")]
[Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable]
public AIBehaviorAfterAttack AfterAttack { get; set; }
[Serialize(false, true), Editable(ToolTip = "Should the ai try to reverse when aiming with this attack?")]
[Serialize(false, true, description: "Should the AI try to reverse when aiming with this attack?"), Editable]
public bool Reverse { 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.")]
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
public float Range { get; set; }
[Serialize(0.0f, true), 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.")]
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
public float DamageRange { get; set; }
[Serialize(0.25f, true), 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.")]
[Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Duration { get; private set; }
[Serialize(5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "How long the AI waits between the attacks.")]
[Serialize(5f, true, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float CoolDown { get; set; } = 5;
[Serialize(0f, true), 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.")]
[Serialize(0f, true, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float SecondaryCoolDown { get; set; } = 0;
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2, ToolTip = "Random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases).")]
[Serialize(0f, true, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float CoolDownRandomFactor { get; private set; } = 0;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
@@ -115,7 +118,7 @@ namespace Barotrauma
[Serialize(0.0f, false)]
public float Stun { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, true, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; private set; }
[Serialize("", true), Editable]
@@ -139,36 +142,38 @@ namespace Barotrauma
}
}
[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.")]
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; private set; }
[Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb.")]
[Serialize(0.0f, true, description: "Applied to the attacking limb."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Torque { get; private set; }
[Serialize(false, true), Editable]
public bool ApplyForcesOnlyOnce { get; private set; }
[Serialize(0.0f, true), 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).")]
[Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float TargetImpulse { get; private set; }
[Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")]
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable]
public Vector2 TargetImpulseWorld { get; private set; }
[Serialize(0.0f, true), 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).")]
[Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)]
public float TargetForce { get; private set; }
[Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")]
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float SeverLimbsProbability { get; set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float StickChance { get; set; }
// TODO: disabled because not synced
//[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
//public float StickChance { get; set; }
public float StickChance => 0f;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Priority { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
{
get { return statusEffects; }
@@ -186,7 +191,7 @@ namespace Barotrauma
//(if none, force is applied only to the limb the attack is attached to)
public readonly List<int> ForceOnLimbIndices = new List<int>();
public readonly List<Affliction> Afflictions = new List<Affliction>();
public readonly Dictionary<Affliction, XElement> Afflictions = new Dictionary<Affliction, XElement>();
/// <summary>
/// Only affects ai decision making. All the conditionals has to be met in order to select the attack. TODO: allow to define conditionals using any (implemented in StatusEffect -> move from there to PropertyConditional?)
@@ -207,7 +212,7 @@ namespace Barotrauma
public List<Affliction> GetMultipliedAfflictions(float multiplier)
{
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions)
foreach (Affliction affliction in Afflictions.Keys)
{
multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source));
}
@@ -227,7 +232,7 @@ namespace Barotrauma
public float GetTotalDamage(bool includeStructureDamage = false)
{
float totalDamage = includeStructureDamage ? StructureDamage : 0.0f;
foreach (Affliction affliction in Afflictions)
foreach (Affliction affliction in Afflictions.Keys)
{
totalDamage += affliction.GetVitalityDecrease(null);
}
@@ -236,9 +241,9 @@ namespace Barotrauma
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));
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null);
Range = range;
DamageRange = range;
@@ -247,8 +252,7 @@ namespace Barotrauma
public Attack(XElement element, string parentDebugName)
{
SourceElement = element;
Deserialize();
Deserialize(element);
if (element.Attribute("damage") != null ||
element.Attribute("bluntdamage") != null ||
@@ -258,8 +262,6 @@ namespace Barotrauma
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())
@@ -297,10 +299,9 @@ namespace Barotrauma
}
}
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
var affliction = afflictionPrefab.Instantiate(afflictionStrength);
affliction.ApplyProbability = subElement.GetAttributeFloat("probability", 1.0f);
Afflictions.Add(affliction);
//float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
//var affliction = afflictionPrefab.Instantiate(afflictionStrength);
//Afflictions.Add(affliction, subElement);
break;
case "conditional":
@@ -310,21 +311,50 @@ namespace Barotrauma
}
break;
}
}
}
partial void InitProjSpecific(XElement element);
partial void InitProjSpecific(XElement element = null);
public void Serialize()
public void ReloadAfflictions(XElement element)
{
if (SourceElement == null) { return; }
SerializableProperty.SerializeProperties(this, SourceElement, true);
Afflictions.Clear();
foreach (var subElement in element.GetChildElements("affliction"))
{
AfflictionPrefab afflictionPrefab;
Affliction affliction;
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
if (afflictionPrefab != null)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
affliction = afflictionPrefab.Instantiate(afflictionStrength);
}
else
{
affliction = new Affliction(null, 0);
}
affliction.Deserialize(subElement);
// add the affliction anyway, so that it can be shown in the editor.
Afflictions.Add(affliction, subElement);
}
}
public void Deserialize()
public void Serialize(XElement element)
{
if (SourceElement == null) { return; }
SerializableProperties = SerializableProperty.DeserializeProperties(this, SourceElement);
SerializableProperty.SerializeProperties(this, element, true);
foreach (var affliction in Afflictions)
{
if (affliction.Value != null)
{
affliction.Key.Serialize(affliction.Value);
}
}
}
public void Deserialize(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ReloadAfflictions(element);
}
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true)
@@ -332,7 +362,10 @@ namespace Barotrauma
Character targetCharacter = target as Character;
if (OnlyHumans)
{
if (targetCharacter != null && targetCharacter.ConfigPath != Character.HumanConfigFile) return new AttackResult();
if (targetCharacter != null && !targetCharacter.IsHuman)
{
return new AttackResult();
}
}
SetUser(attacker);
@@ -389,7 +422,10 @@ namespace Barotrauma
if (OnlyHumans)
{
if (targetLimb.character != null && targetLimb.character.ConfigPath != Character.HumanConfigFile) return new AttackResult();
if (targetLimb.character != null && !targetLimb.character.IsHuman)
{
return new AttackResult();
}
}
SetUser(attacker);
@@ -418,7 +454,6 @@ namespace Barotrauma
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
}
return attackResult;
File diff suppressed because it is too large Load Diff
@@ -86,8 +86,6 @@ namespace Barotrauma
}
}
private static Dictionary<string, XDocument> cachedConfigs = new Dictionary<string, XDocument>();
private static ushort idCounter;
public string Name;
@@ -128,14 +126,24 @@ namespace Barotrauma
}
}
public string SpeciesName => SourceElement.GetAttributeString("name", string.Empty);
private string _speciesName;
public string SpeciesName
{
get
{
if (_speciesName == null)
{
_speciesName = CharacterConfigElement.GetAttributeString("speciesname", string.Empty).ToLowerInvariant();
}
return _speciesName;
}
set { _speciesName = value; }
}
/// <summary>
/// Note: Can be null.
/// </summary>
public Character Character;
public readonly string File;
public Job Job;
@@ -190,7 +198,7 @@ namespace Barotrauma
{
if (portraitBackground == null)
{
var portraitBackgroundElement = SourceElement.Element("portraitbackground");
var portraitBackgroundElement = CharacterConfigElement.Element("portraitbackground");
if (portraitBackgroundElement != null)
{
portraitBackground = new Sprite(portraitBackgroundElement.Element("sprite"));
@@ -229,7 +237,7 @@ namespace Barotrauma
}
}
public XElement SourceElement { get; set; }
public XElement CharacterConfigElement { get; set; }
public readonly string ragdollFileName = string.Empty;
@@ -329,7 +337,7 @@ namespace Barotrauma
if (ragdoll == null)
{
string speciesName = SpeciesName;
bool isHumanoid = SourceElement.GetAttributeBool("humanoid", false);
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(Character.HumanSpeciesName, StringComparison.OrdinalIgnoreCase));
ragdoll = isHumanoid
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
: RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
@@ -342,16 +350,21 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string file, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null)
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
}
ID = idCounter;
idCounter++;
File = file;
_speciesName = speciesName;
SpriteTags = new List<string>();
XDocument doc = GetConfig(file);
SourceElement = doc.Root;
XDocument doc = Character.GetConfigFile(_speciesName);
if (doc == null) { return; }
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
head = new HeadInfo();
HasGenders = doc.Root.GetAttributeBool("genders", false);
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
if (HasGenders)
{
Head.gender = GetRandomGender();
@@ -367,16 +380,16 @@ namespace Barotrauma
else
{
name = "";
if (doc.Root.Element("name") != null)
if (CharacterConfigElement.Element("name") != null)
{
string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", "");
string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", "");
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
Name = ToolBox.GetRandomLine(firstNamePath);
}
string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", "");
string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
@@ -395,18 +408,30 @@ namespace Barotrauma
}
// Used for loading the data
public CharacterInfo(XElement element)
public CharacterInfo(XElement infoElement)
{
ID = idCounter;
idCounter++;
Name = element.GetAttributeString("name", "");
string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant();
File = element.GetAttributeString("file", "");
SourceElement = GetConfig(File).Root;
HasGenders = SourceElement.GetAttributeBool("genders", false);
Salary = element.GetAttributeInt("salary", 1000);
Enum.TryParse(element.GetAttributeString("race", "White"), true, out Race race);
Enum.TryParse(element.GetAttributeString("gender", "None"), true, out Gender gender);
Name = infoElement.GetAttributeString("name", "");
string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant();
Salary = infoElement.GetAttributeInt("salary", 1000);
Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race);
Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender);
_speciesName = infoElement.GetAttributeString("speciesname", null);
XDocument doc = null;
if (_speciesName != null)
{
doc = Character.GetConfigFile(_speciesName);
}
else
{
// Backwards support (human only)
string file = infoElement.GetAttributeString("file", "");
doc = XMLExtensions.TryLoadXml(file);
}
if (doc == null) { return; }
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
if (HasGenders && gender == Gender.None)
{
gender = GetRandomGender();
@@ -416,26 +441,26 @@ namespace Barotrauma
gender = Gender.None;
}
RecreateHead(
element.GetAttributeInt("headspriteid", 1),
infoElement.GetAttributeInt("headspriteid", 1),
race,
gender,
element.GetAttributeInt("hairindex", -1),
element.GetAttributeInt("beardindex", -1),
element.GetAttributeInt("moustacheindex", -1),
element.GetAttributeInt("faceattachmentindex", -1));
infoElement.GetAttributeInt("hairindex", -1),
infoElement.GetAttributeInt("beardindex", -1),
infoElement.GetAttributeInt("moustacheindex", -1),
infoElement.GetAttributeInt("faceattachmentindex", -1));
if (string.IsNullOrEmpty(Name))
{
if (SourceElement.Element("name") != null)
if (CharacterConfigElement.Element("name") != null)
{
string firstNamePath = SourceElement.Element("name").GetAttributeString("firstname", "");
string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", "");
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
Name = ToolBox.GetRandomLine(firstNamePath);
}
string lastNamePath = SourceElement.Element("name").GetAttributeString("lastname", "");
string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
@@ -445,15 +470,14 @@ namespace Barotrauma
}
}
StartItemsGiven = element.GetAttributeBool("startitemsgiven", false);
string personalityName = element.GetAttributeString("personality", "");
ragdollFileName = element.GetAttributeString("ragdoll", string.Empty);
StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
string personalityName = infoElement.GetAttributeString("personality", "");
ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty);
if (!string.IsNullOrEmpty(personalityName))
{
personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName);
}
foreach (XElement subElement in element.Elements())
foreach (XElement subElement in infoElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
Job = new Job(subElement);
@@ -462,20 +486,9 @@ namespace Barotrauma
LoadHeadAttachments();
}
private XDocument GetConfig(string file)
{
if (!cachedConfigs.TryGetValue(file, out XDocument doc))
{
doc = XMLExtensions.TryLoadXml(file);
if (doc == null) { return null; }
cachedConfigs.Add(file, doc);
}
return doc;
}
public int SetRandomHead() => HeadSpriteId = GetRandomHeadID();
public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
public Race GetRandomRace() => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server);
public int GetRandomHeadID() => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, Rand.RandSync.Server) : 0;
@@ -491,7 +504,7 @@ namespace Barotrauma
{
if (wearables == null)
{
var attachments = SourceElement.Element("HeadAttachments");
var attachments = CharacterConfigElement.Element("HeadAttachments");
if (attachments != null)
{
wearables = attachments.Elements("Wearable");
@@ -503,6 +516,7 @@ namespace Barotrauma
public IEnumerable<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> elements, WearableType targetType)
{
if (elements == null) { return elements; }
return elements.Where(e =>
{
if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; }
@@ -522,8 +536,8 @@ namespace Barotrauma
private void CalculateHeadSpriteRange()
{
if (SourceElement == null) { return; }
Head.headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero);
if (CharacterConfigElement == null) { return; }
Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero);
// If range is defined, we use it as it is
// Else we calculate the range from the wearables.
if (Head.headSpriteRange == Vector2.Zero)
@@ -582,11 +596,13 @@ namespace Barotrauma
public void LoadHeadSprite()
{
// TODO: use ragdollparams instead?
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue;
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; }
XElement spriteElement = limbElement.Element("sprite");
if (spriteElement == null) { continue; }
string spritePath = spriteElement.Attribute("texture").Value;
@@ -605,7 +621,7 @@ namespace Barotrauma
}
string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
if (fileWithoutTags != fileName) continue;
if (fileWithoutTags != fileName) { continue; }
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
@@ -788,7 +804,7 @@ namespace Barotrauma
charElement.Add(
new XAttribute("name", Name),
new XAttribute("file", File),
new XAttribute("speciesname", SpeciesName),
new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"),
new XAttribute("race", Head.race.ToString()),
new XAttribute("salary", Salary),
@@ -2,14 +2,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class Affliction
class Affliction : ISerializableEntity
{
public readonly AfflictionPrefab Prefab;
public float Strength;
public string Name => ToString();
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
[Serialize(0f, true), Editable]
public float Strength { get; set; }
[Serialize("", true), Editable]
public string Identifier { get; private set; }
[Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
public float Probability { get; private set; } = 1.0f;
public float DamagePerSecond;
public float DamagePerSecondTimer;
@@ -18,11 +30,6 @@ namespace Barotrauma
public float StrengthDiminishMultiplier = 1.0f;
public Affliction MultiplierSource;
/// <summary>
/// Probability for the affliction to be applied. Used by attacks.
/// </summary>
public float ApplyProbability = 1.0f;
/// <summary>
/// Which character gave this affliction
/// </summary>
@@ -32,6 +39,17 @@ namespace Barotrauma
{
Prefab = prefab;
Strength = strength;
Identifier = prefab?.Identifier;
}
public void Serialize(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
}
public void Deserialize(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public Affliction CreateMultiplied(float multiplier)
@@ -39,10 +57,7 @@ namespace Barotrauma
return Prefab.Instantiate(Strength * multiplier, Source);
}
public override string ToString()
{
return "Affliction (" + Prefab.Name + ")";
}
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
@@ -22,7 +22,7 @@ namespace Barotrauma
{
get { return state; }
}
public AfflictionHusk(AfflictionPrefab prefab, float strength) :
base(prefab, strength)
{
@@ -96,60 +96,27 @@ namespace Barotrauma
}
}
private void ActivateHusk(Character character)
public void ActivateHusk(Character character)
{
character.NeedsAir = false;
if (huskAppendage == null)
{
huskAppendage = AttachHuskAppendage(character);
character.SetStun(0.5f);
}
}
public static List<Limb> AttachHuskAppendage(Character character, Ragdoll ragdoll = null)
{
var huskDoc = XMLExtensions.TryLoadXml(Character.GetConfigFile("humanhusk"));
string pathToAppendage = huskDoc.Root.Element("huskappendage").GetAttributeString("path", string.Empty);
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
if (doc == null || doc.Root == null) { return null; }
if (ragdoll == null)
{
ragdoll = character.AnimController;
}
if (ragdoll.Dir < 1.0f)
{
ragdoll.Flip();
}
var huskAppendages = new List<Limb>();
var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
foreach (var jointElement in doc.Root.Elements("joint"))
{
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement))
huskAppendage = AttachHuskAppendage(character, Prefab.Identifier);
if (huskAppendage != null)
{
JointParams jointParams = new JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = ragdoll.Limbs[jointParams.Limb1];
Limb huskAppendage = new Limb(ragdoll, character, new LimbParams(limbElement, ragdoll.RagdollParams));
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
huskAppendages.Add(huskAppendage);
character.NeedsAir = false;
character.SetStun(0.5f);
}
}
return huskAppendages;
}
private void DeactivateHusk(Character character)
{
character.NeedsAir = true;
RemoveHuskAppendage(character);
}
private void RemoveHuskAppendage(Character character)
{
if (huskAppendage == null) return;
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
huskAppendage = null;
character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
if (huskAppendage != null)
{
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
huskAppendage = null;
}
}
public void Remove(Character character)
@@ -182,7 +149,8 @@ namespace Barotrauma
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
var configFile = Character.GetConfigFile("humanhusk");
string speciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
string configFile = Character.GetConfigFilePath(speciesName);
if (string.IsNullOrEmpty(configFile))
{
@@ -190,16 +158,7 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
//XDocument doc = XMLExtensions.TryLoadXml(configFile);
//if (doc?.Root == null)
//{
// DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk 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, isRemotePlayer: false, hasAi: true);
var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams);
foreach (Limb limb in husk.AnimController.Limbs)
{
@@ -220,7 +179,7 @@ namespace Barotrauma
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
{
string errorMsg = "Failed to move items from a human's inventory into a humanhusk's inventory (inventory sizes don't match)";
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
@@ -234,5 +193,103 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
public static List<Limb> AttachHuskAppendage(Character character, string afflictionIdentifier, XElement appendageDefinition = null, Ragdoll ragdoll = null)
{
var appendage = new List<Limb>();
if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction))
{
DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!");
return appendage;
}
string nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
string huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
string filePath = Character.GetConfigFilePath(huskedSpeciesName);
if (!Character.TryGetConfigFile(filePath, out XDocument huskDoc))
{
DebugConsole.ThrowError($"Error in '{filePath}': Failed to load the config file for the husk infected species with the species name '{huskedSpeciesName}'!");
return appendage;
}
var mainElement = huskDoc.Root.IsOverride() ? huskDoc.Root.FirstElement() : huskDoc.Root;
var element = appendageDefinition;
if (element == null)
{
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier));
}
if (element == null)
{
DebugConsole.ThrowError($"Error in '{filePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!");
return appendage;
}
string pathToAppendage = element.GetAttributeString("path", string.Empty);
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
if (doc == null) { return appendage; }
if (ragdoll == null)
{
ragdoll = character.AnimController;
}
if (ragdoll.Dir < 1.0f)
{
ragdoll.Flip();
}
var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
foreach (var jointElement in doc.Root.Elements("joint"))
{
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement))
{
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is.");
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams)
{
// Ensure that we have a valid id for the new limb
ID = ragdoll.Limbs.Length
};
jointParams.Limb2 = appendageLimbParams.ID;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
else
{
DebugConsole.ThrowError("Attachment limb not found!");
}
}
}
return appendage;
}
public static string GetHuskedSpeciesName(string speciesName, AfflictionPrefabHusk prefab)
{
return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName);
}
public static string GetNonHuskedSpeciesName(string huskedSpeciesName, AfflictionPrefabHusk prefab)
{
string nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag);
return huskedSpeciesName.Remove(nonTag);
}
}
}
@@ -3,11 +3,13 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
public static class CPRSettings
{
public static bool IsLoaded { get; private set; }
public static float ReviveChancePerSkill { get; private set; }
public static float ReviveChanceExponent { get; private set; }
public static float ReviveChanceMin { get; private set; }
@@ -31,9 +33,52 @@ namespace Barotrauma
DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f);
DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f);
IsLoaded = true;
}
}
class AfflictionPrefabHusk : AfflictionPrefab
{
public AfflictionPrefabHusk(XElement element, Type type = null) : base(element, type)
{
HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null);
if (HuskedSpeciesName == null)
{
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
HuskedSpeciesName = "[speciesname]husk";
}
TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true);
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
TargetSpecies = new string[] { "human" };
}
var attachElement = element.GetChildElement("attachlimb");
if (attachElement != null)
{
AttachLimbId = attachElement.GetAttributeInt("id", -1);
AttachLimbName = attachElement.GetAttributeString("name", null);
AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None;
}
else
{
AttachLimbId = -1;
AttachLimbName = null;
AttachLimbType = LimbType.None;
}
}
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
public readonly string HuskedSpeciesName;
public readonly string[] TargetSpecies;
public const string Tag = "[speciesname]";
}
class AfflictionPrefab
{
public class Effect
@@ -126,7 +171,6 @@ namespace Barotrauma
public static AfflictionPrefab Bloodloss;
public static AfflictionPrefab Pressure;
public static AfflictionPrefab Stun;
public static AfflictionPrefab Husk;
public static List<AfflictionPrefab> List = new List<AfflictionPrefab>();
@@ -187,44 +231,109 @@ namespace Barotrauma
foreach (string filePath in filePaths)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
foreach (XElement element in doc.Root.Elements())
if (doc == null) { continue; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
switch (element.Name.ToString().ToLowerInvariant())
DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one.");
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{filePath}'");
continue;
}
var duplicate = List.FirstOrDefault(a => a.Identifier == identifier);
if (duplicate != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{filePath}'", Color.Yellow);
List.Remove(duplicate);
}
else
{
DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{filePath}'");
continue;
}
}
}
string type = sourceElement.GetAttributeString("type", null);
if (sourceElement.Name.ToString().ToLowerInvariant() == "cprsettings")
{
//backwards compatibility
type = "cprsettings";
}
AfflictionPrefab prefab = null;
switch (type)
{
case "internaldamage":
List.Add(InternalDamage = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "bleeding":
List.Add(Bleeding = new AfflictionPrefab(element, typeof(AfflictionBleeding)));
prefab = new AfflictionPrefab(sourceElement, 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)));
case "huskinfection":
prefab = new AfflictionPrefabHusk(sourceElement, typeof(AfflictionHusk));
break;
case "cprsettings":
CPRSettings.Load(element);
if (CPRSettings.IsLoaded)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding the CPR settings with '{filePath}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{filePath}': CPR settings already loaded. Add <override></override> tags as the parent of the custom CPRSettings to allow overriding the vanilla values.");
break;
}
}
CPRSettings.Load(sourceElement);
break;
case "damage":
case "burn":
case "oxygenlow":
case "bloodloss":
case "stun":
case "pressure":
case "internaldamage":
prefab = new AfflictionPrefab(sourceElement, typeof(Affliction));
break;
default:
List.Add(new AfflictionPrefab(element));
prefab = new AfflictionPrefab(sourceElement);
break;
}
switch (identifier)
{
case "internaldamage":
InternalDamage = prefab;
break;
case "bleeding":
Bleeding = prefab;
break;
case "burn":
Burn = prefab;
break;
case "oxygenlow":
OxygenLow = prefab;
break;
case "bloodloss":
Bloodloss = prefab;
break;
case "pressure":
Pressure = prefab;
break;
case "stun":
Stun = prefab;
break;
}
if (prefab != null) { List.Add(prefab); }
}
}
@@ -235,12 +344,15 @@ namespace Barotrauma
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;
if (typeName == "InternalDamage" && type == null)
{
type = typeof(Affliction);
}
Identifier = element.GetAttributeString("identifier", "");
@@ -305,7 +417,7 @@ namespace Barotrauma
catch
{
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
return;
type = typeof(Affliction);
}
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -78,14 +79,33 @@ namespace Barotrauma
public const float InsufficientOxygenThreshold = 30.0f;
public const float LowOxygenThreshold = 50.0f;
protected float minVitality, maxVitality;
protected float minVitality;
protected float maxVitality
{
get => Character.Params.Health.Vitality;
set => Character.Params.Health.Vitality = value;
}
public bool Unkillable;
//bleeding settings
public bool DoesBleed { get; private set; }
public bool DoesBleed
{
get => Character.Params.Health.DoesBleed;
private set => Character.Params.Health.DoesBleed = value;
}
public bool UseHealthWindow { get; set; }
public bool UseHealthWindow
{
get => Character.Params.Health.UseHealthWindow;
set => Character.Params.Health.UseHealthWindow = value;
}
public float CrushDepth
{
get => Character.Params.Health.CrushDepth;
private set => Character.Params.Health.CrushDepth = value;
}
private List<LimbHealth> limbHealths = new List<LimbHealth>();
//non-limb-specific afflictions
@@ -102,11 +122,12 @@ namespace Barotrauma
get { return Vitality <= 0.0f; }
}
public float CrushDepth { get; private set; }
public float PressureKillDelay { get; private set; } = 5.0f;
public float Vitality { get; private set; }
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
public float MaxVitality
{
get
@@ -168,7 +189,6 @@ namespace Barotrauma
{
this.Character = character;
Vitality = 100.0f;
maxVitality = 100.0f;
DoesBleed = true;
UseHealthWindow = false;
@@ -185,15 +205,9 @@ namespace Barotrauma
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;
minVitality = character.IsHuman ? -100.0f : 0.0f;
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
@@ -225,10 +239,9 @@ namespace Barotrauma
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter = null)
{
// TODO: If there can be duplicates, we should use Union instead.
return limbHealthFilter == null
? afflictions.Concat(limbHealths.SelectMany(lh => lh.Afflictions))
: afflictions.Concat(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter)));
? afflictions.Union(limbHealths.SelectMany(lh => lh.Afflictions))
: afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter)));
}
private LimbHealth GetMatchingLimbHealth(Limb limb) => limbHealths[limb.HealthIndex];
@@ -240,11 +253,23 @@ namespace Barotrauma
private IEnumerable<Affliction> GetMatchingAfflictions(LimbHealth limb, Func<Affliction, bool> predicate)
=> limb.Afflictions.Where(predicate).Union(afflictions.Where(a => predicate(a) && GetMatchingLimbHealth(a) == limb));
public Affliction GetAffliction(string afflictionType, bool allowLimbAfflictions = true)
public IEnumerable<Affliction> GetAfflictionsByType(string afflictionType, bool allowLimbAfflictions = true)
{
if (allowLimbAfflictions)
{
return GetAllAfflictions(a => a.Prefab.AfflictionType == afflictionType);
}
else
{
return afflictions.Where(a => a.Prefab.AfflictionType == afflictionType);
}
}
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
if (affliction.Prefab.Identifier == identifier) return affliction;
}
if (!allowLimbAfflictions) return null;
@@ -252,19 +277,30 @@ namespace Barotrauma
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
if (affliction.Prefab.Identifier == identifier) return affliction;
}
}
return null;
}
public T GetAffliction<T>(string afflictionType, bool allowLimbAfflictions = true) where T : Affliction
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(afflictionType, allowLimbAfflictions) as T;
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public Affliction GetAffliction(string afflictionType, Limb limb)
public IEnumerable<Affliction> GetAfflictionsByType(string afflictionType, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return null;
}
return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType);
}
public Affliction GetAffliction(string identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
@@ -274,7 +310,7 @@ namespace Barotrauma
}
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
{
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
if (affliction.Prefab.Identifier == identifier) return affliction;
}
return null;
}
@@ -520,8 +556,14 @@ namespace Barotrauma
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
// Currently only human can get the husk infection.
if (newAffliction.Prefab == AfflictionPrefab.Husk && Character.SpeciesName.ToLowerInvariant() != "human") { return; }
if (newAffliction.Prefab.AfflictionType == "huskinfection")
{
var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk;
if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
return;
}
}
foreach (Affliction affliction in afflictions)
{
if (newAffliction.Prefab == affliction.Prefab)
@@ -547,7 +589,10 @@ namespace Barotrauma
CalculateVitality();
if (Vitality <= MinVitality) Kill();
}
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
@@ -671,6 +716,10 @@ namespace Barotrauma
var causeOfDeath = GetCauseOfDeath();
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
#if CLIENT
DisplayVitalityDelay = 0.0f;
DisplayedVitality = Vitality;
#endif
}
public Pair<CauseOfDeathType, Affliction> GetCauseOfDeath()
@@ -1,86 +1,120 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class DamageModifier
partial class DamageModifier : ISerializableEntity
{
[Serialize(1.0f, false)]
public string Name => "Damage Modifier";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
[Serialize(1.0f, false), Editable(DecimalCount = 2)]
public float DamageMultiplier
{
get;
private set;
}
[Serialize("0.0,360", false)]
[Serialize("0.0,360", false), Editable]
public Vector2 ArmorSector
{
get;
private set;
}
[Serialize(true, false)]
public bool IsArmor
{
get;
private set;
}
public Vector2 ArmorSectorInRadians => new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
[Serialize(false, false)]
[Serialize(false, false), Editable]
public bool DeflectProjectiles
{
get;
private set;
}
public string[] AfflictionIdentifiers
[Serialize("", true), Editable]
public string AfflictionIdentifiers
{
get;
private set;
get
{
return rawAfflictionIdentifierString;
}
private set
{
rawAfflictionIdentifierString = value;
ParseAfflictionIdentifiers();
}
}
public string[] AfflictionTypes
[Serialize("", true), Editable]
public string AfflictionTypes
{
get;
private set;
get
{
return rawAfflictionTypeString;
}
private set
{
rawAfflictionTypeString = value;
ParseAfflictionTypes();
}
}
private string rawAfflictionIdentifierString;
private string rawAfflictionTypeString;
private string[] parsedAfflictionIdentifiers;
private string[] parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
ArmorSector = new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
Deserialize(element);
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++)
private void ParseAfflictionTypes()
{
string[] splitValue = rawAfflictionTypeString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
AfflictionIdentifiers[i] = AfflictionIdentifiers[i].ToLowerInvariant();
splitValue[i] = splitValue[i].ToLowerInvariant().Trim();
}
AfflictionTypes = element.GetAttributeStringArray("afflictiontypes", new string[0]);
for (int i = 0; i < AfflictionTypes.Length; i++)
parsedAfflictionTypes = splitValue;
}
private void ParseAfflictionIdentifiers()
{
string[] splitValue = rawAfflictionIdentifierString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
AfflictionTypes[i] = AfflictionTypes[i].ToLowerInvariant();
splitValue[i] = splitValue[i].ToLowerInvariant().Trim();
}
parsedAfflictionIdentifiers = splitValue;
}
public bool MatchesAffliction(Affliction affliction)
{
//if no identifiers or types have been defined, the damage modifier affects all afflictions
if (AfflictionIdentifiers.Length == 0 && AfflictionTypes.Length == 0) { return true; }
return parsedAfflictionIdentifiers.Any(id => id.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase))
|| parsedAfflictionTypes.Any(t => t.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase));
}
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;
public void Serialize(XElement element)
{
if (element == null) { return; }
SerializableProperty.SerializeProperties(this, element);
}
public void Deserialize(XElement element)
{
if (element == null) { return; }
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
}
@@ -50,38 +50,25 @@ namespace Barotrauma
public Job(XElement element)
{
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
prefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == identifier);
string name = "";
if (prefab == null)
if (!JobPrefab.List.TryGetValue(identifier, out JobPrefab p))
{
name = element.GetAttributeString("name", "").ToLowerInvariant();
prefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == name);
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
p = JobPrefab.Random();
}
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)];
}
prefab = p;
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "skill") continue;
if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) continue;
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
skillIdentifier,
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
}
}
public static Job Random(Rand.RandSync randSync)
{
JobPrefab prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count - 1, randSync)];
return new Job(prefab);
}
public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced) => new Job(JobPrefab.Random(randSync));
public float GetSkillLevel(string skillIdentifier)
{
@@ -186,7 +173,7 @@ namespace Barotrauma
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null) parentItem.Combine(item);
if (parentItem != null) parentItem.Combine(item, user: null);
foreach (XElement childItemElement in itemElement.Elements())
{
@@ -2,18 +2,26 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Linq;
namespace Barotrauma
{
public class AutonomousObjective
{
public string aiTag;
public string identifier;
public string option;
public float priorityModifier;
public AutonomousObjective(XElement element)
{
aiTag = element.GetAttributeString("aitag", null);
identifier = element.GetAttributeString("identifier", null);
//backwards compatibility
if (string.IsNullOrEmpty(identifier))
{
identifier = element.GetAttributeString("aitag", null);
}
option = element.GetAttributeString("option", null);
priorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
priorityModifier = MathHelper.Max(priorityModifier, 0);
@@ -22,13 +30,32 @@ namespace Barotrauma
partial class JobPrefab
{
public static List<JobPrefab> List;
public static Dictionary<string, JobPrefab> List;
public static JobPrefab Get(string identifier)
{
if (List == null)
{
DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded.");
return null;
}
if (List.TryGetValue(identifier, out JobPrefab job))
{
return job;
}
else
{
DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier);
return null;
}
}
public readonly XElement Items;
public readonly List<string> ItemNames = new List<string>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutomaticOrders = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
[Serialize("1,1,1,1", false)]
public Color UIColor
{
@@ -126,6 +153,7 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, element);
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier);
Identifier = Identifier.ToLowerInvariant();
foreach (XElement subElement in element.Elements())
{
@@ -133,35 +161,7 @@ namespace Barotrauma
{
case "items":
Items = subElement;
foreach (XElement itemElement in subElement.Elements())
{
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);
}
}
}
loadItemNames(subElement);
break;
case "skills":
foreach (XElement skillElement in subElement.Elements())
@@ -172,6 +172,44 @@ namespace Barotrauma
case "autonomousobjectives":
subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order)));
break;
case "appropriateobjectives":
case "appropriateorders":
subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant()));
break;
}
}
void loadItemNames(XElement parentElement)
{
foreach (XElement itemElement in parentElement.Elements())
{
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);
}
}
loadItemNames(itemElement);
}
}
@@ -184,24 +222,45 @@ namespace Barotrauma
}
}
public static JobPrefab Random()
{
return List[Rand.Int(List.Count)];
}
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => List.Values.GetRandom(sync);
public static void LoadAll(IEnumerable<string> filePaths)
{
List = new List<JobPrefab>();
List = new Dictionary<string, JobPrefab>();
foreach (string filePath in filePaths)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) return;
foreach (XElement element in doc.Root.Elements())
if (doc == null) { continue; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
JobPrefab job = new JobPrefab(element);
List.Add(job);
DebugConsole.ThrowError($"Error in '{filePath}': Cannot override all job prefabs, because many of them are required by the main game! Please try overriding jobs one by one.");
}
foreach (XElement element in mainElement.Elements())
{
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement());
if (List.TryGetValue(job.Identifier, out JobPrefab duplicate))
{
DebugConsole.NewMessage($"Overriding the job '{duplicate.Identifier}' with another defined in '{filePath}'", Color.Yellow);
List.Remove(duplicate.Identifier);
}
List.Add(job.Identifier, job);
}
else
{
if (List.TryGetValue(element.GetAttributeString("identifier", "").ToLowerInvariant(), out JobPrefab duplicate))
{
DebugConsole.ThrowError($"Error in '{filePath}': Duplicate job definition found for: '{duplicate.Identifier}'. Use the <override> XML element as the parent of job element's definition to override the existing job.");
}
else
{
var job = new JobPrefab(element);
List.Add(job.Identifier, job);
}
}
}
}
}
@@ -9,6 +9,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
using JointParams = Barotrauma.RagdollParams.JointParams;
namespace Barotrauma
{
@@ -21,14 +23,14 @@ namespace Barotrauma
partial class LimbJoint : RevoluteJoint
{
public bool IsSevered;
public bool CanBeSevered => jointParams.CanBeSevered;
public readonly JointParams jointParams;
public bool CanBeSevered => Params.CanBeSevered;
public readonly JointParams Params;
public readonly Ragdoll ragdoll;
public readonly Limb LimbA, LimbB;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One)
{
this.jointParams = jointParams;
Params = jointParams;
this.ragdoll = ragdoll;
LoadParams();
}
@@ -43,63 +45,37 @@ namespace Barotrauma
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))
MaxMotorTorque = Params.Stiffness;
LimitEnabled = Params.LimitEnabled;
if (float.IsNaN(Params.LowerLimit))
{
jointParams.LowerLimit = 0;
Params.LowerLimit = 0;
}
if (float.IsNaN(jointParams.UpperLimit))
if (float.IsNaN(Params.UpperLimit))
{
jointParams.UpperLimit = 0;
Params.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);
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Params.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Params.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
}
else
{
LocalAnchorA = ConvertUnits.ToSimUnits(jointParams.Limb1Anchor * jointParams.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(jointParams.Limb2Anchor * jointParams.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(jointParams.UpperLimit);
LowerLimit = MathHelper.ToRadians(jointParams.LowerLimit);
LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Params.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Params.Ragdoll.JointScale);
UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
}
}
}
partial class Limb : ISerializableEntity, ISpatialEntity
{
// Note: not used
private const float LimbDensity = 15;
private const float LimbAngularDamping = 7;
//how long it takes for severed limbs to fade out
private const float SeveredFadeOutTime = 10.0f;
@@ -108,12 +84,12 @@ namespace Barotrauma
/// 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;
public readonly LimbParams Params;
//the physics body of the limb
public PhysicsBody body;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(limbParams.StepOffset) * ragdoll.RagdollParams.JointScale;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public bool inWater;
@@ -125,19 +101,34 @@ namespace Barotrauma
private bool isSevered;
private float severedFadeOutTimer;
public Vector2? MouthPos;
private Vector2? mouthPos;
public Vector2 MouthPos
{
get
{
if (!mouthPos.HasValue)
{
mouthPos = Params.MouthPos;
}
return mouthPos.Value;
}
set
{
mouthPos = value;
}
}
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 int HealthIndex => Params.HealthIndex;
public float Scale => Params.Ragdoll.LimbScale;
public float AttackPriority => Params.AttackPriority;
public bool DoesFlip => Params.Flip;
public float SteerForce => Params.SteerForce;
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
@@ -198,7 +189,7 @@ namespace Barotrauma
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
}
public int RefJointIndex => limbParams.RefJoint;
public int RefJointIndex => Params.RefJoint;
private List<WearableSprite> wearingItems;
public List<WearableSprite> WearingItems
@@ -291,7 +282,7 @@ namespace Barotrauma
get { return pullJoint.LocalAnchorA; }
}
public string Name => limbParams.Name;
public string Name => Params.Name;
public Dictionary<string, SerializableProperty> SerializableProperties
{
@@ -303,7 +294,7 @@ namespace Barotrauma
{
this.ragdoll = ragdoll;
this.character = character;
this.limbParams = limbParams;
this.Params = limbParams;
wearingItems = new List<WearableSprite>();
dir = Direction.Right;
body = new PhysicsBody(limbParams);
@@ -324,19 +315,16 @@ namespace Barotrauma
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
//MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass
// 150 or even 400 is too low if the joint is used for moving the character position from the mainlimb towards the collider position
MaxForce = 1000 * Mass
};
GameMain.World.AddJoint(pullJoint);
var element = limbParams.Element;
if (element.Attribute("mouthpos") != null)
{
MouthPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("mouthpos", Vector2.Zero));
}
body.BodyType = BodyType.Dynamic;
body.FarseerBody.AngularDamping = LimbAngularDamping;
damageModifiers = new List<DamageModifier>();
@@ -403,19 +391,19 @@ namespace Barotrauma
return AddDamage(simPosition, afflictions, playSound);
}
public AttackResult AddDamage(Vector2 simPosition, List<Affliction> afflictions, bool playSound)
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound)
{
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
afflictions = new List<Affliction>(afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.ApplyProbability));
for (int i = 0; i < afflictions.Count; i++)
var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList();
for (int i = 0; i < afflictionsCopy.Count; i++)
{
foreach (DamageModifier damageModifier in damageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
if (SectorHit(damageModifier.ArmorSector, simPosition))
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
@@ -424,19 +412,19 @@ namespace Barotrauma
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
if (SectorHit(damageModifier.ArmorSector, simPosition))
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
}
}
AddDamageProjSpecific(simPosition, afflictions, playSound, appliedDamageModifiers);
AddDamageProjSpecific(simPosition, afflictionsCopy, playSound, appliedDamageModifiers);
return new AttackResult(afflictions, this, appliedDamageModifiers);
return new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
}
partial void AddDamageProjSpecific(Vector2 simPosition, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
@@ -456,7 +444,7 @@ namespace Barotrauma
protected float GetArmorSectorRotationOffset(Vector2 armorSector)
{
float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y);
float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation);
float spritesheetOrientation = Params.GetSpriteOrientation();
return midAngle + spritesheetOrientation;
}
@@ -495,10 +483,13 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
private readonly List<Body> contactBodies = new List<Body>();
private List<Body> ignoredBodies;
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary>
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1)
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default(AttackResult);
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos));
@@ -514,8 +505,11 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
List<Body> ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
if (ignoredBodies == null)
{
ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
}
structureBody = Submarine.PickBody(
SimPosition, attackSimPos,
@@ -541,46 +535,42 @@ namespace Barotrauma
}
break;
case HitDetection.Contact:
var targetBodies = new List<Body>();
contactBodies.Clear();
if (damageTarget is Character targetCharacter)
{
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (!limb.IsSevered && limb.body?.FarseerBody != null) targetBodies.Add(limb.body.FarseerBody);
if (!limb.IsSevered && limb.body?.FarseerBody != null) contactBodies.Add(limb.body.FarseerBody);
}
}
else if (damageTarget is Structure targetStructure)
{
if (character.Submarine == null && targetStructure.Submarine != null)
{
targetBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody);
contactBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody);
}
else
{
targetBodies.AddRange(targetStructure.Bodies);
contactBodies.AddRange(targetStructure.Bodies);
}
}
else if (damageTarget is Item)
{
Item targetItem = damageTarget as Item;
if (targetItem.body?.FarseerBody != null) targetBodies.Add(targetItem.body.FarseerBody);
if (targetItem.body?.FarseerBody != null) contactBodies.Add(targetItem.body.FarseerBody);
}
if (targetBodies != null)
ContactEdge contactEdge = body.FarseerBody.ContactList;
while (contactEdge != null)
{
ContactEdge contactEdge = body.FarseerBody.ContactList;
while (contactEdge != null)
if (contactEdge.Contact != null &&
contactEdge.Contact.IsTouching &&
contactBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body))
{
if (contactEdge.Contact != null &&
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;
structureBody = contactBodies.LastOrDefault();
wasHit = true;
break;
}
contactEdge = contactEdge.Next;
}
break;
}
@@ -601,11 +591,18 @@ namespace Barotrauma
LastAttackSoundTime = SoundInterval;
}
#endif
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound);
if (damageTarget is Character targetCharacter && targetLimb != null)
{
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound);
}
else
{
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound);
}
if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
{
// TODO: use the hit pos?
var localFront = body.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation));
var localFront = body.GetLocalFront(Params.GetSpriteOrientation());
var from = body.FarseerBody.GetWorldPoint(localFront);
var to = from;
var drawPos = body.DrawPosition;
@@ -665,7 +662,7 @@ namespace Barotrauma
{
PhysicsBody mainLimbBody = ragdoll.MainLimb.body;
Body colliderBody = ragdoll.Collider.FarseerBody;
Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation));
Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(ragdoll.MainLimb.Params.GetSpriteOrientation());
if (Dir < 0)
{
mainLimbLocalFront.X = -mainLimbLocalFront.X;
@@ -715,8 +712,7 @@ namespace Barotrauma
public void LoadParams()
{
attack?.Deserialize();
pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(limbParams.PullPos * Scale);
pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.PullPos * Scale);
LoadParamsProjSpecific();
}
@@ -20,30 +20,42 @@ namespace Barotrauma
abstract class GroundedMovementParams : AnimationParams
{
[Serialize("1.0, 1.0", true), Editable(DecimalCount = 2, ToolTip = "How big steps the character takes.")]
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2)]
public Vector2 StepSize
{
get;
set;
}
[Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's head is positioned.")]
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2)]
public float HeadPosition { get; set; }
[Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's torso is positioned.")]
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2)]
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.")]
[Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
public float StepLiftHeadMultiplier { get; set; }
[Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
public float StepLiftAmount { get; set; }
[Serialize(-0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
public float StepLiftOffset { get; set; }
[Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
public float StepLiftFrequency { get; set; }
[Serialize(0.75f, true, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
public float BackwardsMovementMultiplier { get; set; }
}
abstract class SwimParams : AnimationParams
{
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerTorque { get; set; }
}
abstract class AnimationParams : EditableParams
abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
{
public string SpeciesName { get; private set; }
public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run;
@@ -51,11 +63,11 @@ namespace Barotrauma
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
[Serialize(1.0f, true), Editable(DecimalCount = 2)]
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED)]
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)")]
[Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float CycleSpeed { get; set; }
/// <summary>
@@ -101,8 +113,13 @@ namespace Barotrauma
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
string configFilePath = Character.GetConfigFile(speciesName, contentPackage);
var folder = XMLExtensions.TryLoadXml(configFilePath)?.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
string configFilePath = Character.GetConfigFilePath(speciesName, contentPackage);
if (!Character.TryGetConfigFile(configFilePath, out XDocument configFile))
{
DebugConsole.ThrowError($"Failed to load config file: {configFilePath} for '{speciesName}'");
return string.Empty;
}
var folder = configFile.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
{
folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Animations");
@@ -197,9 +214,10 @@ namespace Barotrauma
T a = new T();
if (a.Load(selectedFile, speciesName))
{
if (!anims.ContainsKey(a.Name))
fileName = Path.GetFileNameWithoutExtension(selectedFile);
if (!anims.ContainsKey(fileName))
{
anims.Add(a.Name, a);
anims.Add(fileName, a);
}
}
else
@@ -211,6 +229,8 @@ namespace Barotrauma
return (T)anim;
}
public static void ClearCache() => allAnimations.Clear();
public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type)
{
if (type == typeof(HumanWalkParams))
@@ -275,11 +295,14 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(animationElement);
instance.Save();
instance.Load(fullPath, speciesName);
anims.Add(instance.Name, instance);
anims.Add(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance as T;
}
public bool Serialize() => base.Serialize();
public bool Deserialize() => base.Deserialize();
protected bool Load(string file, string speciesName)
{
if (Load(file))
@@ -380,14 +403,16 @@ namespace Barotrauma
}
#region Memento
protected void CreateSnapshot<T>() where T : AnimationParams, new()
public Memento<AnimationParams> Memento { get; protected set; } = new Memento<AnimationParams>();
public abstract void StoreSnapshot();
protected void StoreSnapshot<T>() where T : AnimationParams, new()
{
Serialize();
if (doc == null)
{
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!");
return;
}
Serialize();
var copy = new T
{
IsLoaded = true,
@@ -395,10 +420,11 @@ namespace Barotrauma
};
copy.Deserialize();
copy.Serialize();
memento.Store(copy);
Memento.Store(copy);
}
public override void Undo() => Deserialize(memento.Undo().MainElement);
public override void Redo() => Deserialize(memento.Redo().MainElement);
public void Undo() => Deserialize(Memento.Undo().MainElement);
public void Redo() => Deserialize(Memento.Redo().MainElement);
public void ClearHistory() => Memento.Clear();
#endregion
}
}
@@ -16,7 +16,7 @@ namespace Barotrauma
protected static FishWalkParams Empty = new FishWalkParams();
public override void CreateSnapshot() => CreateSnapshot<FishWalkParams>();
public override void StoreSnapshot() => StoreSnapshot<FishWalkParams>();
}
class FishRunParams : FishGroundedParams
@@ -32,7 +32,7 @@ namespace Barotrauma
protected static FishRunParams Empty = new FishRunParams();
public override void CreateSnapshot() => CreateSnapshot<FishRunParams>();
public override void StoreSnapshot() => StoreSnapshot<FishRunParams>();
}
class FishSwimFastParams : FishSwimParams
@@ -43,7 +43,7 @@ namespace Barotrauma
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
}
public override void CreateSnapshot() => CreateSnapshot<FishSwimFastParams>();
public override void StoreSnapshot() => StoreSnapshot<FishSwimFastParams>();
}
class FishSwimSlowParams : FishSwimParams
@@ -54,7 +54,7 @@ namespace Barotrauma
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
}
public override void CreateSnapshot() => CreateSnapshot<FishSwimSlowParams>();
public override void StoreSnapshot() => StoreSnapshot<FishSwimSlowParams>();
}
abstract class FishGroundedParams : GroundedMovementParams, IFishAnimation
@@ -69,38 +69,38 @@ namespace Barotrauma
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.")]
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the head to the correct position.")]
[Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float HeadMoveForce { get; set; }
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the torso to the correct position.")]
[Serialize(10.0f, true, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float TorsoMoveForce { get; set; }
[Serialize(8.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")]
[Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveForce { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")]
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")]
[Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TorsoTorque { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")]
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TailTorque { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float FootTorque { get; set; }
[Serialize(0.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "Optional torque that's constantly applied to legs.")]
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float LegTorque { get; set; }
/// <summary>
/// The angle of the collider when standing (i.e. out of water).
/// In degrees.
/// </summary>
[Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "The angle of the character's collider when standing.")]
[Serialize(0f, true, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
public float ColliderStandAngle
{
get => MathHelper.ToDegrees(ColliderStandAngleInRadians);
@@ -140,13 +140,13 @@ namespace Barotrauma
abstract class FishSwimParams : SwimParams, IFishAnimation
{
[Serialize(false, true), Editable(ToolTip = "TODO")]
[Serialize(false, true, description: "TODO"), Editable]
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.")]
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(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.")]
[Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
public bool Mirror { get; set; }
[Serialize(1f, true), Editable]
@@ -155,19 +155,19 @@ namespace Barotrauma
[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.")]
[Editable, Serialize(true, true, description: "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.")]
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")]
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")]
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TailTorque { get; set; }
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float FootTorque { get; set; }
[Serialize(null, true), Editable]
@@ -10,7 +10,7 @@ namespace Barotrauma
return GetAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk, fileName);
}
public override void CreateSnapshot() => CreateSnapshot<HumanWalkParams>();
public override void StoreSnapshot() => StoreSnapshot<HumanWalkParams>();
}
class HumanRunParams : HumanGroundedParams
@@ -21,7 +21,7 @@ namespace Barotrauma
return GetAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run, fileName);
}
public override void CreateSnapshot() => CreateSnapshot<HumanRunParams>();
public override void StoreSnapshot() => StoreSnapshot<HumanRunParams>();
}
class HumanSwimFastParams: HumanSwimParams
@@ -33,7 +33,7 @@ namespace Barotrauma
}
public override void CreateSnapshot() => CreateSnapshot<HumanSwimFastParams>();
public override void StoreSnapshot() => StoreSnapshot<HumanSwimFastParams>();
}
class HumanSwimSlowParams : HumanSwimParams
@@ -44,7 +44,7 @@ namespace Barotrauma
return GetAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
}
public override void CreateSnapshot() => CreateSnapshot<HumanSwimSlowParams>();
public override void StoreSnapshot() => StoreSnapshot<HumanSwimSlowParams>();
}
abstract class HumanSwimParams : SwimParams, IHumanAnimation
@@ -81,44 +81,44 @@ namespace Barotrauma
}
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.")]
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootRotateStrength { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
{
[Serialize(0.3f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2, ToolTip = "How much force is used to force the character upright.")]
[Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float GetUpForce { get; set; }
// -- TODO: use a separate clip for crawling -> replace these when implemented.
[Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the torso when crouching.")]
[Serialize(0.65f, true, description: "Height of the torso when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)]
public float CrouchingTorsoPos { get; set; }
[Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the head when crouching.")]
[Serialize(0.65f, true, description: "Height of the head when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)]
public float CrouchingHeadPos { get; set; }
/// <summary>
/// In degrees
/// </summary>
[Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the torso when crouching.")]
[Serialize(-10f, true, description: "Angle of the torso when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
public float CrouchingTorsoAngle { get; set; }
/// <summary>
/// In degrees
/// </summary>
[Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the head when crouching.")]
[Serialize(-10f, true, description: "Angle of the head when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)]
public float CrouchingHeadAngle { get; set; }
// --
[Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's head leans forwards when moving.")]
[Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)]
public float HeadLeanAmount { get; set; }
[Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's torso leans forwards when moving.")]
[Serialize(0.25f, true, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)]
public float TorsoLeanAmount { get; set; }
[Serialize(15.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")]
[Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveStrength { get; set; }
/// <summary>
@@ -135,28 +135,28 @@ namespace Barotrauma
}
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.")]
[Serialize(20.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootRotateStrength { get; set; }
[Serialize("0.0, 0.0", true), 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.")]
[Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)]
public Vector2 FootMoveOffset { get; set; }
[Serialize("0.0, 0.0", true), 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.")]
[Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)]
public Vector2 CrouchingFootMoveOffset { get; set; }
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to bend the characters legs when taking a step.")]
[Serialize(10.0f, true, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float LegBendTorque { get; set; }
[Serialize("0.4, 0.15", true), Editable(DecimalCount = 2, ToolTip = "How much the hands move along each axis.")]
[Serialize("0.4, 0.15", true, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize("-0.15, 0.0", true), 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.")]
[Serialize("-0.15, 0.0", true, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
[Serialize(0.7f, true), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2, ToolTip = "How much force is used to move the hands.")]
[Serialize(0.7f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(-1.0f, true), Editable(DecimalCount = 2, ToolTip = "The position of the hands is clamped below this (relative to the position of the character's torso).")]
[Serialize(-1.0f, true, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)]
public float HandClampY { get; set; }
}
@@ -0,0 +1,602 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml;
using System.Linq;
using Barotrauma.Extensions;
#if CLIENT
using SoundType = Barotrauma.CharacterSound.SoundType;
#endif
namespace Barotrauma
{
/// <summary>
/// Contains character data that should be editable in the character editor.
/// </summary>
class CharacterParams : EditableParams
{
[Serialize("", true), Editable]
public string SpeciesName { get; private set; }
[Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
public string Group { get; private set; }
[Serialize(false, true), Editable]
public bool Humanoid { get; private set; }
[Serialize(false, true), Editable]
public bool Husk { get; private set; }
[Serialize(false, true), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, true), Editable]
public bool CanSpeak { get; set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Noise { get; set; }
[Serialize("blood", true), Editable]
public string BloodDecal { get; private set; }
public readonly string File;
public readonly List<SubParam> SubParams = new List<SubParam>();
public readonly List<SoundParams> Sounds = new List<SoundParams>();
public readonly List<ParticleParams> BloodEmitters = new List<ParticleParams>();
public readonly List<ParticleParams> GibEmitters = new List<ParticleParams>();
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
public HealthParams Health { get; private set; }
public AIParams AI { get; private set; }
public CharacterParams(string file)
{
File = file;
Load();
}
protected override string GetName() => "Character Config File";
public override XElement MainElement => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
public bool Load()
{
bool success = base.Load(File);
if (string.IsNullOrEmpty(SpeciesName) && MainElement != null)
{
//backwards compatibility
SpeciesName = MainElement.GetAttributeString("name", "");
}
CreateSubParams();
return success;
}
public bool Save(string fileNameWithoutExtension = null)
{
Serialize();
return base.Save(fileNameWithoutExtension, new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineOnAttributes = false
});
}
public override bool Reset(bool forceReload = false)
{
if (forceReload)
{
return Load();
}
Deserialize(OriginalElement, alsoChildren: true);
SubParams.ForEach(sp => sp.Reset());
return true;
}
public bool CompareGroup(string group) => !string.IsNullOrWhiteSpace(group) && !string.IsNullOrWhiteSpace(Group) && group.Equals(Group, StringComparison.OrdinalIgnoreCase);
protected void CreateSubParams()
{
SubParams.Clear();
var health = MainElement.GetChildElement("health");
if (health != null)
{
Health = new HealthParams(health, this);
SubParams.Add(Health);
}
// TODO: support for multiple ai elements?
var ai = MainElement.GetChildElement("ai");
if (ai != null)
{
AI = new AIParams(ai, this);
SubParams.Add(AI);
}
foreach (var element in MainElement.GetChildElements("bloodemitter"))
{
var emitter = new ParticleParams(element, this);
BloodEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var element in MainElement.GetChildElements("gibemitter"))
{
var emitter = new ParticleParams(element, this);
GibEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var soundElement in MainElement.GetChildElements("sound"))
{
var sound = new SoundParams(soundElement, this);
Sounds.Add(sound);
SubParams.Add(sound);
}
foreach (var inventoryElement in MainElement.GetChildElements("inventory"))
{
var inventory = new InventoryParams(inventoryElement, this);
Inventories.Add(inventory);
SubParams.Add(inventory);
}
}
public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
{
if (base.Deserialize(element))
{
//backwards compatibility
if (string.IsNullOrEmpty(SpeciesName))
{
SpeciesName = element.GetAttributeString("name", "[NAME NOT GIVEN]");
}
if (alsoChildren)
{
SubParams.ForEach(p => p.Deserialize(recursive));
}
return true;
}
return false;
}
public bool Serialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
{
if (base.Serialize(element))
{
if (alsoChildren)
{
SubParams.ForEach(p => p.Serialize(recursive));
}
return true;
}
return false;
}
#if CLIENT
public void AddToEditor(ParamsEditor editor, bool alsoChildren = true, bool recursive = true, int space = 0)
{
base.AddToEditor(editor);
if (alsoChildren)
{
SubParams.ForEach(s => s.AddToEditor(editor, recursive));
}
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
{
CanBeFocused = false
};
}
}
#endif
public bool AddSound() => TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds);
public void AddInventory() => TryAddSubParam(new XElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories);
public void AddBloodEmitter() => AddEmitter("bloodemitter");
public void AddGibEmitter() => AddEmitter("gibemitter");
private void AddEmitter(string type)
{
switch (type)
{
case "gibemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters);
break;
case "bloodemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters);
break;
default: throw new NotImplementedException(type);
}
}
public bool RemoveSound(SoundParams soundParams) => RemoveSubParam(soundParams);
public bool RemoveBloodEmitter(ParticleParams emitter) => RemoveSubParam(emitter, BloodEmitters);
public bool RemoveGibEmitter(ParticleParams emitter) => RemoveSubParam(emitter, GibEmitters);
public bool RemoveInventory(InventoryParams inventory) => RemoveSubParam(inventory, Inventories);
protected bool RemoveSubParam<T>(T subParam, IList<T> collection = null) where T : SubParam
{
if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; }
if (collection != null && !collection.Contains(subParam)) { return false; }
if (!SubParams.Contains(subParam)) { return false; }
collection?.Remove(subParam);
SubParams.Remove(subParam);
subParam.Element.Remove();
return true;
}
protected bool TryAddSubParam<T>(XElement element, Func<XElement, CharacterParams, T> constructor, out T subParam, IList<T> collection = null, Func<IList<T>, bool> filter = null) where T : SubParam
{
subParam = constructor(element, this);
if (collection != null && filter != null)
{
if (filter(collection)) { return false; }
}
MainElement.Add(element);
SubParams.Add(subParam);
collection?.Add(subParam);
return subParam != null;
}
#region Subparams
public class SoundParams : SubParam
{
public override string Name => "Sound";
[Serialize("", true), Editable]
public string File { get; private set; }
#if CLIENT
[Serialize(SoundType.Idle, true), Editable]
public SoundType State { get; private set; }
#endif
[Serialize(1000f, true), Editable(minValue: 0f, maxValue: 10000f)]
public float Range { get; private set; }
[Serialize(1.0f, true), Editable(minValue: 0f, maxValue: 2.0f)]
public float Volume { get; private set; }
[Serialize(Gender.None, true, description: "Is the sound gender specific?"), Editable()]
public Gender Gender { get; private set; }
public SoundParams(XElement element, CharacterParams character) : base(element, character) { }
}
public class ParticleParams : SubParam
{
private string name;
public override string Name
{
get
{
if (name == null && Element != null)
{
name = Element.Name.ToString().FormatCamelCaseWithSpaces();
}
return name;
}
}
[Serialize("", true), Editable]
public string Particle { get; set; }
[Serialize(0f, true), Editable(-360f, 360f, decimals: 0)]
public float AngleMin { get; private set; }
[Serialize(0f, true), Editable(-360f, 360f, decimals: 0)]
public float AngleMax { get; private set; }
[Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)]
public float ScaleMin { get; private set; }
[Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)]
public float ScaleMax { get; private set; }
[Serialize(0f, true), Editable(0f, 10000f, decimals: 0)]
public float VelocityMin { get; private set; }
[Serialize(0f, true), Editable(0f, 10000f, decimals: 0)]
public float VelocityMax { get; private set; }
[Serialize(0f, true), Editable(0f, 100f, decimals: 2)]
public float EmitInterval { get; private set; }
[Serialize(0, true), Editable(0, 1000)]
public int ParticlesPerSecond { get; private set; }
[Serialize(0, true), Editable(0, 1000)]
public int ParticleAmount { get; private set; }
[Serialize(false, true), Editable]
public bool HighQualityCollisionDetection { get; private set; }
[Serialize(false, true), Editable]
public bool CopyEntityAngle { get; private set; }
public ParticleParams(XElement element, CharacterParams character) : base(element, character) { }
}
public class HealthParams : SubParam
{
public override string Name => "Health";
[Serialize(100f, true, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)]
public float Vitality { get; set; }
[Serialize(true, true), Editable]
public bool DoesBleed { get; set; }
[Serialize(float.NegativeInfinity, true), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
public float CrushDepth { get; set; }
// Make editable?
[Serialize(false, true)]
public bool UseHealthWindow { get; set; }
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
public float BleedingReduction { get; private set; }
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
public float BurnReduction { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
public float ConstantHealthRegeneration { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
public float HealthRegenerationWhenEating { get; private set; }
// TODO: limbhealths, sprite?
public HealthParams(XElement element, CharacterParams character) : base(element, character) { }
}
public class InventoryParams : SubParam
{
public class InventoryItem : SubParam
{
public override string Name => "Item";
[Serialize("", true, description: "Item identifier."), Editable()]
public string Identifier { get; private set; }
public InventoryItem(XElement element, CharacterParams character) : base(element, character) { }
}
public override string Name => "Inventory";
[Serialize("Any, Any", true, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()]
public string Slots { get; private set; }
[Serialize(false, true), Editable]
public bool AccessibleWhenAlive { get; private set; }
[Serialize(1.0f, true, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)]
public float Commonness { get; private set; }
public List<InventoryItem> Items { get; private set; } = new List<InventoryItem>();
public InventoryParams(XElement element, CharacterParams character) : base(element, character)
{
foreach (var itemElement in element.GetChildElements("item"))
{
var item = new InventoryItem(itemElement, character);
SubParams.Add(item);
Items.Add(item);
}
}
public void AddItem(string identifier = null)
{
identifier = identifier ?? "";
var element = new XElement("item", new XAttribute("identifier", identifier));
Element.Add(element);
var item = new InventoryItem(element, Character);
SubParams.Add(item);
Items.Add(item);
}
public bool RemoveItem(InventoryItem item) => RemoveSubParam(item, Items);
}
public class AIParams : SubParam
{
public override string Name => "AI";
[Serialize(1.0f, true, description: "How strong other characters think this character is? Only affects AI."), Editable()]
public float CombatStrength { get; private set; }
[Serialize(1.0f, true, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Sight { get; private set; }
[Serialize(1.0f, true, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Hearing { get; private set; }
[Serialize(100f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: -1000f, maxValue: 1000f)]
public float AggressionHurt { get; private set; }
[Serialize(10f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: 0f, maxValue: 1000f)]
public float AggressionGreed { get; private set; }
[Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)]
public float FleeHealthThreshold { get; private set; }
[Serialize(false, true, description: "Does the character attack ONLY when provoked?"), Editable()]
public bool AttackOnlyWhenProvoked { get; private set; }
[Serialize(true, true, description: "When true, the character retaliates quickly when it's taking damage. Enabled by default."), Editable]
public bool RetaliateWhenTakingDamage { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
public bool AggressiveBoarding { get; private set; }
// TODO: latchonto, swarming
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();
public AIParams(XElement element, CharacterParams character) : base(element, character)
{
element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _));
element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _));
}
private bool TryAddTarget(XElement targetElement, out TargetParams target)
{
string tag = targetElement.GetAttributeString("tag", null);
if (!CheckTag(tag))
{
target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
return false;
}
else
{
target = new TargetParams(targetElement, Character);
targets.Add(target);
SubParams.Add(target);
return true;
}
}
public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams);
public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams)
{
var element = TargetParams.CreateNewElement(tag, state, priority);
if (TryAddTarget(element, out targetParams))
{
Element.Add(element);
}
return targetParams != null;
}
private bool CheckTag(string tag)
{
if (tag == null) { return false; }
tag = tag.ToLowerInvariant();
return targets.None(t => t.Tag == tag);
}
public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets);
public bool TryGetTarget(string targetTag, out TargetParams target)
{
target = targets.FirstOrDefault(t => t.Tag == targetTag);
return target != null;
}
public TargetParams GetTarget(string targetTag, bool throwError = true)
{
if (!TryGetTarget(targetTag, out TargetParams target))
{
if (throwError)
{
DebugConsole.ThrowError($"Cannot find a target with the tag {targetTag}!");
}
}
return target;
}
}
public class TargetParams : SubParam
{
public override string Name => "Target";
[Serialize("", true, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()]
public string Tag { get; private set; }
[Serialize(AIState.Idle, true), Editable]
public AIState State { get; set; }
[Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Priority { get; set; }
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
public static XElement CreateNewElement(string tag, AIState state, float priority)
{
return new XElement("target",
new XAttribute("tag", tag),
new XAttribute("state", state),
new XAttribute("priority", priority));
}
}
public abstract class SubParam : ISerializableEntity
{
public virtual string Name { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public XElement Element { get; set; }
public List<SubParam> SubParams { get; set; } = new List<SubParam>();
public CharacterParams Character { get; private set; }
public SubParam(XElement element, CharacterParams character)
{
Element = element;
Character = character;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public virtual bool Deserialize(bool recursive = true)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, Element);
if (recursive)
{
SubParams.ForEach(sp => sp.Deserialize(true));
}
return SerializableProperties != null;
}
public virtual bool Serialize(bool recursive = true)
{
SerializableProperty.SerializeProperties(this, Element, true);
if (recursive)
{
SubParams.ForEach(sp => sp.Serialize(true));
}
return true;
}
public virtual void Reset()
{
// Don't use recursion, because the reset method might be overriden
Deserialize(false);
SubParams.ForEach(sp => sp.Reset());
}
protected bool RemoveSubParam<T>(T subParam, IList<T> collection = null) where T : SubParam
{
if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; }
if (collection != null && !collection.Contains(subParam)) { return false; }
if (!SubParams.Contains(subParam)) { return false; }
collection?.Remove(subParam);
SubParams.Remove(subParam);
subParam.Element.Remove();
return true;
}
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, ScalableFont titleFont = null)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUI.LargeFont);
if (recursive)
{
SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUI.SmallFont));
}
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: new Color(20, 20, 20, 255))
{
CanBeFocused = false
};
}
}
#endif
}
#endregion
}
}
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -32,9 +33,11 @@ namespace Barotrauma
}
}
public XElement MainElement => doc.Root;
public virtual XElement MainElement => doc.Root;
public XElement OriginalElement { get; protected set; }
protected virtual string GetName() => Path.GetFileNameWithoutExtension(FullPath).FormatCamelCaseWithSpaces();
protected virtual bool Deserialize(XElement element = null)
{
element = element ?? MainElement;
@@ -67,7 +70,7 @@ namespace Barotrauma
protected virtual void UpdatePath(string fullPath)
{
FullPath = fullPath;
Name = Path.GetFileNameWithoutExtension(FullPath);
Name = GetName();
FileName = Path.GetFileName(FullPath);
Folder = Path.GetDirectoryName(FullPath);
}
@@ -112,23 +115,22 @@ namespace Barotrauma
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor)
public virtual void AddToEditor(ParamsEditor editor, int space = 0)
{
if (!IsLoaded)
{
DebugConsole.ThrowError("[Params] Not loaded!");
return;
}
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUI.LargeFont);
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
{
CanBeFocused = false
};
}
}
#endif
#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
}
}
File diff suppressed because it is too large Load Diff