using Barotrauma.Lights; using Barotrauma.Particles; using Barotrauma.SpriteDeformations; using Barotrauma.Extensions; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Barotrauma.IO; using Barotrauma.Utils; using System.Linq; using System.Xml.Linq; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; namespace Barotrauma { partial class LimbJoint { public void UpdateDeformations(float deltaTime) { float diff = Math.Abs(UpperLimit - LowerLimit); float strength = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, MathHelper.Pi, diff)); float jointAngle = JointAngle * strength; JointBendDeformation limbADeformation = LimbA.ActiveDeformations.Find(d => d is JointBendDeformation) as JointBendDeformation; JointBendDeformation limbBDeformation = LimbB.ActiveDeformations.Find(d => d is JointBendDeformation) as JointBendDeformation; if (limbADeformation != null && limbBDeformation != null) { UpdateBend(LimbA, limbADeformation, this.LocalAnchorA, -jointAngle); UpdateBend(LimbB, limbBDeformation, this.LocalAnchorB, jointAngle); } void UpdateBend(Limb limb, JointBendDeformation deformation, Vector2 localAnchor, float angle) { deformation.Scale = limb.DeformSprite.Size; Vector2 displayAnchor = ConvertUnits.ToDisplayUnits(localAnchor); displayAnchor.Y = -displayAnchor.Y; Vector2 refPos = displayAnchor + limb.DeformSprite.Origin; refPos.X /= limb.DeformSprite.Size.X; refPos.Y /= limb.DeformSprite.Size.Y; if (Math.Abs(localAnchor.X) > Math.Abs(localAnchor.Y)) { if (localAnchor.X > 0.0f) { deformation.BendRightRefPos = refPos; deformation.BendRight = angle; } else { deformation.BendLeftRefPos = refPos; deformation.BendLeft = angle; } } else { if (localAnchor.Y > 0.0f) { deformation.BendUpRefPos = refPos; deformation.BendUp = angle; } else { deformation.BendDownRefPos = refPos; deformation.BendDown = angle; } } } } public void Draw(SpriteBatch spriteBatch) { //var mouthPos = ragdoll.GetMouthPosition(); //if (mouthPos != null) //{ // var pos = ConvertUnits.ToDisplayUnits(mouthPos.Value); // pos.Y = -pos.Y; // ShapeExtensions.DrawPoint(spriteBatch, pos, GUIStyle.Red, size: 5); //} // A debug visualisation on the bezier curve between limbs. /*var start = LimbA.WorldPosition; var end = LimbB.WorldPosition; var jointAPos = ConvertUnits.ToDisplayUnits(LocalAnchorA); var control = start + Vector2.Transform(jointAPos, Matrix.CreateRotationZ(LimbA.Rotation)); start.Y = -start.Y; end.Y = -end.Y; control.Y = -control.Y; GUI.DrawRectangle(spriteBatch, start, Vector2.One * 5, Color.White, true); GUI.DrawRectangle(spriteBatch, end, Vector2.One * 5, Color.Black, true); GUI.DrawRectangle(spriteBatch, control, Vector2.One * 5, Color.Black, true); GUI.DrawLine(spriteBatch, start, end, Color.White); GUI.DrawLine(spriteBatch, start, control, Color.Black); GUI.DrawLine(spriteBatch, control, end, Color.Black); GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUIStyle.Red);*/ } } partial class Limb { //minimum duration between hit/attack sounds public const float SoundInterval = 0.4f; public float LastAttackSoundTime, LastImpactSoundTime; private float wetTimer; private float dripParticleTimer; private float deadTimer; private Color? randomColor; /// /// Note that different limbs can share the same deformations. /// Use ragdoll.SpriteDeformations for a collection that cannot have duplicates. /// private List Deformations { get; set; } = new List(); private List NonConditionalDeformations { get; set; } = new List(); private List<(ConditionalSprite, IEnumerable)> ConditionalDeformations { get; set; } = new List<(ConditionalSprite, IEnumerable)>(); public List ActiveDeformations { get; set; } = new List(); public Sprite Sprite { get; protected set; } public Sprite TintMask { get; protected set; } public Sprite HuskMask { get; protected set; } public float TintHighlightThreshold { get; protected set; } public float TintHighlightMultiplier { get; protected set; } private SpriteBatch.EffectWithParams tintEffectParams; private SpriteBatch.EffectWithParams huskSpriteParams; protected DeformableSprite _deformSprite; public DeformableSprite DeformSprite { get { var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.DeformableSprite != null); if (conditionalSprite != null) { return conditionalSprite.DeformableSprite; } else { return _deformSprite; } } } public List DecorativeSprites { get; private set; } = new List(); public Sprite ActiveSprite { get { var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.ActiveSprite != null); if (conditionalSprite != null) { return conditionalSprite.ActiveSprite; } else { return _deformSprite != null ? _deformSprite.Sprite : Sprite; } } } public Sprite GetActiveSprite(bool excludeConditionalSprites = true) => excludeConditionalSprites ? (_deformSprite != null ? _deformSprite.Sprite : Sprite) : ActiveSprite; public float DefaultSpriteDepth { get; private set; } public WearableSprite HairWithHatSprite { get; set; } public WearableSprite HuskSprite { get; private set; } public WearableSprite HerpesSprite { get; private set; } public void LoadHuskSprite() => HuskSprite = GetWearableSprite(WearableType.Husk); public void LoadHerpesSprite() => HerpesSprite = GetWearableSprite(WearableType.Herpes); public float TextureScale => Params.Ragdoll.TextureScale; public Sprite DamagedSprite { get; private set; } public bool Hide { get => Params.Hide; set => Params.Hide = value; } public List ConditionalSprites { get; private set; } = new List(); private Dictionary spriteAnimState = new Dictionary(); private Dictionary> DecorativeSpriteGroups = new Dictionary>(); class SpriteState { public float RotationState; public float OffsetState; public Vector2 RandomOffsetMultiplier = new Vector2(Rand.Range(-1.0f, 1.0f), Rand.Range(-1.0f, 1.0f)); public float RandomRotationFactor = Rand.Range(0.0f, 1.0f); public float RandomScaleFactor = Rand.Range(0.0f, 1.0f); public bool IsActive = true; } public Color InitialLightSourceColor { get; private set; } public float? InitialLightSpriteAlpha { get; private set; } public LightSource LightSource { get; private set; } private float damageOverlayStrength; public float DamageOverlayStrength { get { return damageOverlayStrength; } set { damageOverlayStrength = MathHelper.Clamp(value, 0.0f, 1.0f); } } private float burnOverLayStrength; public float BurnOverlayStrength { get { return burnOverLayStrength; } set { burnOverLayStrength = MathHelper.Clamp(value, 0.0f, 1.0f); } } public string HitSoundTag => Params?.Sound?.Tag; private readonly List wearableTypeHidingSprites = new List(); private readonly HashSet wearableTypesToHide = new HashSet(); private bool enableHuskSprite; public bool EnableHuskSprite { get { return enableHuskSprite; } set { if (enableHuskSprite == value) { return; } enableHuskSprite = value; if (enableHuskSprite) { if (HuskSprite == null) { LoadHuskSprite(); } } if (HuskSprite != null) { if (enableHuskSprite) { OtherWearables.Insert(0, HuskSprite); UpdateWearableTypesToHide(); } else { OtherWearables.Remove(HuskSprite); UpdateWearableTypesToHide(); } } } } partial void InitProjSpecific(ContentXElement element) { for (int i = 0; i < Params.decorativeSpriteParams.Count; i++) { var param = Params.decorativeSpriteParams[i]; var decorativeSprite = new DecorativeSprite(param.Element, file: GetSpritePath(param.Element, param, ref _texturePath)); DecorativeSprites.Add(decorativeSprite); int groupID = decorativeSprite.RandomGroupID; if (!DecorativeSpriteGroups.ContainsKey(groupID)) { DecorativeSpriteGroups.Add(groupID, new List()); } DecorativeSpriteGroups[groupID].Add(decorativeSprite); spriteAnimState.Add(decorativeSprite, new SpriteState()); } TintMask = null; float sourceRectScale = ragdoll.RagdollParams.SourceRectScale; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath), sourceRectScale: sourceRectScale); break; case "damagedsprite": DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath), sourceRectScale: sourceRectScale); break; case "conditionalsprite": var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath), sourceRectScale: sourceRectScale); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { var conditionalDeformations = CreateDeformations(subElement.GetChildElement("deformablesprite")); Deformations.AddRange(conditionalDeformations); ConditionalDeformations.Add((conditionalSprite, conditionalDeformations)); } break; case "deformablesprite": _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath), sourceRectScale: sourceRectScale); var deformations = CreateDeformations(subElement); Deformations.AddRange(deformations); NonConditionalDeformations.AddRange(deformations); break; case "randomcolor": randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandomUnsynced(); if (randomColor.HasValue) { Params.GetSprite().Color = randomColor.Value; } break; case "lightsource": LightSource = new LightSource(subElement, GetConditionalTarget()) { ParentBody = body, SpriteScale = Vector2.One * Scale * TextureScale }; if (randomColor.HasValue) { LightSource.Color = new Color(randomColor.Value.R, randomColor.Value.G, randomColor.Value.B, LightSource.Color.A); } InitialLightSourceColor = LightSource.Color; InitialLightSpriteAlpha = LightSource.OverrideLightSpriteAlpha; break; case "tintmask": ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture"); if (!tintMaskPath.IsNullOrWhiteSpace()) { TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath), sourceRectScale: sourceRectScale); TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f); TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f); } break; case "huskmask": ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture"); if (!huskMaskPath.IsNullOrWhiteSpace()) { HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath), sourceRectScale: sourceRectScale); } break; } ISerializableEntity GetConditionalTarget() { ISerializableEntity targetEntity; string target = subElement.GetAttributeString("target", null); if (string.Equals(target, "character", StringComparison.OrdinalIgnoreCase)) { targetEntity = character; } else { targetEntity = this; } return targetEntity; } IEnumerable CreateDeformations(XElement e) { List deformations = new List(); foreach (XElement animationElement in e.GetChildElements("spritedeformation")) { int sync = animationElement.GetAttributeInt("sync", -1); SpriteDeformation deformation = null; if (sync > -1) { // if the element is marked with the sync attribute, use a deformation of the same type with the same sync value, if there is one already. string typeName = animationElement.GetAttributeString("type", "").ToLowerInvariant(); deformation = ragdoll.Limbs .Where(l => l != null) .SelectMany(l => l.Deformations) .FirstOrDefault(d => d.TypeName == typeName && d.Sync == sync); } if (deformation == null) { deformation = SpriteDeformation.Load(animationElement, character.SpeciesName.Value); if (deformation != null) { ragdoll.SpriteDeformations.Add(deformation); } } if (deformation != null) { deformations.Add(deformation); } } return deformations; } } DefaultSpriteDepth = GetActiveSprite()?.Depth ?? 0.0f; LightSource?.CheckConditionals(); } private void RefreshDeformations() { if (_deformSprite == null) { return; } if (ConditionalSprites.None()) { ActiveDeformations = Deformations; } else { ActiveDeformations.Clear(); if (_deformSprite == DeformSprite) { ActiveDeformations.AddRange(NonConditionalDeformations); } foreach (var conditionalDeformation in ConditionalDeformations) { if (conditionalDeformation.Item1.IsActive) { ActiveDeformations.AddRange(conditionalDeformation.Item2); } } } } public void RecreateSprites() { if (Sprite != null) { var source = Sprite.SourceElement; Sprite.Remove(); Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams, ref _texturePath)); } if (_deformSprite != null) { var source = _deformSprite.Sprite.SourceElement; _deformSprite.Remove(); _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams, ref _texturePath)); } if (DamagedSprite != null) { var source = DamagedSprite.SourceElement; DamagedSprite.Remove(); DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams, ref _damagedTexturePath)); } for (int i = 0; i < ConditionalSprites.Count; i++) { var conditionalSprite = ConditionalSprites[i]; var source = conditionalSprite.ActiveSprite.SourceElement; conditionalSprite.Remove(); ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null, ref _texturePath)); } for (int i = 0; i < DecorativeSprites.Count; i++) { var decorativeSprite = DecorativeSprites[i]; var source = decorativeSprite.Sprite.SourceElement; decorativeSprite.Remove(); DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i], ref _texturePath)); } } private void CalculateHeadPosition(Sprite sprite) { if (type != LimbType.Head) { return; } character.Info?.CalculateHeadPosition(sprite); } private string _texturePath; private string _damagedTexturePath; private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, ref string path) { if (path == null) { if (spriteParams != null) { //1. check if the variant file redefines the texture ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage); //2. check if the base prefab defines the texture if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty) { Identifier speciesName = character.GetBaseCharacterSpeciesName(); RagdollParams parentRagdollParams = character.IsHumanoid ? RagdollParams.GetDefaultRagdollParams(speciesName, character.Params, character.Prefab.ContentPackage) : RagdollParams.GetDefaultRagdollParams(speciesName, character.Params, character.Prefab.ContentPackage); texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture"); } //3. "default case", get the texture from this character's XML texturePath ??= ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); path = GetSpritePath(texturePath); } else { ContentPath texturePath = element.GetAttributeContentPath("texture"); texturePath = texturePath.IsNullOrWhiteSpace() ? ContentPath.FromRaw(character.Prefab.ContentPackage, ragdoll.RagdollParams.Texture) : texturePath; path = GetSpritePath(texturePath); } } return path; } /// /// Get the full path of a limb sprite, taking into account tags, gender and head id /// public static string GetSpritePath(ContentPath texturePath, CharacterInfo characterInfo) { string spritePath = texturePath.Value; string spritePathWithTags = spritePath; if (characterInfo != null) { spritePath = characterInfo.ReplaceVars(spritePath); characterInfo.VerifySpriteTagsLoaded(); if (characterInfo.SpriteTags.Any()) { string tags = ""; characterInfo.SpriteTags.ForEach(tag => tags += $"[{tag}]"); spritePathWithTags = Path.Combine( Path.GetDirectoryName(spritePath), Path.GetFileNameWithoutExtension(spritePath) + tags + Path.GetExtension(spritePath)); } } return File.Exists(spritePathWithTags) ? spritePathWithTags : spritePath; } private string GetSpritePath(ContentPath texturePath) { if (!character.IsHumanoid) { return texturePath.Value; } return GetSpritePath(texturePath, character.Info); } partial void LoadParamsProjSpecific() { bool isFlipped = dir == Direction.Left; Sprite?.LoadParams(Params.normalSpriteParams, isFlipped); DamagedSprite?.LoadParams(Params.damagedSpriteParams, isFlipped); _deformSprite?.Sprite.LoadParams(Params.deformSpriteParams, isFlipped); for (int i = 0; i < DecorativeSprites.Count; i++) { DecorativeSprites[i].Sprite?.LoadParams(Params.decorativeSpriteParams[i], isFlipped); } } partial void AddDamageProjSpecific(bool playSound, AttackResult result) { float bleedingDamage = 0; if (character.CharacterHealth.DoesBleed) { foreach (var affliction in result.Afflictions) { if (affliction is AfflictionBleeding bleeding && bleeding.Prefab.DamageParticles) { bleedingDamage += affliction.GetVitalityDecrease(null); } } } float damage = 0; foreach (var affliction in result.Afflictions) { if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { damage += affliction.GetVitalityDecrease(null); } } float damageMultiplier = 1; float bleedingDamageMultiplier = 1; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { if (damageModifier.MatchesAfflictionType(AfflictionPrefab.DamageType)) { damageMultiplier *= damageModifier.DamageMultiplier; } else if (damageModifier.MatchesAfflictionType(AfflictionPrefab.BleedingType)) { bleedingDamageMultiplier *= damageModifier.DamageMultiplier; } } if (playSound) { string damageSoundType = (bleedingDamage > damage) ? "LimbSlash" : "LimbBlunt"; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { if (!string.IsNullOrWhiteSpace(damageModifier.DamageSound)) { damageSoundType = damageModifier.DamageSound; break; } } SoundPlayer.PlayDamageSound(damageSoundType, Math.Max(damage, bleedingDamage), WorldPosition); } // spawn damage particles float damageParticleAmount = damage < 1 ? 0 : Math.Min(damage / 5, 1.0f) * damageMultiplier; if (damageParticleAmount > 0.001f) { foreach (ParticleEmitter emitter in character.DamageEmitters) { if (emitter?.Prefab == null) { continue; } if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } ParticlePrefab overrideParticle = null; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { if (damageModifier.DamageMultiplier > 0 && !string.IsNullOrWhiteSpace(damageModifier.DamageParticle)) { overrideParticle = ParticleManager.FindPrefab(damageModifier.DamageParticle); break; } } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, amountMultiplier: damageParticleAmount, overrideParticle: overrideParticle); } } if (bleedingDamage > 0) { float bloodParticleAmount = Math.Min(bleedingDamage / 5, 1.0f) * bleedingDamageMultiplier; float bloodParticleSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.BloodEmitters) { if (emitter?.Prefab == null) { continue; } if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); } } } partial void UpdateProjSpecific(float deltaTime) { if (!body.Enabled) { return; } if (IsDead) { var spriteParams = Params.GetSprite(); if (spriteParams != null && spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) { deadTimer += deltaTime; } } if (InWater) { wetTimer = 1.0f; } else { wetTimer -= deltaTime * 0.1f; if (wetTimer > 0.0f) { dripParticleTimer += wetTimer * deltaTime * Mass * (wetTimer > 0.9f ? 50.0f : 5.0f); if (dripParticleTimer > 1.0f) { float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.Width, body.Height) : body.Radius; GameMain.ParticleManager.CreateParticle( "waterdrop", WorldPosition + Rand.Vector(Rand.Range(0.0f, ConvertUnits.ToDisplayUnits(dropRadius))), ConvertUnits.ToDisplayUnits(body.LinearVelocity), 0, character.CurrentHull); dripParticleTimer = 0.0f; } } } foreach (var conditionalSprite in ConditionalSprites) { conditionalSprite.CheckConditionals(); } if (LightSource != null) { LightSource.ParentSub = body.Submarine; LightSource.Rotation = (dir == Direction.Right) ? body.Rotation : body.Rotation - MathHelper.Pi; if (LightSource.LightSprite != null) { LightSource.LightSprite.Depth = ActiveSprite.Depth; } if (LightSource.DeformableLightSprite != null) { LightSource.DeformableLightSprite.Sprite.Depth = ActiveSprite.Depth; } LightSource.CheckConditionals(); } UpdateSpriteStates(deltaTime); RefreshDeformations(); } public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { var spriteParams = Params.GetSprite(); if (spriteParams == null || Alpha <= 0) { return; } float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float brightness = Math.Max(1.0f - burn, 0.2f); Color tintedColor = spriteParams.Color; if (!spriteParams.IgnoreTint) { tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color); if (character.Info != null) { tintedColor = tintedColor.Multiply(character.Info.Head.SkinColor); } if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head) { tintedColor = Color.Lerp(tintedColor, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f); } if (character.CharacterHealth.BodyTint.A > 0) { tintedColor = Color.Lerp(tintedColor, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f); } } Color color = new Color(tintedColor.Multiply(brightness), tintedColor.A); Color colorWithoutTint = new Color(spriteParams.Color.Multiply(brightness), spriteParams.Color.A); Color blankColor = new Color(brightness, brightness, brightness, 1); if (deadTimer > 0) { color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); colorWithoutTint = Color.Lerp(colorWithoutTint, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); } color = overrideColor ?? color; colorWithoutTint = overrideColor ?? colorWithoutTint; blankColor = overrideColor ?? blankColor; color *= Alpha; blankColor *= Alpha; if (isSevered) { if (severedFadeOutTimer > SeveredFadeOutTime) { if (LightSource != null) { LightSource.Enabled = false; } return; } else if (severedFadeOutTimer > SeveredFadeOutTime - 1.0f) { color *= SeveredFadeOutTime - severedFadeOutTimer; colorWithoutTint *= SeveredFadeOutTime - severedFadeOutTimer; } } float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || WearingItems.Any(w => w.HideLimb); bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); var activeSprite = ActiveSprite; if (type == LimbType.Head) { CalculateHeadPosition(activeSprite); } body.UpdateDrawPosition(); float depthStep = 0.000001f; if (!hideLimb) { var deformSprite = DeformSprite; if (deformSprite != null && !disableDeformations) { if (ActiveDeformations.Any()) { var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size); deformSprite.Deform(deformation); if (LightSource != null && LightSource.DeformableLightSprite != null) { deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size, dir == Direction.Left); LightSource.DeformableLightSprite.Deform(deformation); } } else { deformSprite.Reset(); } body.Draw(deformSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); } else { bool useTintMask = TintMask != null && spriteBatch.GetCurrentEffect() is null; if (useTintMask && Sprite?.Texture != null && TintMask?.Texture != null) { tintEffectParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; tintEffectParams.Params ??= new Dictionary(); var parameters = tintEffectParams.Params; parameters["xBaseTexture"] = Sprite.Texture; parameters["xTintMaskTexture"] = TintMask.Texture; if (drawHuskSprite && HuskMask != null) { parameters["xCutoffTexture"] = HuskMask.Texture; parameters["baseToCutoffSizeRatio"] = (float)Sprite.Texture.Width / (float)HuskMask.Texture.Width; } else { parameters["xCutoffTexture"] = GUI.WhiteTexture; parameters["baseToCutoffSizeRatio"] = 1.0f; } parameters["highlightThreshold"] = TintHighlightThreshold; parameters["highlightMultiplier"] = TintHighlightMultiplier; spriteBatch.SwapEffect(tintEffectParams); } body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); if (useTintMask) { spriteBatch.SwapEffect(null); } } // Handle non-exlusive, i.e. additional conditional sprites foreach (var conditionalSprite in ConditionalSprites) { // Exclusive conditional sprites are handled in the Properties if (conditionalSprite.Exclusive) { continue; } if (!conditionalSprite.IsActive) { continue; } if (conditionalSprite.DeformableSprite != null) { var defSprite = conditionalSprite.DeformableSprite; if (ActiveDeformations.Any()) { var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, defSprite.Size); defSprite.Deform(deformation); } else { defSprite.Reset(); } body.Draw(defSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); } else { body.Draw(spriteBatch, conditionalSprite.Sprite, color, depth: activeSprite.Depth - (depthStep * 50), Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); } } } SpriteEffects spriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; if (LightSource != null) { LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } float step = depthStep; WearableSprite onlyDrawable = WearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) { spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; } if (Params.MirrorVertically) { spriteEffect |= SpriteEffects.FlipVertically; } if (onlyDrawable == null) { if (HerpesSprite != null && !wearableTypesToHide.Contains(WearableType.Herpes) && herpesStrength > 0) { float alpha = Math.Min(herpesStrength * 2 / 100.0f, 1.0f); DrawWearable(HerpesSprite, depthStep, spriteBatch, blankColor, alpha: alpha, spriteEffect); depthStep += step; } if (drawHuskSprite) { bool useTintEffect = HuskMask != null && spriteBatch.GetCurrentEffect() is null; if (useTintEffect) { huskSpriteParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; huskSpriteParams.Params ??= new Dictionary(); var parameters = huskSpriteParams.Params; parameters["xCutoffTexture"] = GUI.WhiteTexture; parameters["baseToCutoffSizeRatio"] = 1.0f; spriteBatch.SwapEffect(huskSpriteParams); } DrawWearable(HuskSprite, depthStep, spriteBatch, color, alpha: color.A / 255f, spriteEffect); if (useTintEffect) { spriteBatch.SwapEffect(null); } depthStep += step; } foreach (WearableSprite wearable in OtherWearables) { if (wearable.Type == WearableType.Husk) { continue; } if (wearableTypesToHide.Contains(wearable.Type)) { if (wearable.Type == WearableType.Hair) { if (HairWithHatSprite != null && !hideLimb) { DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); depthStep += step; continue; } } else { continue; } } DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } } foreach (WearableSprite wearable in WearingItems) { if (onlyDrawable != null && onlyDrawable != wearable && wearable.CanBeHiddenByOtherWearables) { continue; } if (wearable.CanBeHiddenByItem.Any()) { bool hiddenByOtherItem = false; foreach (var otherWearable in WearingItems) { if (otherWearable == wearable) { continue; } if (wearable.CanBeHiddenByItem.Contains(otherWearable.WearableComponent.Item.Prefab.Identifier)) { hiddenByOtherItem = true; break; } foreach (Identifier tag in wearable.CanBeHiddenByItem) { if (otherWearable.WearableComponent.Item.HasTag(tag)) { hiddenByOtherItem = true; break; } } } if (hiddenByOtherItem) { continue; } } DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } if (!Hide && onlyDrawable == null) { foreach (var decorativeSprite in DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); if (deadTimer > 0) { c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); } c = overrideColor ?? c; float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; var ca = (float)Math.Cos(-body.Rotation); var sa = (float)Math.Sin(-body.Rotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, depth: activeSprite.Depth - depthStep); depthStep += step; } if (damageOverlayStrength > 0.0f && DamagedSprite != null) { DamagedSprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), colorWithoutTint * damageOverlayStrength, activeSprite.Origin, -body.DrawRotation, Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos } } if (GameMain.DebugDraw) { if (pullJoint != null) { Vector2 pos = ConvertUnits.ToDisplayUnits(pullJoint.WorldAnchorB); GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), GUIStyle.Red, true); } var bodyDrawPos = body.DrawPosition; bodyDrawPos.Y = -bodyDrawPos.Y; if (IsStuck) { Vector2 from = ConvertUnits.ToDisplayUnits(attachJoint.WorldAnchorA); from.Y = -from.Y; Vector2 to = ConvertUnits.ToDisplayUnits(attachJoint.WorldAnchorB); to.Y = -to.Y; var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); var front = ConvertUnits.ToDisplayUnits(body.FarseerBody.GetWorldPoint(localFront)); front.Y = -front.Y; GUI.DrawLine(spriteBatch, bodyDrawPos, front, Color.Yellow, width: 2); GUI.DrawLine(spriteBatch, from, to, GUIStyle.Red, width: 1); GUI.DrawRectangle(spriteBatch, new Rectangle((int)from.X, (int)from.Y, 12, 12), Color.White, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 12, 12), Color.White, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)from.X, (int)from.Y, 10, 10), Color.Blue, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 10, 10), GUIStyle.Red, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)front.X, (int)front.Y, 10, 10), Color.Yellow, true); //Vector2 mainLimbFront = ConvertUnits.ToDisplayUnits(ragdoll.MainLimb.body.FarseerBody.GetWorldPoint(ragdoll.MainLimb.body.GetFrontLocal(MathHelper.ToRadians(limbParams.Orientation)))); //mainLimbFront.Y = -mainLimbFront.Y; //var mainLimbDrawPos = ragdoll.MainLimb.body.DrawPosition; //mainLimbDrawPos.Y = -mainLimbDrawPos.Y; //GUI.DrawLine(spriteBatch, mainLimbDrawPos, mainLimbFront, Color.White, width: 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)mainLimbFront.X, (int)mainLimbFront.Y, 10, 10), Color.Yellow, true); } //DrawDamageModifiers(spriteBatch, cam, bodyDrawPos, isScreenSpace: false); } } public void UpdateWearableTypesToHide() { alphaClipEffectParams?.Clear(); wearableTypeHidingSprites.Clear(); void addWearablesFrom(IReadOnlyList wearableSprites) { if (wearableSprites.Count <= 0) { return; } wearableTypeHidingSprites.AddRange( wearableSprites.Where(w => w.HideWearablesOfType.Count > 0)); } addWearablesFrom(WearingItems); addWearablesFrom(OtherWearables); wearableTypesToHide.Clear(); if (wearableTypeHidingSprites.Count <= 0) { return; } foreach (WearableSprite sprite in wearableTypeHidingSprites) { wearableTypesToHide.UnionWith(sprite.HideWearablesOfType); } } private void UpdateSpriteStates(float deltaTime) { foreach (int spriteGroup in DecorativeSpriteGroups.Keys) { for (int i = 0; i < DecorativeSpriteGroups[spriteGroup].Count; i++) { var decorativeSprite = DecorativeSpriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } if (spriteGroup > 0) { // TODO //int activeSpriteIndex = ID % DecorativeSpriteGroups[spriteGroup].Count; //if (i != activeSpriteIndex) //{ // spriteAnimState[decorativeSprite].IsActive = false; // continue; //} } //check if the sprite is active (whether it should be drawn or not) var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = true; foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) { if (!conditional.Matches(this)) { spriteState.IsActive = false; break; } } if (!spriteState.IsActive) { continue; } //check if the sprite should be animated bool animate = true; foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) { if (!conditional.Matches(this)) { animate = false; break; } } if (!animate) { continue; } spriteState.OffsetState += deltaTime; spriteState.RotationState += deltaTime; } } } public void DrawDamageModifiers(SpriteBatch spriteBatch, Camera cam, Vector2 startPos, bool isScreenSpace) { foreach (var modifier in DamageModifiers) { //Vector2 up = VectorExtensions.Backward(-body.TransformedRotation + Params.GetSpriteOrientation() * Dir); //int width = 4; //if (!isScreenSpace) //{ // width = (int)Math.Round(width / cam.Zoom); //} //GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(up) * size, GUIStyle.Red, width: width); Color color = modifier.DamageMultiplier > 1 ? GUIStyle.Red : GUIStyle.Green; float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); if (isScreenSpace) { size *= cam.Zoom; } int thickness = 2; if (!isScreenSpace) { thickness = (int)Math.Round(thickness / cam.Zoom); } float bodyRotation = -body.Rotation; float constantOffset = -MathHelper.PiOver2; Vector2 armorSector = modifier.ArmorSectorInRadians; float armorSectorSize = Math.Abs(armorSector.X - armorSector.Y); float radians = armorSectorSize * Dir; float armorSectorOffset = armorSector.X * Dir; float finalOffset = bodyRotation + constantOffset + armorSectorOffset; ShapeExtensions.DrawSector(spriteBatch, startPos, size, radians, 40, color, finalOffset, thickness); } } private ( Color FinalColor, Vector2 Origin, float Rotation, float Scale, float Depth) CalculateDrawParameters(WearableSprite wearable, float depthStep, Color color, float alpha) { var sprite = ActiveSprite; if (wearable.InheritSourceRect) { if (wearable.SheetIndex.HasValue) { wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, wearable.SheetIndex.Value), sprite.SourceRect.Size); } else if (type == LimbType.Head && character.Info != null) { wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, character.Info.Head.SheetIndex.ToPoint()), sprite.SourceRect.Size); } else { wearable.Sprite.SourceRect = sprite.SourceRect; } } Vector2 origin; if (wearable.InheritOrigin) { origin = sprite.Origin; wearable.Sprite.Origin = origin; } else { origin = wearable.Sprite.Origin; // If the wearable inherits the origin, flipping is already handled. if (body.Dir == -1.0f) { origin.X = wearable.Sprite.SourceRect.Width - origin.X; } } float depth = wearable.Sprite.Depth; if (wearable.InheritLimbDepth) { depth = sprite.Depth - depthStep; Limb depthLimb = (wearable.DepthLimb == LimbType.None) ? this : character.AnimController.GetLimb(wearable.DepthLimb); if (depthLimb != null) { depth = depthLimb.ActiveSprite.Depth - depthStep; } } var wearableItemComponent = wearable.WearableComponent; Color wearableColor = Color.White; if (wearableItemComponent != null) { // Draw outer clothes on top of inner clothes. if (wearableItemComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)) { depth -= depthStep; } if (wearableItemComponent.AllowedSlots.Contains(InvSlotType.Bag)) { depth -= depthStep * 4; } wearableColor = wearableItemComponent.Item.GetSpriteColor(); } else if (character.Info != null) { if (wearable.Type == WearableType.Hair) { wearableColor = character.Info.Head.HairColor; } else if (wearable.Type == WearableType.Beard || wearable.Type == WearableType.Moustache) { wearableColor = character.Info.Head.FacialHairColor; } } float scale = wearable.Scale; if (wearable.InheritScale) { if (!wearable.IgnoreTextureScale) { scale *= TextureScale; } if (!wearable.IgnoreLimbScale) { scale *= Params.Scale; } if (!wearable.IgnoreRagdollScale) { scale *= ragdoll.RagdollParams.LimbScale; } } float rotation = -body.DrawRotation - wearable.Rotation * Dir; float finalAlpha = alpha * wearableColor.A; Color finalColor = color.Multiply(wearableColor); finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha); return (finalColor, origin, rotation, scale, depth); } private static Effect alphaClipEffect; private Dictionary> alphaClipEffectParams; private void ApplyAlphaClip(SpriteBatch spriteBatch, WearableSprite wearable, WearableSprite alphaClipper, SpriteEffects spriteEffect) { SpriteRecorder.Command makeCommand(WearableSprite w) { var (_, origin, rotation, scale, _) = CalculateDrawParameters(w, 0f, Color.White, 0f); var command = SpriteRecorder.Command.FromTransform( texture: w.Sprite.Texture, pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), srcRect: w.Sprite.SourceRect, color: Color.White, rotationRad: rotation, origin: origin, scale: new Vector2(scale, scale), effects: spriteEffect, depth: 0f, index: 0); return command; } void spacesFromCommand(WearableSprite w, SpriteRecorder.Command command, out CoordinateSpace2D textureSpace, out CoordinateSpace2D worldSpace) { var (topLeft, bottomLeft, topRight) = spriteEffect switch { SpriteEffects.None => (command.VertexTL, command.VertexBL, command.VertexTR), SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically => (command.VertexBR, command.VertexTR, command.VertexBL), SpriteEffects.FlipHorizontally => (command.VertexTR, command.VertexBR, command.VertexTL), SpriteEffects.FlipVertically => (command.VertexBL, command.VertexTL, command.VertexBR) }; textureSpace = new CoordinateSpace2D { Origin = topLeft.TextureCoordinate, I = topRight.TextureCoordinate - topLeft.TextureCoordinate, J = bottomLeft.TextureCoordinate - topLeft.TextureCoordinate }; worldSpace = new CoordinateSpace2D { Origin = topLeft.Position.DiscardZ(), I = topRight.Position.DiscardZ() - topLeft.Position.DiscardZ(), J = bottomLeft.Position.DiscardZ() - topLeft.Position.DiscardZ() }; } var wearableCommand = makeCommand(wearable); var clipperCommand = makeCommand(alphaClipper); spacesFromCommand(wearable, wearableCommand, out var wearableTextureSpace, out var wearableWorldSpace); spacesFromCommand(alphaClipper, clipperCommand, out var clipperTextureSpace, out var clipperWorldSpace); var wearableUvToClipperUv = wearableTextureSpace.CanonicalToLocal * wearableWorldSpace.LocalToCanonical * clipperWorldSpace.CanonicalToLocal * clipperTextureSpace.LocalToCanonical; alphaClipEffect ??= EffectLoader.Load("Effects/wearableclip"); alphaClipEffectParams ??= new Dictionary>(); if (!alphaClipEffectParams.ContainsKey(wearable)) { alphaClipEffectParams.Add(wearable, new Dictionary()); } var paramsToPass = new SpriteBatch.EffectWithParams { Effect = alphaClipEffect, Params = alphaClipEffectParams[wearable] }; paramsToPass.Params["wearableUvToClipperUv"] = wearableUvToClipperUv; paramsToPass.Params["stencilUVmin"] = new Vector2( (float)alphaClipper.Sprite.SourceRect.X / alphaClipper.Sprite.Texture.Width, (float)alphaClipper.Sprite.SourceRect.Y / alphaClipper.Sprite.Texture.Height); paramsToPass.Params["stencilUVmax"] = new Vector2( (float)alphaClipper.Sprite.SourceRect.Right / alphaClipper.Sprite.Texture.Width, (float)alphaClipper.Sprite.SourceRect.Bottom / alphaClipper.Sprite.Texture.Height); paramsToPass.Params["clipperTexelSize"] = 2f / alphaClipper.Sprite.Texture.Width; paramsToPass.Params["aCutoff"] = 2f / 255f; paramsToPass.Params["xTexture"] = wearable.Sprite.Texture; paramsToPass.Params["xStencil"] = alphaClipper.Sprite.Texture; spriteBatch.SwapEffect(paramsToPass); } private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) { if (wearable.Sprite?.Texture == null) { return; } var (finalColor, origin, rotation, scale, depth) = CalculateDrawParameters(wearable, depthStep, color, alpha); var prevEffect = spriteBatch.GetCurrentEffect(); var alphaClipper = WearingItems.Find(w => w.AlphaClipOtherWearables); bool shouldApplyAlphaClip = alphaClipper?.Sprite?.Texture != null && wearable != alphaClipper; if (shouldApplyAlphaClip) { ApplyAlphaClip(spriteBatch, wearable, alphaClipper, spriteEffect); } wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); if (shouldApplyAlphaClip) { spriteBatch.SwapEffect(effect: prevEffect); } } private WearableSprite GetWearableSprite(WearableType type) { var info = character.Info; if (info == null) { return null; } ContentXElement element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); return element != null ? new WearableSprite(element.GetChildElement("sprite"), type) : null; } partial void RemoveProjSpecific() { Sprite?.Remove(); Sprite = null; DamagedSprite?.Remove(); DamagedSprite = null; _deformSprite?.Sprite?.Remove(); _deformSprite = null; DecorativeSprites.ForEach(s => s.Remove()); ConditionalSprites.Clear(); ConditionalSprites.ForEach(s => s.Remove()); ConditionalSprites.Clear(); LightSource?.Remove(); LightSource = null; OtherWearables.ForEach(w => w.Sprite.Remove()); OtherWearables.Clear(); HuskSprite?.Sprite?.Remove(); HuskSprite = null; HairWithHatSprite?.Sprite?.Remove(); HairWithHatSprite = null; HerpesSprite?.Sprite?.Remove(); HerpesSprite = null; TintMask?.Remove(); TintMask = null; } } }