CharacterKit/CharacterHornData.cs

Data classes for character horn customization. CharacterHornsData groups left and right horns with linking, and CharacterHornData holds numeric parameters, colors, validation, deep copy and comparison helpers.

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..];
}