Sim/FaceStyle.cs
using System;

namespace SquishyTerry.Sim;

/// <summary>
/// Everything that decides what the face looks like. <see cref="Original"/> reproduces the
/// recovered game exactly (it is what the wasm comparison test runs); <see cref="Terry"/> reshapes
/// it into the s&amp;box citizen. All feature positions are directions on the unit sphere, picked
/// before <see cref="HeadShape"/> is applied.
/// </summary>
public sealed class FaceStyle
{
	/// <summary>Deforms the initial unit sphere. Null keeps the sphere.</summary>
	public Func<Vec3, Vec3> HeadShape;

	public Vec3 LeftEye = new( -0.234667f, 0.159318f, 0.958931f );
	public Vec3 RightEye = new( 0.234666f, 0.159318f, 0.958931f );
	public Vec3 Nose = new( -0.0f, -0.099833f, 0.995004f );
	public Vec3 LeftEar = new( -0.867728f, -0.149438f, 0.474042f );
	public Vec3 RightEar = new( 0.867728f, -0.149438f, 0.474042f );
	public Vec3 Mouth = new( -0.0f, -0.342898f, 0.939373f );

	public bool Hair = true;

	/// <summary>
	/// Flatten feature patches onto a plane fitted to their whole boundary loop. The original uses
	/// three neighbouring boundary points, which tilts mirrored features differently.
	/// </summary>
	public bool FitPatchPlanes;

	/// <summary>How far feature patches are pushed onto their plane (1 = fully flat, as in the original).</summary>
	public float PatchFlatten = 1.0f;

	/// <summary>How many vertex rings around the mouth vertex make up the mouth.</summary>
	public int MouthRings = 2;
	/// <summary>Mouth rings are squashed onto an ellipse of this size.</summary>
	public Vec3 MouthScale = new( 0.16f, 0.08f, 0.16f );
	/// <summary>Skin shading: mouthiness above this is drawn as the inside of the mouth (0.15 in the original).</summary>
	public float MouthShadeThreshold = 0.15f;
	/// <summary>Pulls the mouth corners down (y -= curve * dx^2).</summary>
	public float MouthCurve;

	public int NoseRings = 1;
	public Vec3 NosePatch = new( 0.065f, 0.065f, 0.065f );
	public Vec3 NoseSize = new( 0.05f, 0.05f, 0.075f );
	public float NoseCut = 0.001f;
	/// <summary>Optional sculpt of the nose's unit sphere (+Z out of the face, +Y up) before it is clipped.</summary>
	public Func<Vec3, Vec3> NoseShape;
	/// <summary>Nose mesh subdivisions (3 in the original).</summary>
	public int NoseDetail = 3;
	/// <summary>Optional nose shading mask on the sculpted unit sphere: 1 draws in the mouth colour (nostrils).</summary>
	public Func<Vec3, float> NoseMask;

	public int EyeRings = 3;
	public Vec3 EyePatch = new( 0.17f, 0.17f, 0.17f );
	public Vec3 EyeSize = new( 0.17f, 0.17f, 0.17f );
	public float EyeCut = 0.2f;

	public int EarRings = 2;
	public Vec3 EarPatch = new( 0.12f, 0.12f, 0.07f );
	public Vec3 EarSize = new( 0.05f, 0.12f, 0.08f );
	public float EarCut = 0.001f;
	public float EarShear = 2.0f;
	public float EarRotation = 0.3f;
	/// <summary>Optional sculpt of the right ear's unit sphere (x = back, y = up, z = out from the head); mirrored for the left.</summary>
	public Func<Vec3, Vec3> EarShape;
	/// <summary>Icosphere subdivisions for the ears (3 in the original).</summary>
	public int EarDetail = 3;

	/// <summary>Skin shells over the top of each eyeball.</summary>
	public bool Eyelids;
	/// <summary>Lid edge height in eye space (+Y up, +Z out of the face).</summary>
	public float LidHeight;
	/// <summary>Tilts the lid edge; positive droops it towards the front of the eye.</summary>
	public float LidTilt;
	public float LidThickness = 0.02f;

	public static FaceStyle Original => new();

	/// <summary>0..1 nostril depth on the nose's unit sphere: two soft holes on the lower front.</summary>
	static float NostrilDepth( float x, float y, float z )
	{
		if ( z < 0.1f ) return 0f;
		float depth = 0f;
		for ( int side = -1; side <= 1; side += 2 )
		{
			// Equal radii (x slightly less to cancel the bulb's flare) so the holes read as circles from the front.
			float dx = (x - side * 0.3f) / 0.19f, dy = (y + 0.36f) / 0.205f;
			float d = MathF.Sqrt( dx * dx + dy * dy );
			if ( d < 1f ) depth = MathF.Max( depth, (1f - d) * (1f - d) * (3f - 2f * (1f - d)) );
		}
		return depth;
	}

