diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 52dba841d0..dfdfece979 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -1088,10 +1088,11 @@ namespace Content.Client.Lobby.UI if (Profile is null) return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + var strategy = _prototypeManager.Index(skin).Strategy; - switch (skin) + switch (strategy.InputType) { - case HumanoidSkinColor.HumanToned: + case SkinColorationStrategyInput.Unary: { if (!Skin.Visible) { @@ -1099,39 +1100,14 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = false; } - var color = SkinColor.HumanSkinTone((int) Skin.Value); - - Markings.CurrentSkinColor = color; - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));// - break; - } - case HumanoidSkinColor.Hues: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - Markings.CurrentSkinColor = _rgbSkinColorSelector.Color; - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color)); - break; - } - case HumanoidSkinColor.TintedHues: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color); + var color = strategy.FromUnary(Skin.Value); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + break; } - case HumanoidSkinColor.VoxFeathers: + case SkinColorationStrategyInput.Color: { if (!RgbSkinColorContainer.Visible) { @@ -1139,10 +1115,11 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = true; } - var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color); + var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + break; } } @@ -1321,10 +1298,11 @@ namespace Content.Client.Lobby.UI return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + var strategy = _prototypeManager.Index(skin).Strategy; - switch (skin) + switch (strategy.InputType) { - case HumanoidSkinColor.HumanToned: + case SkinColorationStrategyInput.Unary: { if (!Skin.Visible) { @@ -1332,11 +1310,11 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = false; } - Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor); + Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor); break; } - case HumanoidSkinColor.Hues: + case SkinColorationStrategyInput.Color: { if (!RgbSkinColorContainer.Visible) { @@ -1344,36 +1322,11 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = true; } - // set the RGB values to the direct values otherwise - _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; - break; - } - case HumanoidSkinColor.TintedHues: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - // set the RGB values to the direct values otherwise - _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; - break; - } - case HumanoidSkinColor.VoxFeathers: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor); + _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor); break; } } - } public void UpdateSpeciesGuidebookIcon() diff --git a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs index 66f9108365..583341c815 100644 --- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Numerics; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Robust.Shared.Prototypes; @@ -27,7 +28,7 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, public Color EyeColor { get; set; } = Color.Black; [DataField] - public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone; + public Color SkinColor { get; set; } = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f)); [DataField] public List Markings { get; set; } = new(); @@ -92,14 +93,13 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, public static HumanoidCharacterAppearance DefaultWithSpecies(string species) { - var speciesPrototype = IoCManager.Resolve().Index(species); - var skinColor = speciesPrototype.SkinColoration switch + var protoMan = IoCManager.Resolve(); + var speciesPrototype = protoMan.Index(species); + var skinColoration = protoMan.Index(speciesPrototype.SkinColoration).Strategy; + var skinColor = skinColoration.InputType switch { - HumanoidSkinColor.HumanToned => Humanoid.SkinColor.HumanSkinTone(speciesPrototype.DefaultHumanSkinTone), - HumanoidSkinColor.Hues => speciesPrototype.DefaultSkinTone, - HumanoidSkinColor.TintedHues => Humanoid.SkinColor.TintedHues(speciesPrototype.DefaultSkinTone), - HumanoidSkinColor.VoxFeathers => Humanoid.SkinColor.ClosestVoxColor(speciesPrototype.DefaultSkinTone), - _ => Humanoid.SkinColor.ValidHumanSkinTone, + SkinColorationStrategyInput.Unary => skinColoration.FromUnary(speciesPrototype.DefaultHumanSkinTone), + SkinColorationStrategyInput.Color => skinColoration.ClosestSkinColor(speciesPrototype.DefaultSkinTone), }; return new( @@ -147,23 +147,15 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, var newEyeColor = random.Pick(RealisticEyeColors); - var skinType = IoCManager.Resolve().Index(species).SkinColoration; + var protoMan = IoCManager.Resolve(); + var skinType = protoMan.Index(species).SkinColoration; + var strategy = protoMan.Index(skinType).Strategy; - var newSkinColor = new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1); - switch (skinType) + var newSkinColor = strategy.InputType switch { - case HumanoidSkinColor.HumanToned: - newSkinColor = Humanoid.SkinColor.HumanSkinTone(random.Next(0, 101)); - break; - case HumanoidSkinColor.Hues: - break; - case HumanoidSkinColor.TintedHues: - newSkinColor = Humanoid.SkinColor.ValidTintedHuesSkinTone(newSkinColor); - break; - case HumanoidSkinColor.VoxFeathers: - newSkinColor = Humanoid.SkinColor.ProportionalVoxColor(newSkinColor); - break; - } + SkinColorationStrategyInput.Unary => strategy.FromUnary(random.NextFloat(0f, 100f)), + SkinColorationStrategyInput.Color => strategy.ClosestSkinColor(new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1)), + }; return new HumanoidCharacterAppearance(newHairStyle, newHairColor, newFacialHairStyle, newHairColor, newEyeColor, newSkinColor, new ()); @@ -207,10 +199,8 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, markingSet = new MarkingSet(appearance.Markings, speciesProto.MarkingPoints, markingManager, proto); markingSet.EnsureValid(markingManager); - if (!Humanoid.SkinColor.VerifySkinColor(speciesProto.SkinColoration, skinColor)) - { - skinColor = Humanoid.SkinColor.ValidSkinTone(speciesProto.SkinColoration, skinColor); - } + var strategy = proto.Index(speciesProto.SkinColoration).Strategy; + skinColor = strategy.EnsureVerified(skinColor); markingSet.EnsureSpecies(species, skinColor, markingManager); markingSet.EnsureSexes(sex, markingManager); diff --git a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs index 0c63a88d5b..a23ecdfc53 100644 --- a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs +++ b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs @@ -81,7 +81,7 @@ public sealed partial class SpeciesPrototype : IPrototype /// Method of skin coloration used by the species. /// [DataField(required: true)] - public HumanoidSkinColor SkinColoration { get; private set; } + public ProtoId SkinColoration { get; private set; } [DataField] public ProtoId MaleFirstNames { get; private set; } = "NamesFirstMale"; diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 7a22c0c29e..e88b99b593 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -297,9 +297,10 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem return; } - if (verify && !SkinColor.VerifySkinColor(species.SkinColoration, skinColor)) + if (verify && _proto.Resolve(species.SkinColoration, out var index)) { - skinColor = SkinColor.ValidSkinTone(species.SkinColoration, skinColor); + var strategy = index.Strategy; + skinColor = strategy.EnsureVerified(skinColor); } humanoid.SkinColor = skinColor; diff --git a/Content.Shared/Humanoid/SkinColor.cs b/Content.Shared/Humanoid/SkinColor.cs deleted file mode 100644 index d4d52682f3..0000000000 --- a/Content.Shared/Humanoid/SkinColor.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Numerics; -using System.Security.Cryptography; -using Microsoft.VisualBasic.CompilerServices; - -namespace Content.Shared.Humanoid; - -public static class SkinColor -{ - public const float MaxTintedHuesSaturation = 0.1f; - public const float MinTintedHuesLightness = 0.85f; - - public const float MinHuesLightness = 0.175f; - - public const float MinFeathersHue = 29f / 360; - public const float MaxFeathersHue = 174f / 360; - public const float MinFeathersSaturation = 20f / 100; - public const float MaxFeathersSaturation = 88f / 100; - public const float MinFeathersValue = 36f / 100; - public const float MaxFeathersValue = 55f / 100; - - public static Color ValidHumanSkinTone => Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f)); - - /// - /// Turn a color into a valid tinted hue skin tone. - /// - /// The color to validate - /// Validated tinted hue skin tone - public static Color ValidTintedHuesSkinTone(Color color) - { - return TintedHues(color); - } - - /// - /// Get a human skin tone based on a scale of 0 to 100. The value is clamped between 0 and 100. - /// - /// Skin tone. Valid range is 0 to 100, inclusive. 0 is gold/yellowish, 100 is dark brown. - /// A human skin tone. - public static Color HumanSkinTone(int tone) - { - // 0 - 100, 0 being gold/yellowish and 100 being dark - // HSV based - // - // 0 - 20 changes the hue - // 20 - 100 changes the value - // 0 is 45 - 20 - 100 - // 20 is 25 - 20 - 100 - // 100 is 25 - 100 - 20 - - tone = Math.Clamp(tone, 0, 100); - - var rangeOffset = tone - 20; - - float hue = 25; - float sat = 20; - float val = 100; - - if (rangeOffset <= 0) - { - hue += Math.Abs(rangeOffset); - } - else - { - sat += rangeOffset; - val -= rangeOffset; - } - - var color = Color.FromHsv(new Vector4(hue / 360, sat / 100, val / 100, 1.0f)); - - return color; - } - - /// - /// Gets a human skin tone from a given color. - /// - /// - /// - /// - /// Does not cause an exception if the color is not originally from the human color range. - /// Instead, it will return the approximation of the skin tone value. - /// - public static float HumanSkinToneFromColor(Color color) - { - var hsv = Color.ToHsv(color); - // check for hue/value first, if hue is lower than this percentage - // and value is 1.0 - // then it'll be hue - if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f - && hsv.Z == 1.0) - { - return Math.Abs(45 - (hsv.X * 360)); - } - // otherwise it'll directly be the saturation - else - { - return hsv.Y * 100; - } - } - - /// - /// Verify if a color is in the human skin tone range. - /// - /// The color to verify - /// True if valid, false otherwise. - public static bool VerifyHumanSkinTone(Color color) - { - var colorValues = Color.ToHsv(color); - - var hue = Math.Round(colorValues.X * 360f); - var sat = Math.Round(colorValues.Y * 100f); - var val = Math.Round(colorValues.Z * 100f); - // rangeOffset makes it so that this value - // is 25 <= hue <= 45 - if (hue < 25 || hue > 45) - { - return false; - } - - // rangeOffset makes it so that these two values - // are 20 <= sat <= 100 and 20 <= val <= 100 - // where saturation increases to 100 and value decreases to 20 - if (sat < 20 || val < 20) - { - return false; - } - - return true; - } - - /// - /// Convert a color to the 'tinted hues' skin tone type. - /// - /// Color to convert - /// Tinted hue color - public static Color TintedHues(Color color) - { - var newColor = Color.ToHsl(color); - newColor.Y *= MaxTintedHuesSaturation; - newColor.Z = MathHelper.Lerp(MinTintedHuesLightness, 1f, newColor.Z); - - return Color.FromHsv(newColor); - } - - /// - /// Verify if this color is a valid tinted hue color type, or not. - /// - /// The color to verify - /// True if valid, false otherwise - public static bool VerifyTintedHues(Color color) - { - // tinted hues just ensures saturation is always .1, or 10% saturation at all times - return Color.ToHsl(color).Y <= MaxTintedHuesSaturation && Color.ToHsl(color).Z >= MinTintedHuesLightness; - } - - /// - /// Converts a Color proportionally to the allowed vox color range. - /// Will NOT preserve the specific input color even if it is within the allowed vox color range. - /// - /// Color to convert - /// Vox feather coloration - public static Color ProportionalVoxColor(Color color) - { - var newColor = Color.ToHsv(color); - - newColor.X = newColor.X * (MaxFeathersHue - MinFeathersHue) + MinFeathersHue; - newColor.Y = newColor.Y * (MaxFeathersSaturation - MinFeathersSaturation) + MinFeathersSaturation; - newColor.Z = newColor.Z * (MaxFeathersValue - MinFeathersValue) + MinFeathersValue; - - return Color.FromHsv(newColor); - } - - // /// - // /// Ensures the input Color is within the allowed vox color range. - // /// - // /// Color to convert - // /// The same Color if it was within the allowed range, or the closest matching Color otherwise - public static Color ClosestVoxColor(Color color) - { - var hsv = Color.ToHsv(color); - - hsv.X = Math.Clamp(hsv.X, MinFeathersHue, MaxFeathersHue); - hsv.Y = Math.Clamp(hsv.Y, MinFeathersSaturation, MaxFeathersSaturation); - hsv.Z = Math.Clamp(hsv.Z, MinFeathersValue, MaxFeathersValue); - - return Color.FromHsv(hsv); - } - - /// - /// Verify if this color is a valid vox feather coloration, or not. - /// - /// The color to verify - /// True if valid, false otherwise - public static bool VerifyVoxFeathers(Color color) - { - var colorHsv = Color.ToHsv(color); - - if (colorHsv.X < MinFeathersHue || colorHsv.X > MaxFeathersHue) - return false; - - if (colorHsv.Y < MinFeathersSaturation || colorHsv.Y > MaxFeathersSaturation) - return false; - - if (colorHsv.Z < MinFeathersValue || colorHsv.Z > MaxFeathersValue) - return false; - - return true; - } - - /// - /// This takes in a color, and returns a color guaranteed to be above MinHuesLightness - /// - /// - /// Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness - public static Color MakeHueValid(Color color) - { - var manipulatedColor = Color.ToHsv(color); - manipulatedColor.Z = Math.Max(manipulatedColor.Z, MinHuesLightness); - return Color.FromHsv(manipulatedColor); - } - - /// - /// Verify if this color is above a minimum luminosity - /// - /// - /// True if valid, false if not - public static bool VerifyHues(Color color) - { - return Color.ToHsv(color).Z >= MinHuesLightness; - } - - public static bool VerifySkinColor(HumanoidSkinColor type, Color color) - { - return type switch - { - HumanoidSkinColor.HumanToned => VerifyHumanSkinTone(color), - HumanoidSkinColor.TintedHues => VerifyTintedHues(color), - HumanoidSkinColor.Hues => VerifyHues(color), - HumanoidSkinColor.VoxFeathers => VerifyVoxFeathers(color), - _ => false, - }; - } - - public static Color ValidSkinTone(HumanoidSkinColor type, Color color) - { - return type switch - { - HumanoidSkinColor.HumanToned => ValidHumanSkinTone, - HumanoidSkinColor.TintedHues => ValidTintedHuesSkinTone(color), - HumanoidSkinColor.Hues => MakeHueValid(color), - HumanoidSkinColor.VoxFeathers => ClosestVoxColor(color), - _ => color - }; - } -} - -public enum HumanoidSkinColor : byte -{ - HumanToned, - Hues, - VoxFeathers, // Vox feathers are limited to a specific color range - TintedHues, //This gives a color tint to a humanoid's skin (10% saturation with full hue range). -} diff --git a/Content.Shared/Humanoid/SkinColorationPrototype.cs b/Content.Shared/Humanoid/SkinColorationPrototype.cs new file mode 100644 index 0000000000..e37265cea1 --- /dev/null +++ b/Content.Shared/Humanoid/SkinColorationPrototype.cs @@ -0,0 +1,302 @@ +using System.Numerics; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Humanoid; + +/// +/// A prototype containing a SkinColorationStrategy +/// +[Prototype] +public sealed partial class SkinColorationPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The skin coloration strategy specified by this prototype + /// + [DataField(required: true)] + public ISkinColorationStrategy Strategy = default!; +} + +/// +/// The type of input taken by a +/// +[Serializable, NetSerializable] +public enum SkinColorationStrategyInput +{ + /// + /// A single floating point number from 0 to 100 (inclusive) + /// + Unary, + + /// + /// A + /// + Color, +} + +/// +/// Takes in the given and returns an adjusted Color +/// +public interface ISkinColorationStrategy +{ + /// + /// The type of input expected by the implementor; callers should consult InputType before calling the methods that require a given input + /// + SkinColorationStrategyInput InputType { get; } + + /// + /// Returns whether or not the provided is within bounds of this strategy + /// + bool VerifySkinColor(Color color); + + /// + /// Returns the closest skin color that this strategy would provide to the given + /// + Color ClosestSkinColor(Color color); + + /// + /// Returns the input if it passes , otherwise returns + /// + Color EnsureVerified(Color color) + { + if (VerifySkinColor(color)) + { + return color; + } + + return ClosestSkinColor(color); + } + + /// + /// Returns a colour representation of the given unary input + /// + Color FromUnary(float unary) + { + throw new InvalidOperationException("This coloration strategy does not support unary input"); + } + + /// + /// Returns a colour representation of the given unary input + /// + float ToUnary(Color color) + { + throw new InvalidOperationException("This coloration strategy does not support unary input"); + } +} + +/// +/// Unary coloration strategy that returns human skin tones, with 0 being lightest and 100 being darkest +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy +{ + [DataField] + public Color ValidHumanSkinTone = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f)); + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Unary; + + public bool VerifySkinColor(Color color) + { + var colorValues = Color.ToHsv(color); + + var hue = Math.Round(colorValues.X * 360f); + var sat = Math.Round(colorValues.Y * 100f); + var val = Math.Round(colorValues.Z * 100f); + // rangeOffset makes it so that this value + // is 25 <= hue <= 45 + if (hue < 25f || hue > 45f) + { + return false; + } + + // rangeOffset makes it so that these two values + // are 20 <= sat <= 100 and 20 <= val <= 100 + // where saturation increases to 100 and value decreases to 20 + if (sat < 20f || val < 20f) + { + return false; + } + + return true; + } + + public Color ClosestSkinColor(Color color) + { + return ValidHumanSkinTone; + } + + public Color FromUnary(float color) + { + // 0 - 100, 0 being gold/yellowish and 100 being dark + // HSV based + // + // 0 - 20 changes the hue + // 20 - 100 changes the value + // 0 is 45 - 20 - 100 + // 20 is 25 - 20 - 100 + // 100 is 25 - 100 - 20 + + var tone = Math.Clamp(color, 0f, 100f); + + var rangeOffset = tone - 20f; + + var hue = 25f; + var sat = 20f; + var val = 100f; + + if (rangeOffset <= 0) + { + hue += Math.Abs(rangeOffset); + } + else + { + sat += rangeOffset; + val -= rangeOffset; + } + + return Color.FromHsv(new Vector4(hue / 360f, sat / 100f, val / 100f, 1.0f)); + } + + public float ToUnary(Color color) + { + var hsv = Color.ToHsv(color); + // check for hue/value first, if hue is lower than this percentage + // and value is 1.0 + // then it'll be hue + if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f + && hsv.Z == 1.0) + { + return Math.Abs(45 - (hsv.X * 360)); + } + // otherwise it'll directly be the saturation + else + { + return hsv.Y * 100; + } + } +} + +/// +/// Unary coloration strategy that clamps the color within the HSV colorspace +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class ClampedHsvColoration : ISkinColorationStrategy +{ + /// + /// The (min, max) of the hue channel. + /// + [DataField] + public (float, float)? Hue; + + /// + /// The (min, max) of the saturation channel. + /// + [DataField] + public (float, float)? Saturation; + + /// + /// The (min, max) of the value channel. + /// + [DataField] + public (float, float)? Value; + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; + + public bool VerifySkinColor(Color color) + { + var hsv = Color.ToHsv(color); + + if (Hue is (var minHue, var maxHue) && (hsv.X < minHue || hsv.X > maxHue)) + return false; + + if (Saturation is (var minSaturation, var maxSaturation) && (hsv.Y < minSaturation || hsv.Y > maxSaturation)) + return false; + + if (Value is (var minValue, var maxValue) && (hsv.Z < minValue || hsv.Z > maxValue)) + return false; + + return true; + } + + public Color ClosestSkinColor(Color color) + { + var hsv = Color.ToHsv(color); + + if (Hue is (var minHue, var maxHue)) + hsv.X = Math.Clamp(hsv.X, minHue, maxHue); + + if (Saturation is (var minSaturation, var maxSaturation)) + hsv.Y = Math.Clamp(hsv.Y, minSaturation, maxSaturation); + + if (Value is (var minValue, var maxValue)) + hsv.Z = Math.Clamp(hsv.Z, minValue, maxValue); + + return Color.FromHsv(hsv); + } +} + +/// +/// Unary coloration strategy that clamps the color within the HSL colorspace +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class ClampedHslColoration : ISkinColorationStrategy +{ + /// + /// The (min, max) of the hue channel. + /// + [DataField] + public (float, float)? Hue; + + /// + /// The (min, max) of the saturation channel. + /// + [DataField] + public (float, float)? Saturation; + + /// + /// The (min, max) of the lightness channel. + /// + [DataField] + public (float, float)? Lightness; + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; + + public bool VerifySkinColor(Color color) + { + var hsl = Color.ToHsl(color); + + if (Hue is (var minHue, var maxHue) && (hsl.X < minHue || hsl.X > maxHue)) + return false; + + if (Saturation is (var minSaturation, var maxSaturation) && (hsl.Y < minSaturation || hsl.Y > maxSaturation)) + return false; + + if (Lightness is (var minValue, var maxValue) && (hsl.Z < minValue || hsl.Z > maxValue)) + return false; + + return true; + } + + public Color ClosestSkinColor(Color color) + { + var hsl = Color.ToHsl(color); + + if (Hue is (var minHue, var maxHue)) + hsl.X = Math.Clamp(hsl.X, minHue, maxHue); + + if (Saturation is (var minSaturation, var maxSaturation)) + hsl.Y = Math.Clamp(hsl.Y, minSaturation, maxSaturation); + + if (Lightness is (var minValue, var maxValue)) + hsl.Z = Math.Clamp(hsl.Z, minValue, maxValue); + + return Color.FromHsl(hsl); + } +} diff --git a/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs b/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs index e13825ea28..63cefac812 100644 --- a/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs +++ b/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs @@ -9,16 +9,20 @@ public sealed class SkinTonesTest [Test] public void TestHumanSkinToneValidity() { + var strategy = new HumanTonedSkinColoration(); + for (var i = 0; i <= 100; i++) { - var color = SkinColor.HumanSkinTone(i); - Assert.That(SkinColor.VerifyHumanSkinTone(color)); + var color = strategy.FromUnary(i); + Assert.That(strategy.VerifySkinColor(color)); } } [Test] public void TestDefaultSkinToneValid() { - Assert.That(SkinColor.VerifyHumanSkinTone(SkinColor.ValidHumanSkinTone)); + var strategy = new HumanTonedSkinColoration(); + + Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone)); } } diff --git a/Resources/Prototypes/Species/skin_colorations.yml b/Resources/Prototypes/Species/skin_colorations.yml new file mode 100644 index 0000000000..c4b7c7b22d --- /dev/null +++ b/Resources/Prototypes/Species/skin_colorations.yml @@ -0,0 +1,21 @@ +- type: skinColoration + id: Hues + strategy: !type:ClampedHsvColoration + value: [0.175, 1] + +- type: skinColoration + id: TintedHues + strategy: !type:ClampedHslColoration + saturation: [0, 0.1] + lightness: [0.85, 1] + +- type: skinColoration + id: VoxFeathers + strategy: !type:ClampedHsvColoration + hue: [0.081, 0.48] + saturation: [0.2, 0.8] + value: [0.36, 0.55] + +- type: skinColoration + id: HumanToned + strategy: !type:HumanTonedSkinColoration {}