Data classes for character horn appearance. CharacterHornsData holds left/right CharacterHornData with linking, validation, deep copy and equality checks; CharacterHornData stores numeric parameters, color properties, validation helpers, deep copy, equality and a label helper.
using System;
using System.Text.Json.Serialization;
namespace CharacterEditor;
/// <summary>Portable rigid accessories. Old appearances deserialize with both horns absent.</summary>
public sealed class CharacterHornsData
{
public bool Linked { get; set; } = true;
public CharacterHornData Left { get; set; } = new();
public CharacterHornData Right { get; set; } = new();
[JsonIgnore]
public bool Active => Left?.Family != "none" || (!Linked && Right?.Family != "none");
[JsonIgnore]
public bool Valid => Left?.Valid == true && Right?.Valid == true;
public CharacterHornsData DeepCopy() => new()
{
Linked = Linked,
Left = Left?.DeepCopy(),
Right = Right?.DeepCopy()
};
public static bool Same(CharacterHornsData a, CharacterHornsData b) => ReferenceEquals(a, b) || (a is not null && b is not null && a.Linked == b.Linked && CharacterHornData.Same(a.Left, b.Left) && CharacterHornData.Same(a.Right, b.Right));
}
public sealed class CharacterHornData
{
public static readonly string[] Families =
{
"spike",
"swept",
"recurved",
"hooked",
"ram",
"spiral"
};
public string Family { get; set; } = "none";
public float Length { get; set; } = .5f;
public float Thickness { get; set; } = .5f;
public float Curve { get; set; } = .5f;
public float Twist { get; set; } = .5f;
public float Tilt { get; set; } = .5f;
public float Splay { get; set; } = .5f;
public float Placement { get; set; } = .25f;
public float Spacing { get; set; } = .5f;
public float Ridges { get; set; } = .5f;
public float Roughness { get; set; } = .55f;
// Match the original smoothstep(.42,.99) when older appearances omit these.
public float TipCoverage { get; set; } = .295f;
public float BlendSoftness { get; set; } = .57f;
public float Red { get; set; } = .29f;
public float Green { get; set; } = .23f;
public float Blue { get; set; } = .17f;
public float TipRed { get; set; } = .095f;
public float TipGreen { get; set; } = .065f;
public float TipBlue { get; set; } = .045f;
[JsonIgnore]
public Color Color
{
get => new(Red, Green, Blue);
set
{
Red = value.r;
Green = value.g;
Blue = value.b;
}
}
[JsonIgnore]
public Color TipColor
{
get => new(TipRed, TipGreen, TipBlue);
set
{
TipRed = value.r;
TipGreen = value.g;
TipBlue = value.b;
}
}
[JsonIgnore]
public bool Valid => (Family == "none" || Families.Contains(Family)) && new[]
{
Length,
Thickness,
Curve,
Twist,
Tilt,
Splay,
Placement,
Spacing,
Ridges,
Roughness,
TipCoverage,
BlendSoftness,
Red,
Green,
Blue,
TipRed,
TipGreen,
TipBlue
}.All(Unit);
static bool Unit(float x) => float.IsFinite(x) && x >= 0 && x <= 1;
public CharacterHornData DeepCopy() => new()
{
Family = Family,
Length = Length,
Thickness = Thickness,
Curve = Curve,
Twist = Twist,
Tilt = Tilt,
Splay = Splay,
Placement = Placement,
Spacing = Spacing,
Ridges = Ridges,
Roughness = Roughness,
TipCoverage = TipCoverage,
BlendSoftness = BlendSoftness,
Red = Red,
Green = Green,
Blue = Blue,
TipRed = TipRed,
TipGreen = TipGreen,
TipBlue = TipBlue
};
public static bool Same(CharacterHornData a, CharacterHornData b) => ReferenceEquals(a, b) || (a is not null && b is not null && a.Family == b.Family && a.Length == b.Length && a.Thickness == b.Thickness && a.Curve == b.Curve && a.Twist == b.Twist && a.Tilt == b.Tilt && a.Splay == b.Splay && a.Placement == b.Placement && a.Spacing == b.Spacing && a.Ridges == b.Ridges && a.Roughness == b.Roughness && a.TipCoverage == b.TipCoverage && a.BlendSoftness == b.BlendSoftness && a.Color == b.Color && a.TipColor == b.TipColor);
public static string Label(string family) => family == "none" ? "None" : char.ToUpperInvariant(family[0]) + family[1..];
}