	public static FaceStyle Terry => new()
	{
		HeadShape = p =>
		{
			// Bullet head: round dome on top, long jaw below, and a rounded-square cross-section so
			// the face is flat enough for the eyes to look forward.
			float y = p.Y >= 0 ? p.Y * 0.95f : p.Y * 1.9f;
			float widen = p.Y >= 0 ? 1.0f : 1.0f + 0.35f * p.Y * p.Y;
			float r = MathF.Sqrt( p.X * p.X + p.Z * p.Z );
			float square = 1.0f;
			if ( r > 1e-6f )
			{
				float c = MathF.Abs( p.X / r ), s = MathF.Abs( p.Z / r );
				square = MathF.Pow( c * c * c + s * s * s, -1.0f / 3.0f ) * 0.92f;
			}
			float k = widen * square;
			return new Vec3( p.X * k, y + 0.45f, p.Z * 0.95f * k );
		},

		LeftEye = new Vec3( -0.52f, -0.2f, 0.843f ).Normalized,
		RightEye = new Vec3( 0.52f, -0.2f, 0.843f ).Normalized,
		Nose = new Vec3( 0.0f, -0.34f, 0.94f ).Normalized,
		Mouth = new Vec3( 0.0f, -0.6f, 0.8f ).Normalized,
		LeftEar = new Vec3( -0.9f, -0.25f, 0.357f ).Normalized,
		RightEar = new Vec3( 0.9f, -0.25f, 0.357f ).Normalized,

		Hair = false,
		FitPatchPlanes = true,
		PatchFlatten = 1.0f,

		MouthRings = 3,
		MouthScale = new Vec3( 0.25f, 0.03f, 0.1f ),
		MouthCurve = 0.6f,
		MouthShadeThreshold = 0.35f,

		NoseRings = 2,
		NosePatch = new Vec3( 0.11f, 0.11f, 0.11f ),
		// Citizen nose: a small, low, rounded bulb, slightly wider at the bottom, with a snub tip so the
		// two nostrils show from the front.
		NoseSize = new Vec3( 0.13f, 0.112f, 0.08f ),
		NoseCut = 0.0f,
		NoseDetail = 4,
		NoseShape = p =>
		{
			float x = p.X, y = p.Y, z = p.Z;

			// Flatten the underside into a face pointing forward-down, where the nostrils sit; the upper
			// part stays a full rounded bulb.
			const float ny = -0.857f, nz = 0.514f, plane = 0.35f;
			float along = y * ny + z * nz;
			if ( along > plane )
			{
				float pull = (along - plane) * 0.75f;
				y -= pull * ny;
				z -= pull * nz;
			}

			// Nostrils: push two holes into that flat underside.
			float hole = NostrilDepth( x, y, z );
			float inward = 1f - 0.45f * hole;
			x *= inward; y *= inward; z *= inward;

			// A rounded bulb, only slightly wider at the bottom than the top.
			float flare = y < 0 ? 1f + 0.15f * -y : 1f - 0.1f * y;
			x *= flare;

			return new Vec3( x, y, z );
		},
		NoseMask = p =>
		{
			// The sculpted holes are shallow at this size, so darken them like the original does the mouth.
			float x = p.X / (p.Y < 0 ? 1f + 0.15f * -p.Y : 1f - 0.1f * p.Y);
			// A smooth value (not 0/1) so the shader's threshold cuts a round contour across the triangles.
			return Math.Clamp( (NostrilDepth( x, p.Y, p.Z ) - 0.05f) / 0.2f, 0f, 1f );
		},

		EyeRings = 3,
		EyePatch = new Vec3( 0.185f, 0.185f, 0.185f ),
		EyeSize = new Vec3( 0.185f, 0.185f, 0.185f ),
		EyeCut = 0.1f,

		EarSize = new Vec3( 0.075f, 0.19f, 0.12f ),
		EarPatch = new Vec3( 0.14f, 0.14f, 0.08f ),
		EarShear = 1.3f,
		EarDetail = 4,
		EarShape = p =>
		{
			// Local frame: x = back, y = up, z = out from the head. The shear leans the ear back, so
			// its front (x < 0) is the side you see.
			float x = p.X, y = p.Y, z = p.Z;

			// A "C" outline: full at the top, narrowing down into the lobe.
			x *= 1f + 0.2f * y;

			// Hollow bowl in the front face, leaving a rolled rim around the top, back and lobe.
			if ( x < 0f )
			{
				float dy = y / 0.72f, dz = (z - 0.5f) / 0.42f;
				float r = MathF.Sqrt( dy * dy + dz * dz );
				if ( r < 1f )
				{
					float s = 1f - r;
					x *= 1f - 1.5f * s * s * (3f - 2f * s);
				}
			}

			return new Vec3( x, y, z );
		},

		Eyelids = true,
		LidHeight = 0.01f,
		LidTilt = 0.15f,
		LidThickness = 0.02f,
	};
}