export type RolePalette = { coat: string; front: string; sleeves: string; legs: string; accent: string; hair: string; }; export type RoleMeta = { icon: string; label: string; color: string; }; export function roleMeta(role: string): RoleMeta { switch (role) { case "guard": return { icon: "🗡️", label: "Guard", color: "#e2675a" }; case "builder": return { icon: "🔨", label: "Builder", color: "#5b96f0" }; case "forager": case "gatherer": return { icon: "🌾", label: "Gatherer", color: "#6fc46a" }; default: return { icon: "👤", label: role || "Villager", color: "#d9b35a" }; } } export function rolePalette(role: string): RolePalette { switch (role) { case "builder": return { coat: "#3f78d3", front: "#76a9f8", sleeves: "#2f62b5", legs: "#56687f", accent: "#f0b34b", hair: "#4c3a2f", }; case "forager": case "gatherer": return { coat: "#5faa59", front: "#91d67b", sleeves: "#468e45", legs: "#4d6e4b", accent: "#e6d16a", hair: "#5a3c2a", }; case "guard": return { coat: "#b94f45", front: "#df7d68", sleeves: "#983d38", legs: "#4e5361", accent: "#d7d3c1", hair: "#312b29", }; case "scribe": return { coat: "#805ec8", front: "#aa8ce4", sleeves: "#684aae", legs: "#5d5574", accent: "#f0df8a", hair: "#463245", }; default: return { coat: "#c98f35", front: "#f0bd59", sleeves: "#a9772c", legs: "#635948", accent: "#7dc95e", hair: "#6a4028", }; } } export type FaceVariant = { skin: string; skinShade: string; eyes: string; mouthColor: string; mouthWidth: number; brows: boolean; beard: boolean; hairStyle: "cap" | "long" | "buzz" | "bald"; }; const SKIN_TONES = [ { skin: "#f7c89b", shade: "#eab184" }, { skin: "#e8b483", shade: "#d89e6c" }, { skin: "#c98a5b", shade: "#b87748" }, { skin: "#9c6b43", shade: "#8a5a35" }, { skin: "#7a4f30", shade: "#6a4226" }, ] as const; const EYE_COLORS = ["#23231f", "#37261a", "#2c4a6e", "#3f5d3a"] as const; const MOUTH_COLORS = ["#7d4a39", "#94584a", "#6b3d30"] as const; const HAIR_STYLES = ["cap", "long", "buzz", "bald"] as const; /** Deterministic per-NPC facial features so villagers differ beyond role clothes. */ export function faceVariant(id: string): FaceVariant { const tone = pickByHash(SKIN_TONES, id, "skin"); return { skin: tone.skin, skinShade: tone.shade, eyes: pickByHash(EYE_COLORS, id, "eyes"), mouthColor: pickByHash(MOUTH_COLORS, id, "mouth"), mouthWidth: 0.1 + hashToUnit(`${id}:mouthWidth`) * 0.08, brows: hashToUnit(`${id}:brows`) > 0.3, beard: hashToUnit(`${id}:beard`) > 0.65, hairStyle: pickByHash(HAIR_STYLES, id, "hairStyle"), }; } function pickByHash(options: readonly T[], id: string, salt: string): T { return options[Math.floor(hashToUnit(`${id}:${salt}`) * options.length)]; } export function hashToUnit(input: string): number { let hash = 0; for (let index = 0; index < input.length; index += 1) { hash = (hash * 31 + input.charCodeAt(index)) % 1000; } return hash / 1000; } export function dampAngle( current: number, target: number, smoothing: number, delta: number, ): number { return current + signedAngleDelta(current, target) * (1 - Math.exp(-smoothing * delta)); } export function angularDistance(current: number, target: number): number { return Math.abs(signedAngleDelta(current, target)); } function signedAngleDelta(current: number, target: number): number { return Math.atan2(Math.sin(target - current), Math.cos(target - current)); }