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(speciesName, fileName); public static HumanRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); } class FishRagdollParams : RagdollParams { public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); } class RagdollParams : EditableParams, IMemorizable { #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> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); public List Limbs { get; private set; } = new List(); public List Joints { get; private set; } = new List(); protected IEnumerable 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(string speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); /// /// 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. /// public static T GetRagdollParams(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 ragdolls)) { ragdolls = new Dictionary(); 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; } /// /// 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! /// public static T CreateDefault(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(); 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 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; } /// /// Applies the current properties to the xml definition without saving to file. /// public void Apply() { Serialize(); } /// /// Resets the current properties to the xml (stored in memory). Force reload reloads the file from disk. /// 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 Memento { get; protected set; } = new Memento(); 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; } /// /// Should be converted to sim units. /// [Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb1."), Editable()] public Vector2 Limb1Anchor { get; set; } /// /// Should be converted to sim units. /// [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; } /// /// In degrees. /// [Serialize(0f, true), Editable] public float UpperLimit { get; set; } /// /// In degrees. /// [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 = new List(); public AttackParams Attack { get; private set; } public SoundParams Sound { get; private set; } public LightSourceParams LightSource { get; private set; } public List DamageModifiers { get; private set; } = new List(); 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; } /// /// The orientation of the sprite as drawn on the sprite sheet (in radians). /// 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(XElement element, Func constructor, out T subParam, IList collection = null, Func, 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 subParam, IList 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(); 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 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 SerializableProperties { get; private set; } public XElement Element { get; set; } public XElement OriginalElement { get; protected set; } public List SubParams { get; set; } = new List(); 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 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(); } 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 } }