UI/IdCard.razor

Razor panel that renders the fictional New Horizon state ID card for a roleplay character. Front side shows a live 3D head render via ScenePanel/Preview3D plus name, birth date, sex, nationality, citizen number and issue/expiry dates; the back shows a generated barcode and legal text. Override properties let the character creation form feed in unsaved name, birth, sex and appearance.

Rough Code
@using Sandbox;
@using Sandbox.UI;
@namespace HorizonRP
@inherits Panel

@*
	La carte d'identité de l'État (fictif) de New Horizon. Recto : photo (le
	personnage en 3D, de face), nom, naissance, sexe, nationalité, numéro de
	citoyen, dates. Verso : code-barres et mention légale. Sert au portefeuille, à
	la carte qu'un autre joueur vous montre, et à la création du personnage, où
	nom, naissance, sexe et visage viennent du formulaire, en direct.
*@

<root class="@(Flipped ? "flipped" : "")">
	@if ( Player is null )
	{
	}
	else if ( !Flipped )
	{
		<div class="head">
			<div class="state">@Documents.State</div>
			<div class="kind">CARTE D'IDENTITÉ</div>
		</div>
		<div class="body">
			<div class="photo"><ScenePanel @ref="Photo" class="photo-3d"></ScenePanel></div>
			<div class="fields">
				<div class="id-label">NOM</div>
				<div class="name">@ShownName</div>
				<div class="row">
					<div class="cell"><div class="id-label">DATE DE NAISSANCE</div><div class="value">@Format.Date( ShownBirth )</div></div>
					<div class="cell"><div class="id-label">SEXE</div><div class="value">@(ShownSex ?? "—")</div></div>
				</div>
				<div class="row">
					<div class="cell"><div class="id-label">NATIONALITÉ</div><div class="value">Américaine</div></div>
					<div class="cell"><div class="id-label">N° DE CITOYEN</div><div class="value mono">@Documents.CitizenId( Player.SteamId )</div></div>
				</div>
			</div>
		</div>
		<div class="foot">
			<div class="cell"><div class="id-label">DÉLIVRÉE LE</div><div class="value">@Format.Date( ShownIssued )</div></div>
			<div class="cell"><div class="id-label">EXPIRE LE</div><div class="value">@Documents.PlusYears( ShownIssued, Documents.IdValidYears )</div></div>
			<div class="seal"><i>verified</i></div>
		</div>
	}
	else
	{
		<div class="back">
			<div class="barcode">
				@for ( var i = 0; i < 46; i++ )
				{
					<div class="bar" style="width: @(Bar( i ))px"></div>
				}
			</div>
			<div class="back-id">@($"{(Documents.CitizenId( Player.SteamId ))} · {(Player.PhoneNumber)}")</div>
			<div class="back-text">Cette carte est la propriété de l'État de New Horizon. Si vous la trouvez, rapportez-la à un commissariat d'Union City.</div>
		</div>
	}
</root>

@code
{
	public RoleplayPlayer Player { get; set; }
	public bool Flipped { get; set; }

	// Création du personnage : ce que le formulaire dit, avant tout enregistrement.
	public string NameOverride { get; set; }
	public string BirthOverride { get; set; }
	public string SexOverride { get; set; }
	public string AppearanceOverride { get; set; }

	string ShownName => NameOverride ?? Player?.RpName;
	string ShownBirth => BirthOverride ?? Player?.BirthDate;
	string ShownSex => SexOverride ?? Player?.Sex;
	string ShownAppearance => AppearanceOverride ?? Player?.AppearanceJson;

	/// <summary>Une carte faite à la création est délivrée aujourd'hui.</summary>
	string ShownIssued => NameOverride is not null ? DateTime.UtcNow.ToString( "yyyy-MM-dd" ) : Player?.IdIssued;

	ScenePanel Photo { get; set; }
	readonly Preview3D photo = new() { TurnSpeed = 0, HeadDistance = 20f, HeadHeight = 67f, FaceOnly = true };

	/// <summary>Largeur des barres du code-barres, tirée du numéro de citoyen : la même pour une même personne.</summary>
	int Bar( int i ) => 1 + (Documents.CitizenId( Player?.SteamId ?? 0 )[i % 9] + i * 7) % 3;

	public override void Tick()
	{
		base.Tick();

		if ( Player is null || Flipped || Photo is null )
			return;

		photo.Attach( Photo );
		photo.ShowCharacter( ShownAppearance, ShownSex, headOnly: true );
		photo.Tick();
	}

	protected override int BuildHash() => HashCode.Combine( ShownName, ShownBirth, ShownSex, ShownIssued, Flipped );
}