| 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; |
|
|
| |
| 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<T>(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)); |
| } |
|
|