Data model for character makeup and makeup layers. Defines MakeupLayerData with color components, amount, validity and deep copy, and CharacterMakeupData with preset layers, eyeliner/wing parameters, markings, validity checks, presentation key serialization, and a migration method for tattoo->markings.
using System;
using System.Text.Json.Serialization;
namespace CharacterEditor;
public sealed class MakeupLayerData
{
public float Red { get; set; } = 0.5f;
public float Green { get; set; } = 0.12f;
public float Blue { get; set; } = 0.18f;
public float Amount { get; set; }
[JsonIgnore]
public Color Color
{
get => new(Red, Green, Blue);
set
{
Red = value.r;
Green = value.g;
Blue = value.b;
}
}
public MakeupLayerData DeepCopy() => new()
{
Red = Red,
Green = Green,
Blue = Blue,
Amount = Amount
};
[JsonIgnore]
public bool Valid => new[]
{
Red,
Green,
Blue,
Amount
}.All(x => float.IsFinite(x) && x >= 0 && x <= 1);
}
public sealed class CharacterMakeupData
{
public const int CosmeticRevision = 18;
public MakeupLayerData Lipstick { get; set; } = new();
public MakeupLayerData Eyeshadow { get; set; } = new()
{
Red = 0.2f,
Green = 0.12f,
Blue = 0.28f
};
public MakeupLayerData Eyeliner { get; set; } = new()
{
Red = .025f,
Green = .02f,
Blue = .025f
};
public MakeupLayerData LowerEyeshadow { get; set; } = new()
{
Red = .2f,
Green = .12f,
Blue = .28f
};
// Eyeliner.Amount remains the upper strength for compatibility with existing JSON.
public float EyelinerLowerStrength { get; set; }
public float EyelinerInnerStrength { get; set; }
public string EyelinerWing { get; set; } = "none";
public float EyelinerWingStrength { get; set; } = 1;
public float EyelinerWingHorizontal { get; set; } = .25f;
public float EyelinerWingVertical { get; set; }
public float EyelinerWingThickness { get; set; } = .25f;
// Retain legacy wing names when importing old presets; the UI converts them
// to an adjustable wing as soon as its tip is moved.
[JsonIgnore]
public float WingTipHorizontal => EyelinerWing switch { "flick" => .1f, "cat-eye" => .25f, "downward" => .22f, _ => EyelinerWingHorizontal };
[JsonIgnore]
public float WingTipVertical => EyelinerWing switch { "flick" => .18f, "cat-eye" => .38f, "downward" => -.38f, _ => EyelinerWingVertical };
[JsonIgnore]
public bool EyelinerActive => Eyeliner.Amount > 0 || EyelinerLowerStrength > 0 || EyelinerInnerStrength > 0;
/// <summary>Explicit new-field key also invalidates presentations after live code/asset updates.</summary>
public string PresentationKey() => System.Text.Json.JsonSerializer.Serialize(this) +
$"|eyeliner-v{CosmeticRevision}|{EyelinerLowerStrength:R}|{EyelinerInnerStrength:R}|{EyelinerWing}|{EyelinerWingStrength:R}|{EyelinerWingHorizontal:R}|{EyelinerWingVertical:R}|{EyelinerWingThickness:R}";
public MakeupLayerData Blush { get; set; } = new()
{
Red = 0.7f,
Green = 0.3f,
Blue = 0.32f
};
public MakeupLayerData Tattoo { get; set; } = new()
{
Red = 0.1f,
Green = 0.12f,
Blue = 0.16f
};
public string TattooStyle { get; set; } = "none";
public List<CharacterMarkingData> Markings { get; set; } = new();
[JsonIgnore]
public bool Active => Lipstick.Amount > 0 || Eyeshadow.Amount > 0 || EyelinerActive || LowerEyeshadow.Amount > 0 || Blush.Amount > 0 || (TattooStyle != "none" && Tattoo.Amount > 0) || Markings.Any(x => x.Style != "none" && x.Ink.Amount > 0);
public CharacterMakeupData DeepCopy() => new()
{
Lipstick = Lipstick?.DeepCopy(),
Eyeshadow = Eyeshadow?.DeepCopy(),
Eyeliner = Eyeliner?.DeepCopy(),
EyelinerLowerStrength = EyelinerLowerStrength,
EyelinerInnerStrength = EyelinerInnerStrength,
EyelinerWing = EyelinerWing,
EyelinerWingStrength = EyelinerWingStrength,
EyelinerWingHorizontal = EyelinerWingHorizontal,
EyelinerWingVertical = EyelinerWingVertical,
EyelinerWingThickness = EyelinerWingThickness,
LowerEyeshadow = LowerEyeshadow?.DeepCopy(),
Blush = Blush?.DeepCopy(),
Tattoo = Tattoo?.DeepCopy(),
TattooStyle = TattooStyle,
Markings = Markings?.Select(x => x?.DeepCopy()).ToList()
};
[JsonIgnore]
public bool Valid =>
Lipstick?.Valid == true && Eyeshadow?.Valid == true && Eyeliner?.Valid == true &&
LowerEyeshadow?.Valid == true && Blush?.Valid == true && Tattoo?.Valid == true &&
(TattooStyle is "none" or "crescent") &&
(EyelinerWing is "none" or "wing" or "flick" or "cat-eye" or "downward") &&
float.IsFinite(EyelinerWingVertical) && EyelinerWingVertical >= -1 && EyelinerWingVertical <= 1 &&
new[] { EyelinerLowerStrength, EyelinerInnerStrength, EyelinerWingStrength, EyelinerWingHorizontal, EyelinerWingThickness }
.All(x => float.IsFinite(x) && x >= 0 && x <= 1) &&
Markings is not null && Markings.Count <= MarkingCatalog.Maximum &&
Markings.All(x => x?.Valid == true) &&
!(TattooStyle != "none" && Markings.Count == MarkingCatalog.Maximum);
// Upgrade only when opening the markings editor. Disk reads/copies remain exact and cold-safe.
public void EditMarkings()
{
if (TattooStyle == "crescent")
{
Markings.Insert(0, new() { Region = "left-cheek", Ink = Tattoo.DeepCopy() });
TattooStyle = "none";
Tattoo.Amount = 0;
}
}
}