CharacterKit/CharacterTeethData.cs

Data class for character teeth appearance. Stores upper and lower tooth style, linked-rows flag, color values, provides computed properties, validation, cloning, equality check, available styles, and methods to prepare/apply teeth morphs to a skinned model.

ReflectionFile Access
using System;
using System.Text.Json.Serialization;

namespace CharacterEditor;
/// <summary>Portable dental appearance. Omitted older data keeps the stock mouth.</summary>
public sealed class CharacterTeethData
{
	// Keep the original upper-row field for portable appearances made before
	// independent rows. Missing lower-row data always means stock lower teeth.
	public string Style { get; set; } = "natural";
	public string LowerStyle { get; set; } = "natural";
	public bool LinkedRows { get; set; }

	[JsonIgnore]
	public string EffectiveLowerStyle => LinkedRows ? Style : LowerStyle;
	public bool CustomColor { get; set; }
	public float ColorR { get; set; } = 1;
	public float ColorG { get; set; } = 1;
	public float ColorB { get; set; } = 1;

	[JsonIgnore]
	public Color Color
	{
		get => new(ColorR, ColorG, ColorB);
		set
		{
			ColorR = value.r;
			ColorG = value.g;
			ColorB = value.b;
		}
	}

	[JsonIgnore]
	public bool Valid => Styles.Any(x => x.Id == Style) && Styles.Any(x => x.Id == LowerStyle) && new[]
	{
		ColorR,
		ColorG,
		ColorB
	}.All(x => float.IsFinite(x) && x >= 0 && x <= 1);

	public CharacterTeethData DeepCopy() => new()
	{
		Style = Style,
		LowerStyle = LowerStyle,
		LinkedRows = LinkedRows,
		CustomColor = CustomColor,
		ColorR = ColorR,
		ColorG = ColorG,
		ColorB = ColorB
	};
	public static bool Same(CharacterTeethData a, CharacterTeethData b) => ReferenceEquals(a, b) || a is not null && b is not null && a.Style == b.Style && a.LowerStyle == b.LowerStyle && a.LinkedRows == b.LinkedRows && a.CustomColor == b.CustomColor && a.ColorR == b.ColorR && a.ColorG == b.ColorG && a.ColorB == b.ColorB;
	public static IReadOnlyList<(string Id, string Label)> Styles { get; } = new[]
	{
		("natural", "Natural"),
		("vampire", "Vampire"),
		("missing_front", "Missing front tooth"),
		("missing_multiple", "Missing teeth")
	};

	public static void Prepare(CharacterAppearanceData appearance)
	{
		if (appearance?.BaseShape is null && appearance?.Teeth is { } teeth && (teeth.Style != "natural" || teeth.EffectiveLowerStyle != "natural"))
			UnifiedHuman.Enable(appearance);
	}

	public static void ApplyShape(SkinnedModelRenderer body, CharacterAppearanceData appearance)
	{
		if (appearance.BaseShape is null)
			return;
		for (int i = 1; i < Styles.Count; i++)
			body.Morphs.Set("ce_teeth_" + Styles[i].Id, appearance.Teeth.Style == Styles[i].Id ? 1 : 0, 0);
		for (int i = 1; i < Styles.Count; i++)
			body.Morphs.Set("ce_teeth_lower_" + Styles[i].Id, appearance.Teeth.EffectiveLowerStyle == Styles[i].Id ? 1 : 0, 0);
	}
}