Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs
T
2020-05-13 12:55:42 -03:00

1209 lines
51 KiB
C#

using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.IO;
using System.Xml;
using Barotrauma.Extensions;
#if CLIENT
using Barotrauma.SpriteDeformations;
#endif
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, IMemorizable<RagdollParams>
{
#region Ragdoll
public const float MIN_SCALE = 0.1f;
public const float MAX_SCALE = 2;
public string SpeciesName { get; private set; }
[Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable]
public string Texture { get; set; }
[Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)]
public float SpritesheetOrientation { get; set; }
public bool IsSpritesheetOrientationHorizontal
{
get
{
return
(SpritesheetOrientation > 45.0f && SpritesheetOrientation < 135.0f) ||
(SpritesheetOrientation > 255.0f && SpritesheetOrientation < 315.0f);
}
}
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, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)]
public float ColliderHeightFromFloor { get; set; }
[Serialize(50f, true, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float ImpactTolerance { get; set; }
[Serialize(true, true, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public bool CanEnterSubmarine { get; set; }
[Serialize(true, true), Editable]
public bool CanWalk { get; set; }
[Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()]
public bool Draggable { get; set; }
[Serialize(LimbType.Torso, true), Editable]
public LimbType MainLimb { get; set; }
private static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
public List<JointParams> Joints { get; private set; } = new List<JointParams>();
protected IEnumerable<SubParam> GetAllSubParams() =>
Colliders.Select(c => c as SubParam)
.Concat(Limbs.Select(j => j as SubParam)
.Concat(Joints.Select(j => j as SubParam)));
public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll";
public static string GetDefaultFile(string speciesName, ContentPackage contentPackage = null)
=> Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml");
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage));
if (prefab?.XDocument == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
return string.Empty;
}
return GetFolder(prefab.XDocument, prefab.FilePath);
}
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar;
}
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 (string.IsNullOrWhiteSpace(speciesName))
{
throw new Exception($"Species name null or empty!");
}
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).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
selectedFile = GetDefaultFile(speciesName);
}
}
}
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
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early.
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
}
}
return (T)ragdoll;
}
/// <summary>
/// Creates a default ragdoll for the species using a predefined configuration.
/// Note: Use only to create ragdolls for new characters, because this overrides the old ragdoll!
/// </summary>
public static T CreateDefault<T>(string fullPath, string speciesName, XElement mainElement) where T : RagdollParams, new()
{
// 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
{
doc = new XDocument(mainElement)
};
instance.UpdatePath(fullPath);
instance.IsLoaded = instance.Deserialize(mainElement);
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;
}
public static void ClearCache() => allRagdolls.Clear();
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;
}
/// <summary>
/// Applies the current properties to the xml definition without saving to file.
/// </summary>
public void Apply()
{
Serialize();
}
/// <summary>
/// Resets the current properties to the xml (stored in memory). Force reload reloads the file from disk.
/// </summary>
public override bool Reset(bool forceReload = false)
{
if (forceReload)
{
return Load(FullPath, SpeciesName);
}
// Don't use recursion, because the reset method might be overriden
Deserialize(OriginalElement, alsoChildren: false, recursive: false);
GetAllSubParams().ForEach(sp => sp.Reset());
return true;
}
protected void CreateColliders()
{
Colliders.Clear();
for (int i = 0; i < MainElement.GetChildElements("collider").Count(); i++)
{
var element = MainElement.GetChildElements("collider").ElementAt(i);
string name = i > 0 ? "Secondary Collider" : "Main Collider";
Colliders.Add(new ColliderParams(element, this, name));
}
}
protected void CreateLimbs()
{
Limbs.Clear();
foreach (var element in MainElement.GetChildElements("limb"))
{
Limbs.Add(new LimbParams(element, this));
}
Limbs = Limbs.OrderBy(l => l.ID).ToList();
}
protected void CreateJoints()
{
Joints.Clear();
foreach (var element in MainElement.GetChildElements("joint"))
{
Joints.Add(new JointParams(element, this));
}
}
public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
{
if (base.Deserialize(element))
{
if (alsoChildren)
{
GetAllSubParams().ForEach(p => p.Deserialize(recursive: recursive));
}
return true;
}
return false;
}
public bool Serialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
{
if (base.Serialize(element))
{
if (alsoChildren)
{
GetAllSubParams().ForEach(p => p.Serialize(recursive: recursive));
}
return true;
}
return false;
}
#if CLIENT
public void AddToEditor(ParamsEditor editor, bool alsoChildren = true, int space = 0)
{
base.AddToEditor(editor);
if (alsoChildren)
{
var subParams = GetAllSubParams();
foreach (var subParam in subParams)
{
subParam.AddToEditor(editor, true, space);
}
}
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
#endregion
#region Memento
public Memento<RagdollParams> Memento { get; protected set; } = new Memento<RagdollParams>();
public void StoreSnapshot()
{
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 void Undo() => RevertTo(Memento.Undo() as RagdollParams);
public void Redo() => RevertTo(Memento.Redo() as RagdollParams);
public void ClearHistory() => Memento.Clear();
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, alsoChildren: 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
#region Subparams
public class JointParams : SubParam
{
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, description: "Local position of the joint in the Limb1."), Editable()]
public Vector2 Limb1Anchor { get; set; }
/// <summary>
/// Should be converted to sim units.
/// </summary>
[Serialize("1.0, 1.0", true, description: "Local position of the join in the Limb2."), Editable()]
public Vector2 Limb2Anchor { get; set; }
[Serialize(true, true), Editable]
public bool CanBeSevered { get; set; }
[Serialize(1f, true, description:"Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects limbs of type None, Shield, or Tail on non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SeveranceProbabilityModifier { get; set; }
[Serialize("gore", true), Editable]
public string BreakSound { 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; }
[Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
public float Scale { get; set; }
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
public class LimbParams : SubParam
{
public readonly SpriteParams normalSpriteParams;
public readonly SpriteParams damagedSpriteParams;
public readonly DeformSpriteParams deformSpriteParams;
public readonly List<DecorativeSpriteParams> decorativeSpriteParams = new List<DecorativeSpriteParams>();
public AttackParams Attack { get; private set; }
public SoundParams Sound { get; private set; }
public LightSourceParams LightSource { get; private set; }
public List<DamageModifierParams> DamageModifiers { get; private set; } = new List<DamageModifierParams>();
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}";
public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams;
[Serialize(-1, true), Editable(ReadOnly = true)]
public int ID { get; set; }
[Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
public LimbType Type { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360)]
public float SpriteOrientation { get; set; }
/// <summary>
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
/// </summary>
public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation);
[Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()]
public bool Flip { get; set; }
[Serialize(false, true, description: "Currently only works with non-deformable (normal) sprites."), Editable()]
public bool MirrorVertically { get; set; }
[Serialize(false, true), Editable]
public bool MirrorHorizontally { get; set; }
[Serialize(false, true, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
[Serialize(0f, true, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Radius { get; set; }
[Serialize(0f, true, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Height { get; set; }
[Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float Density { get; set; }
[Serialize(false, true), Editable]
public bool IgnoreCollisions { get; set; }
[Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable]
public float AngularDamping { get; set; }
[Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()]
public Vector2 PullPos { get; set; }
[Serialize("0, 0", true, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()]
public Vector2 StepOffset { get; set; }
[Serialize(-1, true, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()]
public int RefJoint { get; set; }
[Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
public Vector2 MouthPos { get; set; }
[Serialize("", true), Editable]
public string Notes { get; set; }
[Serialize(0f, true), Editable]
public float ConstantTorque { get; set; }
[Serialize(0f, true), Editable]
public float ConstantAngle { get; set; }
[Serialize(1f, true), Editable]
public float Scale { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
public float AttackForceMultiplier { get; set; }
[Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)]
public float MinSeveranceDamage { get; set; }
// Non-editable ->
// TODO: make read-only
[Serialize(0, true)]
public int HealthIndex { get; set; }
[Serialize(0.3f, true)]
public float Friction { get; set; }
[Serialize(0.05f, true)]
public float Restitution { get; set; }
public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
var spriteElement = element.GetChildElement("sprite");
if (spriteElement != null)
{
normalSpriteParams = new SpriteParams(spriteElement, ragdoll);
SubParams.Add(normalSpriteParams);
}
var damagedSpriteElement = element.GetChildElement("damagedsprite");
if (damagedSpriteElement != null)
{
damagedSpriteParams = new SpriteParams(damagedSpriteElement, ragdoll);
// Hide the damaged sprite params in the editor for now.
//SubParams.Add(damagedSpriteParams);
}
var deformSpriteElement = element.GetChildElement("deformablesprite");
if (deformSpriteElement != null)
{
deformSpriteParams = new DeformSpriteParams(deformSpriteElement, ragdoll);
SubParams.Add(deformSpriteParams);
}
foreach (var decorativeSpriteElement in element.GetChildElements("decorativesprite"))
{
var decorativeParams = new DecorativeSpriteParams(decorativeSpriteElement, ragdoll);
decorativeSpriteParams.Add(decorativeParams);
SubParams.Add(decorativeParams);
}
var attackElement = element.GetChildElement("attack");
if (attackElement != null)
{
Attack = new AttackParams(attackElement, ragdoll);
SubParams.Add(Attack);
}
foreach (var damageElement in element.GetChildElements("damagemodifier"))
{
var damageModifier = new DamageModifierParams(damageElement, ragdoll);
DamageModifiers.Add(damageModifier);
SubParams.Add(damageModifier);
}
var soundElement = element.GetChildElement("sound");
if (soundElement != null)
{
Sound = new SoundParams(soundElement, ragdoll);
SubParams.Add(Sound);
}
var lightElement = element.GetChildElement("lightsource");
if (lightElement != null)
{
LightSource = new LightSourceParams(lightElement, ragdoll);
SubParams.Add(LightSource);
}
}
public bool AddAttack()
{
if (Attack != null) { return false; }
TryAddSubParam(new XElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack);
Attack = newAttack;
return Attack != null;
}
public bool AddSound()
{
if (Sound != null) { return false; }
TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound);
Sound = newSound;
return Sound != null;
}
public bool AddLight()
{
if (LightSource != null) { return false; }
var lightSourceElement = new XElement("lightsource",
new XElement("lighttexture", new XAttribute("texture", "Content/Lights/light.png")));
TryAddSubParam(lightSourceElement, (e, c) => new LightSourceParams(e, c), out LightSourceParams newLightSource);
LightSource = newLightSource;
return LightSource != null;
}
public bool AddDamageModifier() => TryAddSubParam(new XElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers);
public bool RemoveAttack()
{
if (RemoveSubParam(Attack))
{
Attack = null;
return true;
}
return false;
}
public bool RemoveSound()
{
if (RemoveSubParam(Sound))
{
Sound = null;
return true;
}
return false;
}
public bool RemoveLight()
{
if (RemoveSubParam(LightSource))
{
LightSource = null;
return true;
}
return false;
}
public bool RemoveDamageModifier(DamageModifierParams damageModifier) => RemoveSubParam(damageModifier, DamageModifiers);
protected bool TryAddSubParam<T>(XElement element, Func<XElement, RagdollParams, T> constructor, out T subParam, IList<T> collection = null, Func<IList<T>, bool> filter = null) where T : SubParam
{
subParam = constructor(element, Ragdoll);
if (collection != null && filter != null)
{
if (filter(collection)) { return false; }
}
Element.Add(element);
SubParams.Add(subParam);
collection?.Add(subParam);
return subParam != null;
}
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;
}
}
public class DecorativeSpriteParams : SpriteParams
{
public DecorativeSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
#if CLIENT
DecorativeSprite = new DecorativeSprite(element);
#endif
}
#if CLIENT
public DecorativeSprite DecorativeSprite { get; private set; }
public override bool Deserialize(XElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
SerializableProperty.SerializeProperties(DecorativeSprite, element ?? Element);
return true;
}
public override void Reset()
{
base.Reset();
DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, OriginalElement);
}
#endif
}
public class DeformSpriteParams : SpriteParams
{
public DeformationParams Deformation { get; private set; }
public DeformSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
Deformation = new DeformationParams(element, ragdoll);
SubParams.Add(Deformation);
}
}
public class SpriteParams : SubParam
{
[Serialize("0, 0, 0, 0", true), Editable]
public Rectangle SourceRect { get; set; }
[Serialize("0.5, 0.5", true, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)]
public Vector2 Origin { get; set; }
[Serialize(0f, true, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)]
public float Depth { get; set; }
[Serialize("", true), Editable()]
public string Texture { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true), Editable()]
public Color Color { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()]
public Color DeadColor { get; set; }
[Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)]
public float DeadColorTime { get; set; }
public override string Name => "Sprite";
public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
public string GetTexturePath() => string.IsNullOrWhiteSpace(Texture) ? Ragdoll.Texture : Texture;
}
public class DeformationParams : SubParam
{
public DeformationParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
#if CLIENT
Deformations = new Dictionary<SpriteDeformationParams, XElement>();
foreach (var deformationElement in element.GetChildElements("spritedeformation"))
{
string typeName = deformationElement.GetAttributeString("typename", null) ?? deformationElement.GetAttributeString("type", "");
SpriteDeformationParams deformation = null;
switch (typeName.ToLowerInvariant())
{
case "inflate":
deformation = new InflateParams(deformationElement);
break;
case "custom":
deformation = new CustomDeformationParams(deformationElement);
break;
case "noise":
deformation = new NoiseDeformationParams(deformationElement);
break;
case "jointbend":
case "bendjoint":
deformation = new JointBendDeformationParams(deformationElement);
break;
case "reacttotriggerers":
deformation = new PositionalDeformationParams(deformationElement);
break;
default:
DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'");
break;
}
if (deformation != null)
{
deformation.TypeName = typeName;
}
Deformations.Add(deformation, deformationElement);
}
#endif
}
#if CLIENT
public Dictionary<SpriteDeformationParams, XElement> Deformations { get; private set; }
public override bool Deserialize(XElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value));
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
Deformations.ForEach(d => SerializableProperty.SerializeProperties(d.Key, d.Value));
return true;
}
public override void Reset()
{
base.Reset();
Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value));
}
#endif
}
public class ColliderParams : SubParam
{
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; }
public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
{
Name = name;
}
}
public class LightSourceParams : SubParam
{
public class LightTexture : SubParam
{
public override string Name => "Light Texture";
[Serialize("Content/Lights/pointlight_bright.png", true), Editable]
public string Texture { get; private set; }
[Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)]
public Vector2 Origin { get; set; }
[Serialize("1.0, 1.0", true), Editable(DecimalCount = 2)]
public Vector2 Size { get; set; }
public LightTexture(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
public LightTexture Texture { get; private set; }
#if CLIENT
public Lights.LightSourceParams LightSource { get; private set; }
#endif
public LightSourceParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
#if CLIENT
LightSource = new Lights.LightSourceParams(element);
#endif
var lightTextureElement = element.GetChildElement("lighttexture");
if (lightTextureElement != null)
{
Texture = new LightTexture(lightTextureElement, ragdoll);
SubParams.Add(Texture);
}
}
#if CLIENT
public override bool Deserialize(XElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
LightSource.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
LightSource.Serialize(element ?? Element);
return true;
}
public override void Reset()
{
base.Reset();
LightSource.Serialize(OriginalElement);
}
#endif
}
// TODO: conditionals?
public class AttackParams : SubParam
{
public Attack Attack { get; private set; }
public AttackParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
Attack = new Attack(element, ragdoll.SpeciesName);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Attack.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
Attack.Serialize(element ?? Element);
return true;
}
public override void Reset()
{
base.Reset();
Attack.Deserialize(OriginalElement);
Attack.ReloadAfflictions(OriginalElement);
}
public bool AddNewAffliction()
{
Serialize();
var subElement = new XElement("affliction",
new XAttribute("identifier", "internaldamage"),
new XAttribute("strength", 0f),
new XAttribute("probability", 1.0f));
Element.Add(subElement);
Attack.ReloadAfflictions(Element);
Serialize();
return true;
}
public bool RemoveAffliction(XElement affliction)
{
Serialize();
affliction.Remove();
Attack.ReloadAfflictions(Element);
return Serialize();
}
}
public class DamageModifierParams : SubParam
{
public DamageModifier DamageModifier { get; private set; }
public DamageModifierParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
DamageModifier = new DamageModifier(element, ragdoll.SpeciesName);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
DamageModifier.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
DamageModifier.Serialize(element ?? Element);
return true;
}
public override void Reset()
{
base.Reset();
DamageModifier.Deserialize(OriginalElement);
}
}
public class SoundParams : SubParam
{
public override string Name => "Sound";
[Serialize("", true), Editable]
public string Tag { get; private set; }
public SoundParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
public abstract class SubParam : ISerializableEntity
{
public virtual string Name { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public XElement Element { get; set; }
public XElement OriginalElement { get; protected set; }
public List<SubParam> SubParams { get; set; } = new List<SubParam>();
public RagdollParams Ragdoll { get; private set; }
public virtual string GenerateName() => Element.Name.ToString();
public SubParam(XElement element, RagdollParams ragdoll)
{
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(recursive: true));
}
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(recursive: true));
}
return true;
}
public virtual void SetCurrentElementAsOriginalElement()
{
OriginalElement = Element;
SubParams.ForEach(sp => sp.SetCurrentElementAsOriginalElement());
}
public virtual void Reset()
{
// Don't use recursion, because the reset method might be overriden
Deserialize(OriginalElement, false);
SubParams.ForEach(sp => sp.Reset());
}
#if CLIENT
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public Dictionary<Affliction, SerializableEntityEditor> AfflictionEditors { get; private set; }
public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUI.LargeFont);
if (this is DecorativeSpriteParams decSpriteParams)
{
new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUI.LargeFont);
}
else if (this is DeformSpriteParams deformSpriteParams)
{
foreach (var deformation in deformSpriteParams.Deformation.Deformations.Keys)
{
new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUI.LargeFont);
}
}
else if (this is AttackParams attackParams)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUI.LargeFont);
if (AfflictionEditors == null)
{
AfflictionEditors = new Dictionary<Affliction, SerializableEntityEditor>();
}
else
{
AfflictionEditors.Clear();
}
foreach (var affliction in attackParams.Attack.Afflictions.Keys)
{
var afflictionEditor = new SerializableEntityEditor(SerializableEntityEditor.RectTransform, affliction, inGame: false, showName: true);
AfflictionEditors.Add(affliction, afflictionEditor);
SerializableEntityEditor.AddCustomContent(afflictionEditor, SerializableEntityEditor.ContentCount);
}
}
else if (this is LightSourceParams lightParams)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUI.LargeFont);
}
else if (this is DamageModifierParams damageModifierParams)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUI.LargeFont);
}
if (recursive)
{
SubParams.ForEach(sp => sp.AddToEditor(editor, true));
}
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
}
}