De-enumify humanoid species skin colours (#39175)
* De-enumify humanoid species skin colours * Change index to resolve --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
2820882754
commit
d9c24b3d10
@@ -1088,10 +1088,11 @@ namespace Content.Client.Lobby.UI
|
||||
if (Profile is null) return;
|
||||
|
||||
var skin = _prototypeManager.Index<SpeciesPrototype>(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<SpeciesPrototype>(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()
|
||||
|
||||
@@ -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<Marking> Markings { get; set; } = new();
|
||||
@@ -92,14 +93,13 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
|
||||
|
||||
public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
|
||||
{
|
||||
var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species);
|
||||
var skinColor = speciesPrototype.SkinColoration switch
|
||||
var protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||
var speciesPrototype = protoMan.Index<SpeciesPrototype>(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<IPrototypeManager>().Index<SpeciesPrototype>(species).SkinColoration;
|
||||
var protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||
var skinType = protoMan.Index<SpeciesPrototype>(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);
|
||||
|
||||
@@ -81,7 +81,7 @@ public sealed partial class SpeciesPrototype : IPrototype
|
||||
/// Method of skin coloration used by the species.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public HumanoidSkinColor SkinColoration { get; private set; }
|
||||
public ProtoId<SkinColorationPrototype> SkinColoration { get; private set; }
|
||||
|
||||
[DataField]
|
||||
public ProtoId<LocalizedDatasetPrototype> MaleFirstNames { get; private set; } = "NamesFirstMale";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
/// <summary>
|
||||
/// Turn a color into a valid tinted hue skin tone.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to validate</param>
|
||||
/// <returns>Validated tinted hue skin tone</returns>
|
||||
public static Color ValidTintedHuesSkinTone(Color color)
|
||||
{
|
||||
return TintedHues(color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a human skin tone based on a scale of 0 to 100. The value is clamped between 0 and 100.
|
||||
/// </summary>
|
||||
/// <param name="tone">Skin tone. Valid range is 0 to 100, inclusive. 0 is gold/yellowish, 100 is dark brown.</param>
|
||||
/// <returns>A human skin tone.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human skin tone from a given color.
|
||||
/// </summary>
|
||||
/// <param name="color"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify if a color is in the human skin tone range.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to verify</param>
|
||||
/// <returns>True if valid, false otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a color to the 'tinted hues' skin tone type.
|
||||
/// </summary>
|
||||
/// <param name="color">Color to convert</param>
|
||||
/// <returns>Tinted hue color</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify if this color is a valid tinted hue color type, or not.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to verify</param>
|
||||
/// <returns>True if valid, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="color">Color to convert</param>
|
||||
/// <returns>Vox feather coloration</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// Ensures the input Color is within the allowed vox color range.
|
||||
// /// </summary>
|
||||
// /// <param name="color">Color to convert</param>
|
||||
// /// <returns>The same Color if it was within the allowed range, or the closest matching Color otherwise</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify if this color is a valid vox feather coloration, or not.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to verify</param>
|
||||
/// <returns>True if valid, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This takes in a color, and returns a color guaranteed to be above MinHuesLightness
|
||||
/// </summary>
|
||||
/// <param name="color"></param>
|
||||
/// <returns>Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness</returns>
|
||||
public static Color MakeHueValid(Color color)
|
||||
{
|
||||
var manipulatedColor = Color.ToHsv(color);
|
||||
manipulatedColor.Z = Math.Max(manipulatedColor.Z, MinHuesLightness);
|
||||
return Color.FromHsv(manipulatedColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify if this color is above a minimum luminosity
|
||||
/// </summary>
|
||||
/// <param name="color"></param>
|
||||
/// <returns>True if valid, false if not</returns>
|
||||
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).
|
||||
}
|
||||
302
Content.Shared/Humanoid/SkinColorationPrototype.cs
Normal file
302
Content.Shared/Humanoid/SkinColorationPrototype.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Humanoid;
|
||||
|
||||
/// <summary>
|
||||
/// A prototype containing a SkinColorationStrategy
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class SkinColorationPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The skin coloration strategy specified by this prototype
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ISkinColorationStrategy Strategy = default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of input taken by a <see cref="SkinColorationStrategy" />
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum SkinColorationStrategyInput
|
||||
{
|
||||
/// <summary>
|
||||
/// A single floating point number from 0 to 100 (inclusive)
|
||||
/// </summary>
|
||||
Unary,
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Color" />
|
||||
/// </summary>
|
||||
Color,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes in the given <see cref="SkinColorationStrategyInput" /> and returns an adjusted Color
|
||||
/// </summary>
|
||||
public interface ISkinColorationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of input expected by the implementor; callers should consult InputType before calling the methods that require a given input
|
||||
/// </summary>
|
||||
SkinColorationStrategyInput InputType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the provided <see cref="Color" /> is within bounds of this strategy
|
||||
/// </summary>
|
||||
bool VerifySkinColor(Color color);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the closest skin color that this strategy would provide to the given <see cref="Color" />
|
||||
/// </summary>
|
||||
Color ClosestSkinColor(Color color);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the input if it passes <see cref="VerifySkinColor">, otherwise returns <see cref="ClosestSkinColor" />
|
||||
/// </summary>
|
||||
Color EnsureVerified(Color color)
|
||||
{
|
||||
if (VerifySkinColor(color))
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
return ClosestSkinColor(color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a colour representation of the given unary input
|
||||
/// </summary>
|
||||
Color FromUnary(float unary)
|
||||
{
|
||||
throw new InvalidOperationException("This coloration strategy does not support unary input");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a colour representation of the given unary input
|
||||
/// </summary>
|
||||
float ToUnary(Color color)
|
||||
{
|
||||
throw new InvalidOperationException("This coloration strategy does not support unary input");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unary coloration strategy that returns human skin tones, with 0 being lightest and 100 being darkest
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unary coloration strategy that clamps the color within the HSV colorspace
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ClampedHsvColoration : ISkinColorationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The (min, max) of the hue channel.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public (float, float)? Hue;
|
||||
|
||||
/// <summary>
|
||||
/// The (min, max) of the saturation channel.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public (float, float)? Saturation;
|
||||
|
||||
/// <summary>
|
||||
/// The (min, max) of the value channel.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unary coloration strategy that clamps the color within the HSL colorspace
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ClampedHslColoration : ISkinColorationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The (min, max) of the hue channel.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public (float, float)? Hue;
|
||||
|
||||
/// <summary>
|
||||
/// The (min, max) of the saturation channel.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public (float, float)? Saturation;
|
||||
|
||||
/// <summary>
|
||||
/// The (min, max) of the lightness channel.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
21
Resources/Prototypes/Species/skin_colorations.yml
Normal file
21
Resources/Prototypes/Species/skin_colorations.yml
Normal file
@@ -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 {}
|
||||
Reference in New Issue
Block a user