diff --git a/Content.IntegrationTests/Fixtures/GameTest.cs b/Content.IntegrationTests/Fixtures/GameTest.cs index 55903cf4a2..0c33782aee 100644 --- a/Content.IntegrationTests/Fixtures/GameTest.cs +++ b/Content.IntegrationTests/Fixtures/GameTest.cs @@ -250,6 +250,7 @@ public abstract partial class GameTest catch (Exception) { _pairDestroyed = true; + Assert.Fail(); throw; } finally diff --git a/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs b/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs index 417d9f0641..ce7ad47269 100644 --- a/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs +++ b/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.IntegrationTests.Utility; using Content.Shared.Body; using Content.Shared.Humanoid; @@ -21,10 +23,10 @@ public sealed class HumanoidProfileTests : GameTest private static readonly ProtoId Vox = "Vox"; private static string[] _species = GameDataScrounger.PrototypesOfKind(); - private BodySystem _bodySystem; - private HumanoidProfileSystem _humanoidProfile; - private MarkingManager _markingManager; - private SharedVisualBodySystem _visualBody; + [SidedDependency(Side.Server)] private BodySystem _bodySystem = default!; + [SidedDependency(Side.Server)] private HumanoidProfileSystem _humanoidProfile = default!; + [SidedDependency(Side.Server)] private MarkingManager _markingManager = default!; + [SidedDependency(Side.Server)] private SharedVisualBodySystem _visualBody = default!; [Test] public async Task EnsureValidLoading() @@ -36,17 +38,15 @@ public sealed class HumanoidProfileTests : GameTest await server.WaitAssertion(() => { - var entityManager = server.ResolveDependency(); - var humanoidProfile = entityManager.System(); - var human = entityManager.Spawn(BaseSpecies); - humanoidProfile.ApplyProfileTo(human, + LoadDependencies(out var body, out var humanoidComponent); + + _humanoidProfile.ApplyProfileTo(body, new HumanoidCharacterProfile() .WithSex(Sex.Female) .WithAge(67) .WithGender(Gender.Neuter) .WithSpecies(Vox)); - var humanoidComponent = entityManager.GetComponent(human); - var voiceComponent = entityManager.GetComponent(human); + var voiceComponent = SEntMan.GetComponent(body); Assert.That(humanoidComponent.Age, Is.EqualTo(67)); Assert.That(humanoidComponent.Sex, Is.EqualTo(Sex.Female)); @@ -71,6 +71,7 @@ public sealed class HumanoidProfileTests : GameTest await server.WaitAssertion(() => { LoadDependencies(out var body, out var humanoidComponent); + var profile = HumanoidCharacterProfile.Random(); _humanoidProfile.ApplyProfileTo(body, profile); _visualBody.ApplyProfileTo(body, profile); @@ -91,17 +92,17 @@ public sealed class HumanoidProfileTests : GameTest { LoadDependencies(out var body, out var humanoidComponent); - var proto = Server.ProtoMan.Index(species); + var proto = SProtoMan.Index(species); var profile = HumanoidCharacterProfile.RandomWithSpecies(species); _humanoidProfile.ApplyProfileTo(body, profile); _visualBody.ApplyProfileTo(body, profile); - Assert.That(humanoidComponent.Age, Is.LessThanOrEqualTo(proto.MaxAge)); - Assert.That(humanoidComponent.Age, Is.GreaterThanOrEqualTo(proto.MinAge)); - Assert.That(proto.Sexes.Contains(humanoidComponent.Sex), Is.True); - Assert.That(humanoidComponent.Species, Is.EqualTo(species)); + Assert.That(humanoidComponent.Age, Is.LessThanOrEqualTo(proto.MaxAge), $"Expected age is above the maximum age limit! Current: {humanoidComponent.Age} Max: {proto.MaxAge}"); + Assert.That(humanoidComponent.Age, Is.GreaterThanOrEqualTo(proto.MinAge), $"Expected age is below the minimum age limit! Current: {humanoidComponent.Age} Min: {proto.MinAge}"); + Assert.That(proto.Sexes.Contains(humanoidComponent.Sex), Is.True, $"Character has sex not found in the species prototype! Current: {humanoidComponent.Sex}"); + Assert.That(humanoidComponent.Species, Is.EqualTo(species), $"Species does not match! Expected: {species} Current: {humanoidComponent.Species}"); var strategy = Server.ProtoMan.Index(proto.SkinColoration).Strategy; - Assert.That(strategy.VerifySkinColor(profile.Appearance.SkinColor), Is.True); + Assert.That(strategy.VerifySkinColor(profile.Appearance.SkinColor, out var reason), Is.True, $"Failed to verify the skin color from strategy {strategy}. Reason: {reason}"); AssertValidProfile((body, humanoidComponent), profile); }); @@ -109,29 +110,24 @@ public sealed class HumanoidProfileTests : GameTest private void LoadDependencies(out EntityUid body, out HumanoidProfileComponent humanoidComponent) { - var entityManager = Server.ResolveDependency(); - _humanoidProfile = entityManager.System(); - _markingManager = Server.ResolveDependency(); - _visualBody = entityManager.System(); - _bodySystem = entityManager.System(); - body = entityManager.Spawn(BaseSpecies); - humanoidComponent = entityManager.GetComponent(body); + body = SEntMan.Spawn(BaseSpecies); + humanoidComponent = SEntMan.GetComponent(body); } private void AssertValidProfile(Entity body, HumanoidCharacterProfile profile) { _bodySystem.TryGetOrgansWithComponent(body.Owner, out var organs); - foreach (var (_, visualOrgan) in organs) + foreach (var (uid, visualOrgan) in organs) { - Assert.That(visualOrgan.Profile.Sex, Is.EqualTo(profile.Sex)); - Assert.That(visualOrgan.Profile.EyeColor, Is.EqualTo(profile.Appearance.EyeColor)); - Assert.That(visualOrgan.Profile.SkinColor, Is.EqualTo(profile.Appearance.SkinColor)); + Assert.That(visualOrgan.Profile.Sex, Is.EqualTo(profile.Sex), $"Organ {uid} has invalid sex appearance! Expected: {profile.Sex} Current: {visualOrgan.Profile.Sex}"); + Assert.That(visualOrgan.Profile.EyeColor, Is.EqualTo(profile.Appearance.EyeColor), $"Organ {uid} has invalid eye color! Expected: {profile.Appearance.EyeColor} Current: {visualOrgan.Profile.EyeColor}"); + Assert.That(visualOrgan.Profile.SkinColor, Is.EqualTo(profile.Appearance.SkinColor), $"Organ {uid} has invalid skin color! Expected: {profile.Appearance.SkinColor} Current: {visualOrgan.Profile.SkinColor}"); } _bodySystem.TryGetOrgansWithComponent(body.Owner, out var markings); - foreach (var (_, markingOrgan) in markings) + foreach (var (uid, markingOrgan) in markings) { // Needed to avoid access restrictions var data = markingOrgan.MarkingData; @@ -143,9 +139,9 @@ public sealed class HumanoidProfileTests : GameTest { var markingProto = Server.ProtoMan.Index(marking.MarkingId); - Assert.That(markingProto.Sprites.Count, Is.EqualTo(marking.MarkingColors.Count)); - Assert.That(_markingManager.CanBeApplied(data.Group, profile.Sex, markingProto), Is.True); - Assert.That(data.Layers.Contains(markingProto.BodyPart), Is.True); + Assert.That(markingProto.Sprites.Count, Is.EqualTo(marking.MarkingColors.Count), $"Organ {uid} has invald amount of marking sprites! Expected: {marking.MarkingColors.Count} Current: {markingProto.Sprites.Count}"); + Assert.That(_markingManager.CanBeApplied(data.Group, profile.Sex, markingProto), Is.True, $"Marking {markingProto.ID} cannot be applied to group {data.Group.Id} with sex {profile.Sex}"); + Assert.That(data.Layers.Contains(markingProto.BodyPart), Is.True, $"Organ {uid} marking visual layers do not contain an entry for {markingProto.BodyPart}"); if (!markingProto.ForcedColoring && groupProto.Appearances.GetValueOrDefault(markingProto.BodyPart)?.MatchSkin != true) freeMarkings.Add(marking); @@ -172,7 +168,7 @@ public sealed class HumanoidProfileTests : GameTest Is.EqualTo(MarkingColoring.GetMarkingLayerColors(markingProto, profile.Appearance.SkinColor, profile.Appearance.EyeColor, markingOrgan.AppliedMarkings))); if (markingProto.SexRestriction != null) - Assert.That(markingProto.SexRestriction, Is.EqualTo(profile.Sex)); + Assert.That(markingProto.SexRestriction, Is.EqualTo(profile.Sex), $"Marking {markingProto.ID} has invalid sex restriction! Expected: {profile.Sex} Current: {markingProto.SexRestriction}"); } } } diff --git a/Content.Shared/Humanoid/SkinColorationPrototype.cs b/Content.Shared/Humanoid/SkinColorationPrototype.cs index 6d4619ac2e..4a1c5bcf02 100644 --- a/Content.Shared/Humanoid/SkinColorationPrototype.cs +++ b/Content.Shared/Humanoid/SkinColorationPrototype.cs @@ -1,6 +1,6 @@ -using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; -using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -51,8 +51,9 @@ public interface ISkinColorationStrategy /// /// Returns whether or not the provided is within bounds of this strategy + /// Outs a reason if the verification fails. /// - bool VerifySkinColor(Color color); + bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason); /// /// Returns the closest skin color that this strategy would provide to the given @@ -64,7 +65,7 @@ public interface ISkinColorationStrategy /// Color EnsureVerified(Color color) { - if (VerifySkinColor(color)) + if (VerifySkinColor(color, out _)) { return color; } @@ -101,8 +102,10 @@ public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Unary; - public bool VerifySkinColor(Color color) + public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason) { + reason = null; + var colorValues = Color.ToHsv(color); var hue = Math.Round(colorValues.X * 360f); @@ -112,6 +115,7 @@ public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy // is 25 <= hue <= 45 if (hue < 25f || hue > 45f) { + reason = $"Hue {hue} is outside of expected ranges 25 and 45."; return false; } @@ -120,6 +124,7 @@ public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy // where saturation increases to 100 and value decreases to 20 if (sat < 20f || val < 20f) { + reason = "Saturation or value are below expected number of 20."; return false; } @@ -212,18 +217,29 @@ public sealed partial class ClampedHsvColoration : ISkinColorationStrategy public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; - public bool VerifySkinColor(Color color) + public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason) { + reason = null; + var hsv = Color.ToHsv(color); if (Hue is (var minHue, var maxHue) && !SkinColorationUtils.IsHueInRange(hsv.X, minHue, maxHue)) + { + reason = $"Hue {Hue} is outside of range of min {minHue} max {maxHue}"; return false; + } if (Saturation is (var minSat, var maxSat) && (hsv.Y < minSat - SkinColorationUtils.Epsilon || hsv.Y > maxSat + SkinColorationUtils.Epsilon)) + { + reason = $"Saturation {Saturation} is outside of range of min {minSat} max {maxSat}"; return false; + } if (Value is (var minVal, var maxVal) && (hsv.Z < minVal - SkinColorationUtils.Epsilon || hsv.Z > maxVal + SkinColorationUtils.Epsilon)) + { + reason = $"Value {Value} is outside of range of min {minVal} max {maxVal}"; return false; + } return true; } @@ -271,18 +287,29 @@ public sealed partial class ClampedHslColoration : ISkinColorationStrategy public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; - public bool VerifySkinColor(Color color) + public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason) { + reason = null; + var hsl = Color.ToHsl(color); if (Hue is (var minHue, var maxHue) && !SkinColorationUtils.IsHueInRange(hsl.X, minHue, maxHue)) + { + reason = $"Hue {Hue} is outside of range of min {minHue} max {maxHue}"; return false; + } if (Saturation is (var minSat, var maxSat) && (hsl.Y < minSat - SkinColorationUtils.Epsilon || hsl.Y > maxSat + SkinColorationUtils.Epsilon)) + { + reason = $"Saturation {Saturation} is outside of range of min {minSat} max {maxSat}"; return false; + } if (Lightness is (var minLight, var maxLight) && (hsl.Z < minLight - SkinColorationUtils.Epsilon || hsl.Z > maxLight + SkinColorationUtils.Epsilon)) + { + reason = $"Lightness {Lightness} is outside of range of min {minLight} max {maxLight}"; return false; + } return true; } @@ -302,6 +329,177 @@ public sealed partial class ClampedHslColoration : ISkinColorationStrategy } } +/// +/// Coloration strategy that clamps the color between nodes within the HSV colorspace. +/// Clamped values depend on the nodes and are linearly interpolated between them. +/// +/// +/// For example: +/// A node at hue 0 with a saturation of [0,1] and a node at hue 1 with a staturation of [0.1,0.8] +/// At hue 0.5, the saturation would be clamped within [0.05,0.9] +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class HueNodeClampedHsvColoration : ISkinColorationStrategy +{ + /// + /// List of valid nodes in this coloration. + /// + [DataField(required: true)] + public List Nodes = default!; + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; + + public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason) + { + reason = null; + var hsv = Color.ToHsv(color); + + // Clamp the hue between the first and last node. + // We don't want anything going outside of these values. + var hue = SkinColorationUtils.ClampHue(hsv.X, Nodes.First().Hue, Nodes.Last().Hue); + + var range = GetNodeValuesForHue(hue); + + // If no range was found, this color is invalid. + if (range is null) + { + reason = "No valid range was found."; + return false; + } + + // If a range is found, check if the saturation is within the provided ranges. + if (hsv.Y < range.Saturation.Min - SkinColorationUtils.Epsilon || hsv.Y > range.Saturation.Max + SkinColorationUtils.Epsilon) + { + reason = $"Saturation {hsv.Y} is outside of range of min {range.Saturation.Item1} max {range.Saturation.Item2}"; + return false; + } + + // Check if the value is within provided ranges. + if (hsv.Z < range.Value.Min - SkinColorationUtils.Epsilon || hsv.Z > range.Value.Max + SkinColorationUtils.Epsilon) + { + reason = $"Value {hsv.Z} is outside of range of min {range.Value.Min} max {range.Value.Max}"; + return false; + } + + return true; + } + + /// + public Color ClosestSkinColor(Color color) + { + var hsv = Color.ToHsv(color); + + // Clamp within specified nodes. + hsv.X = SkinColorationUtils.ClampHue(hsv.X, Nodes.First().Hue, Nodes.Last().Hue); + + var range = GetNodeValuesForHue(hsv.X); + if (range == null) + return color; + + hsv.Y = Math.Clamp(hsv.Y, range.Saturation.Min, range.Saturation.Max); + hsv.Z = Math.Clamp(hsv.Z, range.Value.Min, range.Value.Max); + + return Color.FromHsv(hsv); + } + + /// + /// Finds the nodes affecting value and saturation at a specified hue value. + /// + /// The hue value at which to find the nodes. Between 0 and 1. + /// The nodes taking effect at the specified hue. + private (HueNodeClampedHsvColorationNode Prev, HueNodeClampedHsvColorationNode Next)? GetAffectingNodes(float hue) + { + if (Nodes.Count == 0) + return null; + + // If only one node is provided we just consider it to control all values. + if (Nodes.Count == 1) + return (Nodes.First(), Nodes.Last()); + + for (int i = 0; i < Nodes.Count; i++) + { + // We get the currently iterated element. + var current = Nodes[i]; + + // If there is no element after this one, we just loop back to the first element. + // Basically a node list of [0, 0.5] will fall back to node at 0 if the hue is ever higher than 0.5 + var next = Nodes.ElementAtOrDefault(i + 1) ?? Nodes.First(); + + // Is the hue within the range of the nodes we're considering? + if (!SkinColorationUtils.IsHueInRange(hue, current.Hue, next.Hue)) + continue; + + return (current, next); + } + + return null; + } + + /// + /// Gets the values at which to clamp the value and saturation based on the given hue. + /// + /// The hue for which to get the clamping values. Between 0 and 1. + /// Node containing the value and saturation clamping. + private HueNodeClampedHsvColorationNode? GetNodeValuesForHue(float hue) + { + if (Nodes.Count == 0) + return null; + + // No node is actually affecting this coloring, so it's invalid. + if (GetAffectingNodes(hue) is not { } affectingNodes) + return null; + + var firstNode = affectingNodes.Prev; + var secondNode = affectingNodes.Next; + + // If both values are equal we just return 0f. + // This is to prevent dividing by 0. + var weight = MathHelper.CloseTo(firstNode.Hue, secondNode.Hue) ? 0f : (hue - firstNode.Hue) / (secondNode.Hue - firstNode.Hue); + + // I know this is also used to define the nodes, however it contains all the data necessary + // And I don't think creating a new DataDefinition is worth it just to get rid of the hue from this one. + var finalNode = new HueNodeClampedHsvColorationNode(); + finalNode.Hue = hue; + + finalNode.Saturation.Min = MathHelper.Lerp(firstNode.Saturation.Min, secondNode.Saturation.Min, weight); + finalNode.Saturation.Max = MathHelper.Lerp(firstNode.Saturation.Max, secondNode.Saturation.Max, weight); + + finalNode.Value.Min = MathHelper.Lerp(firstNode.Value.Min, secondNode.Value.Min, weight); + finalNode.Value.Max = MathHelper.Lerp(firstNode.Value.Max, secondNode.Value.Max, weight); + + return finalNode; + } +} + +/// +/// A node to be used with . +/// Represents a single point on the hue spectrum with corresponding clamping limits for saturation and value. +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class HueNodeClampedHsvColorationNode +{ + /// + /// The point on the hue spectrum where this node is placed. + /// Between 0 and 1. + /// + [DataField] + public float Hue; + + /// + /// Defines the (min, max) saturation on the provided node. + /// + [DataField] + public (float Min, float Max) Saturation; + + /// + /// Defines the (min, max) value on the provided node. + /// + [DataField] + public (float Min, float Max) Value; +} + /// /// Contains shared utility methods for handling color manipulations in skin coloration strategies. /// diff --git a/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs b/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs index 82ef82c5d4..2a3a66a45a 100644 --- a/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs +++ b/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs @@ -43,7 +43,7 @@ public sealed class SkinTonesTest { var unaryInput = i / 100f; // Test values like 0.0, 0.01, ..., 100.0 var color = strategy.FromUnary(unaryInput); - Assert.That(strategy.VerifySkinColor(color), $"Color {color} from unary value {unaryInput} failed verification."); + Assert.That(strategy.VerifySkinColor(color, out var reason), $"Color {color} from unary value {unaryInput} failed verification. Reason: {reason}"); } } @@ -75,7 +75,7 @@ public sealed class SkinTonesTest public void TestDefaultHumanSkinToneValid() { var strategy = new HumanTonedSkinColoration(); - Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone)); + Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone, out _)); } /// @@ -98,8 +98,8 @@ public sealed class SkinTonesTest var skinColor = strategy.ClosestSkinColor(color); LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift - Assert.That(strategy.VerifySkinColor(skinColor), - $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}"); + Assert.That(strategy.VerifySkinColor(skinColor, out var reason), + $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}"); } } @@ -122,8 +122,8 @@ public sealed class SkinTonesTest var skinColor = strategy.ClosestSkinColor(color); LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift - Assert.That(strategy.VerifySkinColor(skinColor), - $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}"); + Assert.That(strategy.VerifySkinColor(skinColor, out var reason), + $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}"); } } @@ -147,8 +147,8 @@ public sealed class SkinTonesTest var skinColor = strategy.ClosestSkinColor(color); LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift - Assert.That(strategy.VerifySkinColor(skinColor), - $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}"); + Assert.That(strategy.VerifySkinColor(skinColor, out var reason), + $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}"); } } @@ -172,8 +172,8 @@ public sealed class SkinTonesTest var skinColor = strategy.ClosestSkinColor(color); LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift - Assert.That(strategy.VerifySkinColor(skinColor), - $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}"); + Assert.That(strategy.VerifySkinColor(skinColor, out var reason), + $"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}"); } } @@ -197,8 +197,8 @@ public sealed class SkinTonesTest var skinColor = strategy.ClosestSkinColor(color); LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift - Assert.That(strategy.VerifySkinColor(skinColor), - $"Color {skinColor} (from input {color}) with circular hue failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}"); + Assert.That(strategy.VerifySkinColor(skinColor, out var reason), + $"Color {skinColor} (from input {color}) with circular hue failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}"); } } @@ -217,7 +217,7 @@ public sealed class SkinTonesTest var validColor = Color.FromHsl(new Vector4(0.5f, 0.5f, 0.5f, 1.0f)); var result = strategy.ClosestSkinColor(validColor); - Assert.That(strategy.VerifySkinColor(result), Is.True); + Assert.That(strategy.VerifySkinColor(result, out _), Is.True); } /// @@ -236,7 +236,7 @@ public sealed class SkinTonesTest var invalidColor = Color.FromHsl(new Vector4(0.5f, 0.9f, 0.2f, 1.0f)); var result = strategy.ClosestSkinColor(invalidColor); - Assert.That(strategy.VerifySkinColor(result), Is.True); + Assert.That(strategy.VerifySkinColor(result, out _), Is.True); Assert.That(result, Is.Not.EqualTo(invalidColor)); } diff --git a/Resources/Prototypes/Species/skin_colorations.yml b/Resources/Prototypes/Species/skin_colorations.yml index f5bc658c2b..a9e033c448 100644 --- a/Resources/Prototypes/Species/skin_colorations.yml +++ b/Resources/Prototypes/Species/skin_colorations.yml @@ -11,10 +11,23 @@ - type: skinColoration id: VoxFeathers - strategy: !type:ClampedHsvColoration - hue: [0.081, 0.48] - saturation: [0.2, 0.8] - value: [0.36, 0.55] + strategy: !type:HueNodeClampedHsvColoration + nodes: + - hue: 0 + saturation: [ 0.2, 0.5 ] + value: [ 0.36, 0.55 ] + - hue: 0.045 + saturation: [ 0.2, 0.8 ] + value: [ 0.36, 0.55 ] + - hue: 0.44 + saturation: [ 0.2, 0.8 ] + value: [ 0.36, 0.55 ] + - hue: 0.55 + saturation: [ 0.2, 0.5 ] + value: [ 0.36, 0.55 ] + - hue: 1 + saturation: [ 0.2, 0.5 ] + value: [ 0.36, 0.5 ] - type: skinColoration id: HumanToned @@ -22,6 +35,23 @@ - type: skinColoration id: VulpkaninColors - strategy: !type:ClampedHslColoration - saturation: [0.0, 0.60] - lightness: [0.2, 0.9] + strategy: !type:HueNodeClampedHsvColoration + nodes: + - hue: 0 # From red to orange slowly increase saturation. (0.75 -> 0.9) + saturation: [ 0, 0.75 ] + value: [ 0.2, 0.9 ] + - hue: 0.05 # From orange to yellow, keep it the same. (0.9 -> 0.8) + saturation: [ 0, 0.9 ] + value: [ 0.2, 0.9 ] + - hue: 0.1666 # From yellow to green, slowly decrease it. (0.8 -> 0.65) + saturation: [ 0, 0.8 ] + value: [ 0.2, 0.9 ] + - hue: 0.33 # From green to pink keep it the same. (0.65 -> 0.65) + saturation: [ 0, 0.65 ] + value: [ 0.2, 0.9 ] + - hue: 0.94 # From pink, increase saturation for red. (0.65 -> 0.75) + saturation: [ 0, 0.65 ] + value: [ 0.2, 0.9 ] + - hue: 1 # Red + saturation: [ 0, 0.75 ] + value: [ 0.2, 0.9 ]