Static helper that builds layered CSS-shape portraits for player cards. Picks skin, hair, eye and face parameters from a seeded Random or from explicit portrait fields on the card, and caches the generated CSS fragments by a composite key.
using System;
using System.Collections.Generic;
namespace Socer;
// Original layered vector portraits. Cosmetic seed never touches simulation RNG.
public static class PlayerPortrait
{
static readonly Dictionary<(int Seed,int Id,int Skin,int Hair,int Style,int Beard),List<string>> Cache=new();
public static IReadOnlyList<string> Parts(int clubSeed,PlayerCard card)=>Parts(clubSeed,unchecked((int)PlayerCatalog.Hash(card.CatalogId)),card);
public static IReadOnlyList<string> Parts(int clubSeed,int id)
=>Parts(clubSeed,id,null);
static IReadOnlyList<string> Parts(int clubSeed,int id,PlayerCard card)
{
var key=(clubSeed,id,card?.PortraitSkin??-1,card?.PortraitHair??-1,card?.PortraitStyle??-1,card?.PortraitBeard??-1);
if(Cache.TryGetValue(key,out var cached)) return cached;
var rng=new Random(unchecked(clubSeed*31+id*7919));
string[] skins={"#f0c5a2","#dca780","#bd845f","#985e42","#754b39","#513a30"};
string[] hairs={"#211c1c","#35271f","#65452c","#b38b52","#8c482c"};
string skin=skins[card?.PortraitSkin>=0?Math.Clamp(card.PortraitSkin,0,skins.Length-1):rng.Next(skins.Length)];
string hair=hairs[card?.PortraitHair>=0?Math.Clamp(card.PortraitHair,0,hairs.Length-1):rng.Next(hairs.Length)];
int style=card?.PortraitStyle>=0?card.PortraitStyle:rng.Next(5),beard=card?.PortraitBeard>=0?card.PortraitBeard:rng.Next(4);
float face=32+rng.Next(11),x=50-face/2;
float eyeY=37+rng.Next(3),eyeSpacing=7+rng.Next(4),jaw=43+rng.Next(12);
var parts=new List<string>();
void Shape(float l,float t,float w,float h,string color,string radius="45%",string extra="")
{
parts.Add(FormattableString.Invariant($"left:{l}%;top:{t}%;width:{w}%;height:{h}%;background-color:{color};border-radius:{radius};{extra}"));
}
Shape(7,72,86,44,"#101e2a","48% 48% 0% 0%");
Shape(9,70,82,45,"#183e4f","48% 48% 0% 0%");
Shape(14,77,13,32,"#2c6377","35% 20% 0% 0%");
Shape(72,77,13,32,"#102c3e","20% 35% 0% 0%");
Shape(40,60,20,24,skin,"22%");Shape(41,61,18,8,"#00000020","30%");
Shape(36,77,28,7,"#bce4e4","16%","transform:rotate(-5deg);");
Shape(40,78,20,6,"#103d4a","10%");
Shape(24,37,10,16,skin);Shape(66,37,10,16,skin);
Shape(25,41,5,5,"#00000025");Shape(70,41,5,5,"#00000025");
Shape(x,17,face,52,skin,$"42% 42% {jaw}% {jaw}%");
Shape(x+2,23,4,34,"#ffffff25","50%");
Shape(x+face-6,22,5,38,"#00000018","50%");
Shape(x+5,45,9,9,"#ffffff18","50%");Shape(x+face-14,45,9,9,"#00000010","50%");
Shape(x+5,eyeY-5,10,2.4f,hair,"35%","transform:rotate(-5deg);");
Shape(x+face-15,eyeY-5,10,2.4f,hair,"35%","transform:rotate(5deg);");
Shape(50-eyeSpacing-5,eyeY,9,3.5f,"#e7d9c8");Shape(50+eyeSpacing-4,eyeY,9,3.5f,"#e7d9c8");
Shape(50-eyeSpacing-2,eyeY,3.2f,3.7f,"#293031");Shape(50+eyeSpacing-1,eyeY,3.2f,3.7f,"#293031");
Shape(50-eyeSpacing-1,eyeY+.3f,1,1,"#ffffffbd");Shape(50+eyeSpacing,eyeY+.3f,1,1,"#ffffffbd");
Shape(48,eyeY+1,4,12,"#00000017","15% 15% 45% 45%");
Shape(48,eyeY+11,5,2,"#00000025","50%");
Shape(47,eyeY+9,4,3,"#ffffff28","50%");
// Only the explicit beard style draws facial hair. Other styles leave the face clean.
if(beard==0)
{
Shape(x+2,52,face-4,18,hair,"12% 12% 45% 45%","opacity:.86;");
Shape(x+4,48,5,15,hair,"35%","opacity:.65;");Shape(x+face-9,48,5,15,hair,"35%","opacity:.65;");
Shape(44,53,12,2,hair,"35%","opacity:.8;");
}
Shape(44,56,12,2.7f,"#754a42","45%");Shape(46,59,8,1.3f,"#ffffff35");
if(style!=4)
{
Shape(x-2,13,face+4,18,hair,style==2?"25% 25% 15% 15%":"48% 48% 12% 12%");
Shape(x-2,24,5,20,hair,"30%");Shape(x+face-3,24,5,20,hair,"30%");
if(style==0) Shape(x,9,face,17,hair,"60% 60% 10% 10%","transform:rotate(-9deg);");
if(style==1) for(int i=0;i<7;i++)Shape(x-3+i*6,10+rng.Next(5),11,13,hair);
if(style==2){Shape(x+4,15,face-8,4,"#ffffff1f","50%");Shape(x+8,12,face-16,3,"#ffffff15","50%");}
if(style==3){Shape(x+2,12,face-5,10,"#ffffff15","35%");Shape(x+face-11,17,2,13,skin,"0%");}
if(style==5){Shape(72,14,10,61,hair,"45%","transform:rotate(-7deg);");Shape(x-2,12,face+4,12,hair,"48% 48% 12% 12%");}
for(int i=0;i<4;i++)Shape(x+5+i*(face-10)/4,17+rng.Next(5),3,4,"#ffffff16","45%");
}
Shape(64,87,7,9,"#ace3df","15% 15% 45% 45%");
if(Cache.Count>512)Cache.Clear();Cache[key]=parts;return parts;
}
}