From 1023b3edc65897b49062028640cd7e2254224835 Mon Sep 17 00:00:00 2001 From: pricewowen <231476659+pricewowen@users.noreply.github.com> Date: Sat, 23 May 2026 21:35:59 -0600 Subject: [PATCH] Complete character creation screen Finished the character creation screen with the final layout, UI elements, and customization flow ready for integration. --- components/character/character-creator.tsx | 14 ++- .../character/character-layer-preview.tsx | 14 ++- .../character/character-piece-selector.tsx | 38 ++++++- components/character/tinted-sprite-icon.tsx | 20 +++- lib/character/color-utils.ts | 85 +++++++++++++-- lib/character/presets.ts | 100 +++++++++++++----- public/character/eyes/eyes-1.png | Bin 0 -> 431 bytes public/character/eyes/eyes-2.png | Bin 0 -> 438 bytes public/character/head/head-1.png | Bin 0 -> 282 bytes public/character/head/head-2.png | Bin 0 -> 283 bytes public/character/head/head-3.png | Bin 0 -> 250 bytes public/character/head/head-4.png | Bin 0 -> 315 bytes public/character/head/head-5.png | Bin 0 -> 312 bytes public/character/head/head-6.png | Bin 0 -> 333 bytes public/character/head/head-7.png | Bin 0 -> 308 bytes public/character/pants/pants-1.png | Bin 0 -> 637 bytes public/character/pants/pants-2.png | Bin 0 -> 296 bytes public/character/pants/pants-3.png | Bin 0 -> 351 bytes public/character/shoes/shoes-1.png | Bin 0 -> 258 bytes public/character/shoes/shoes-2.png | Bin 0 -> 269 bytes public/character/shoes/shoes-3.png | Bin 0 -> 267 bytes public/character/skin/skin-1.png | Bin 580 -> 583 bytes public/character/torso/torso-1.png | Bin 261 -> 409 bytes public/character/torso/torso-2.png | Bin 261 -> 468 bytes public/character/torso/torso-3.png | Bin 0 -> 396 bytes public/character/torso/torso-4.png | Bin 0 -> 419 bytes 26 files changed, 232 insertions(+), 39 deletions(-) create mode 100644 public/character/eyes/eyes-1.png create mode 100644 public/character/eyes/eyes-2.png create mode 100644 public/character/head/head-1.png create mode 100644 public/character/head/head-2.png create mode 100644 public/character/head/head-3.png create mode 100644 public/character/head/head-4.png create mode 100644 public/character/head/head-5.png create mode 100644 public/character/head/head-6.png create mode 100644 public/character/head/head-7.png create mode 100644 public/character/pants/pants-1.png create mode 100644 public/character/pants/pants-2.png create mode 100644 public/character/pants/pants-3.png create mode 100644 public/character/shoes/shoes-1.png create mode 100644 public/character/shoes/shoes-2.png create mode 100644 public/character/shoes/shoes-3.png create mode 100644 public/character/torso/torso-3.png create mode 100644 public/character/torso/torso-4.png diff --git a/components/character/character-creator.tsx b/components/character/character-creator.tsx index f817438..b8c37fb 100644 --- a/components/character/character-creator.tsx +++ b/components/character/character-creator.tsx @@ -15,7 +15,7 @@ import { clampHsl } from "@/lib/character/color-utils"; import { CHARACTER_LAYERS, COLOR_PRESETS, - DEFAULT_SKIN_COLOR, + DEFAULT_GRAY_COLOR, LAYER_DEFAULT_COLORS, buildDefaultVariants, getLayerById, @@ -41,7 +41,7 @@ export function CharacterCreator() { buildDefaultVariants, ); const [activePresetId, setActivePresetId] = useState( - DEFAULT_SKIN_COLOR.id, + DEFAULT_GRAY_COLOR.id, ); const [activeLayerId, setActiveLayerId] = useState< (typeof CHARACTER_LAYERS)[number]["id"] @@ -69,7 +69,14 @@ export function CharacterCreator() { const handleLayerChange = (layerId: (typeof CHARACTER_LAYERS)[number]["id"]) => { setActiveLayerId(layerId); - setActivePresetId(""); + const layerColor = colors[layerId]; + const matchingPreset = COLOR_PRESETS.find( + (preset) => + preset.hsl.h === layerColor.h && + preset.hsl.s === layerColor.s && + preset.hsl.l === layerColor.l, + ); + setActivePresetId(matchingPreset?.id ?? ""); }; const handleVariantChange = (variantId: string) => { @@ -107,6 +114,7 @@ export function CharacterCreator() { variants={pieceVariants} activeId={variants[activeLayerId]} color={activeColor} + skinColor={colors.skin} onSelect={handleVariantChange} /> diff --git a/components/character/character-layer-preview.tsx b/components/character/character-layer-preview.tsx index 2dbfd57..069ff34 100644 --- a/components/character/character-layer-preview.tsx +++ b/components/character/character-layer-preview.tsx @@ -65,10 +65,22 @@ export function CharacterLayerPreview({ for (const { layer, image } of images) { const layerColor = colors[layer.id]; + const rgb = hslToRgb(layerColor.h, layerColor.s, layerColor.l); + drawTintedSprite( ctx, image, - hslToRgb(layerColor.h, layerColor.s, layerColor.l), + rgb, + layer.id === "eyes" + ? { + mode: "eyes", + skinColor: hslToRgb( + colors.skin.h, + colors.skin.s, + colors.skin.l, + ), + } + : undefined, ); } } diff --git a/components/character/character-piece-selector.tsx b/components/character/character-piece-selector.tsx index 1c8c372..9b05be1 100644 --- a/components/character/character-piece-selector.tsx +++ b/components/character/character-piece-selector.tsx @@ -1,7 +1,9 @@ "use client"; +import { CircleSlash } from "lucide-react"; + import type { HSL } from "@/lib/character/color-utils"; -import { PIECE_SLOT_COUNT } from "@/lib/character/presets"; +import { NONE_VARIANT_ID, PIECE_SLOT_COUNT } from "@/lib/character/presets"; import { cn } from "@/lib/utils"; import type { LayerVariant } from "@/lib/character/presets"; @@ -11,6 +13,7 @@ type CharacterPieceSelectorProps = { variants: LayerVariant[]; activeId: string; color: HSL; + skinColor?: HSL; onSelect: (variantId: string) => void; }; @@ -18,6 +21,7 @@ export function CharacterPieceSelector({ variants, activeId, color, + skinColor, onSelect, }: CharacterPieceSelectorProps) { const slots = Array.from({ length: PIECE_SLOT_COUNT }, (_, index) => @@ -37,6 +41,31 @@ export function CharacterPieceSelector({ ); } + if (variant.id === NONE_VARIANT_ID) { + return ( + + ); + } + return ( ); })} diff --git a/components/character/tinted-sprite-icon.tsx b/components/character/tinted-sprite-icon.tsx index dbc2407..f391348 100644 --- a/components/character/tinted-sprite-icon.tsx +++ b/components/character/tinted-sprite-icon.tsx @@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"; type TintedSpriteIconProps = { src: string; color: HSL; + skinColor?: HSL; size?: number; className?: string; }; @@ -20,6 +21,7 @@ type TintedSpriteIconProps = { export function TintedSpriteIcon({ src, color, + skinColor, size = 40, className, }: TintedSpriteIconProps) { @@ -41,7 +43,21 @@ export function TintedSpriteIcon({ canvas.width = image.width; canvas.height = image.height; ctx.clearRect(0, 0, image.width, image.height); - drawTintedSprite(ctx, image, hslToRgb(color.h, color.s, color.l)); + + const rgb = hslToRgb(color.h, color.s, color.l); + const isEyesLayer = src.includes("/character/eyes/"); + + drawTintedSprite( + ctx, + image, + rgb, + isEyesLayer && skinColor + ? { + mode: "eyes", + skinColor: hslToRgb(skinColor.h, skinColor.s, skinColor.l), + } + : undefined, + ); } render(); @@ -49,7 +65,7 @@ export function TintedSpriteIcon({ return () => { cancelled = true; }; - }, [src, color]); + }, [src, color, skinColor]); return ( = 240 && g >= 240 && b >= 240; +} + +function isMagentaPixel(r: number, g: number, b: number): boolean { + return r >= 200 && g <= 80 && b >= 200; +} + +function applyMonochromeTint( + data: Uint8ClampedArray, + index: number, + color: RGB, + brightness?: number, +): void { + const pixelBrightness = + brightness ?? + Math.max(data[index], data[index + 1], data[index + 2]) / 255; + + data[index] = Math.round(color.r * pixelBrightness); + data[index + 1] = Math.round(color.g * pixelBrightness); + data[index + 2] = Math.round(color.b * pixelBrightness); +} + +/** Darkest shadow in the skin template — magenta maps here, not bright red. */ +const SKIN_TEMPLATE_SHADOW = hexToRgb("#330000"); +const SKIN_SHADOW_BRIGHTNESS = + Math.max( + SKIN_TEMPLATE_SHADOW.r, + SKIN_TEMPLATE_SHADOW.g, + SKIN_TEMPLATE_SHADOW.b, + ) / 255; + +/** Eyes: white stays white, magenta uses skin color, other pixels use eye color. */ +export function tintEyesImageData( + imageData: ImageData, + eyeColor: RGB, + skinColor: RGB, +): void { + const { data } = imageData; + + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + if (alpha === 0) continue; + + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + + if (isWhitePixel(r, g, b)) { + data[i] = 255; + data[i + 1] = 255; + data[i + 2] = 255; + continue; + } + + if (isMagentaPixel(r, g, b)) { + applyMonochromeTint(data, i, skinColor, SKIN_SHADOW_BRIGHTNESS); + continue; + } + + applyMonochromeTint(data, i, eyeColor); + } +} + +export type TintOptions = { + mode?: "monochrome" | "eyes"; + skinColor?: RGB; +}; + export function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const image = new Image(); @@ -159,6 +223,7 @@ export function drawTintedSprite( ctx: CanvasRenderingContext2D, image: HTMLImageElement, color: RGB, + options?: TintOptions, ): void { const width = image.width; const height = image.height; @@ -171,7 +236,13 @@ export function drawTintedSprite( offscreenCtx.drawImage(image, 0, 0); const imageData = offscreenCtx.getImageData(0, 0, width, height); - tintImageData(imageData, color); + + if (options?.mode === "eyes" && options.skinColor) { + tintEyesImageData(imageData, color, options.skinColor); + } else { + tintImageData(imageData, color); + } + offscreenCtx.putImageData(imageData, 0, 0); ctx.drawImage(offscreen, 0, 0); } diff --git a/lib/character/presets.ts b/lib/character/presets.ts index 161d980..0889070 100644 --- a/lib/character/presets.ts +++ b/lib/character/presets.ts @@ -21,7 +21,21 @@ export type LayerVariant = { src: string; }; -export type CharacterLayerId = "skin" | "pants" | "torso" | "head" | "eyes"; +export const NONE_VARIANT_ID = "none"; + +export const NONE_VARIANT: LayerVariant = { + id: NONE_VARIANT_ID, + label: "None", + src: "", +}; + +export type CharacterLayerId = + | "skin" + | "pants" + | "shoes" + | "torso" + | "eyes" + | "head"; export type CharacterLayer = { id: CharacterLayerId; @@ -45,10 +59,33 @@ function presetFromHex(id: string, name: string, hex: string): ColorPreset { return presetFromHsl(id, name, rgbToHsl(rgb.r, rgb.g, rgb.b)); } +function layerVariants( + layerId: Exclude, + label: string, + count: number, +): LayerVariant[] { + return Array.from({ length: count }, (_, index) => { + const number = index + 1; + return { + id: `${layerId}-${number}`, + label: `${label} ${number}`, + src: `/character/${layerId}/${layerId}-${number}.png`, + }; + }); +} + +function layerVariantsWithNone( + layerId: Exclude, + label: string, + count: number, +): LayerVariant[] { + return [NONE_VARIANT, ...layerVariants(layerId, label, count)]; +} + export const COLOR_PRESETS: ColorPreset[] = [ - presetFromHsl("black", "Black", { h: 0, s: 0, l: 5 }), presetFromHsl("gray", "Gray", { h: 0, s: 0, l: 50 }), presetFromHsl("white", "White", { h: 0, s: 0, l: 100 }), + presetFromHsl("black", "Black", { h: 0, s: 0, l: 5 }), presetFromHex("red", "Red", "#FF2020"), presetFromHex("orange", "Orange", "#FF8800"), presetFromHex("yellow", "Yellow", "#FFD700"), @@ -66,11 +103,14 @@ export const COLOR_PRESETS: ColorPreset[] = [ presetFromHex("onyx", "Onyx", "#140C08"), ]; -export const DEFAULT_SKIN_COLOR = - COLOR_PRESETS.find((preset) => preset.id === "light")!; +export const DEFAULT_GRAY_COLOR = + COLOR_PRESETS.find((preset) => preset.id === "gray")!; + +export const DEFAULT_SKIN_COLOR = DEFAULT_GRAY_COLOR; export const PIECE_SLOT_COUNT = 8; +/** Stack order: bottom → top */ export const CHARACTER_LAYERS: CharacterLayer[] = [ { id: "skin", @@ -84,42 +124,48 @@ export const CHARACTER_LAYERS: CharacterLayer[] = [ id: "pants", label: "Pants", allowVariants: true, - variants: [], + variants: layerVariantsWithNone("pants", "Pants", 3), + }, + { + id: "shoes", + label: "Shoes", + allowVariants: true, + variants: layerVariantsWithNone("shoes", "Shoes", 3), }, { id: "torso", label: "Torso", allowVariants: true, - variants: [ - { id: "torso-1", label: "Torso 1", src: "/character/torso/torso-1.png" }, - { id: "torso-2", label: "Torso 2", src: "/character/torso/torso-2.png" }, - ], - }, - { - id: "head", - label: "Head", - allowVariants: true, - variants: [], + variants: layerVariantsWithNone("torso", "Torso", 4), }, { id: "eyes", label: "Eyes", allowVariants: true, - variants: [], + variants: layerVariantsWithNone("eyes", "Eyes", 2), + }, + { + id: "head", + label: "Head", + allowVariants: true, + variants: layerVariantsWithNone("head", "Head", 7), }, ]; export const LAYER_DEFAULT_COLORS: Record = { - skin: DEFAULT_SKIN_COLOR.hsl, - pants: COLOR_PRESETS.find((preset) => preset.id === "blue")!.hsl, - torso: COLOR_PRESETS.find((preset) => preset.id === "red")!.hsl, - head: COLOR_PRESETS.find((preset) => preset.id === "yellow")!.hsl, - eyes: COLOR_PRESETS.find((preset) => preset.id === "indigo")!.hsl, + skin: DEFAULT_GRAY_COLOR.hsl, + pants: DEFAULT_GRAY_COLOR.hsl, + shoes: DEFAULT_GRAY_COLOR.hsl, + torso: DEFAULT_GRAY_COLOR.hsl, + eyes: DEFAULT_GRAY_COLOR.hsl, + head: DEFAULT_GRAY_COLOR.hsl, }; export function buildDefaultVariants(): LayerVariantState { return CHARACTER_LAYERS.reduce((state, layer) => { - state[layer.id] = layer.variants[0]?.id ?? ""; + state[layer.id] = layer.allowVariants + ? NONE_VARIANT_ID + : (layer.variants[0]?.id ?? ""); return state; }, {} as LayerVariantState); } @@ -132,9 +178,15 @@ export function getSelectedVariant( layerId: CharacterLayerId, variantId: string, ): LayerVariant | undefined { - return getLayerById(layerId).variants.find( - (variant) => variant.id === variantId, + if (variantId === NONE_VARIANT_ID) return undefined; + + const variant = getLayerById(layerId).variants.find( + (entry) => entry.id === variantId, ); + + if (!variant?.src) return undefined; + + return variant; } /** @deprecated Use DEFAULT_SKIN_COLOR */ diff --git a/public/character/eyes/eyes-1.png b/public/character/eyes/eyes-1.png new file mode 100644 index 0000000000000000000000000000000000000000..1b2a17acfa6c3703999ccf001851fd1363816d2c GIT binary patch literal 431 zcmV;g0Z{&lP)IE1ACNE9R2v-0A02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{009?CL_t(Y$L*3KPXj>^g}-a6 z5GPvEw8^!}c2!lg7hL~B-5F9**Wyr3ia3S3pm5|Ig75>7MdB_cc)}7|LQS#QMNl?( z$DvS=dv4ymd9yRK@aN&AgP~CLTYKounz7%`2rrf^MAPH`^_^>dXZdPT@yTQ#SILZl z^c#e6Lciq!%9>QtqOZ=blGFz?>Vuihxk@tj+vv?YV_eP8B>e^eZ?~ru=9a4rSILX= zPcO=)cVG>AqV~IUjJh3k6tG>)F&JYoMn?fU3T(Vx%+XQ6sN4B@1ON<7Qx~4%fGKWl z4Mfu;jFZm=-W)rV-OR3%Om-YV7$;mlK7ap6L!s!28ZS=|oSh#Ob~`$}$`qFIE1ACO2N8kO2Sy02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00ACJL_t(Y$L*4#Z^BRz#(yQK zi3$(|l0s6dP~AoXW6*B>Y@vE`p-?bNTzG^5 z>Wj-jFVU`B1dEt?G&5L&59;9A2lmUEK?K1#=H~hJ`$y{Af{tC`?d6I2)oHHHv+-@J gxj=ye1^yX60ZqkSh^qL-L;wH)07*qoM6N<$f+y~~yZ`_I literal 0 HcmV?d00001 diff --git a/public/character/head/head-1.png b/public/character/head/head-1.png new file mode 100644 index 0000000000000000000000000000000000000000..19a7a71d1df90685cc4f2d490b22b445d1b87bcc GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1qVw#Nbv{r?vcxr_#5q4VH#M(>!MP|ku_QG` zp**uBL&4qCHz2%`PaLSI$ML7h~$SJnUZTQ>1z4r-?LlUwisMkeWui!&3cxL(!?LydY5k} VR{tqG5esw;gQu&X%Q~loCIGN4V%q=! literal 0 HcmV?d00001 diff --git a/public/character/head/head-2.png b/public/character/head/head-2.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c8cdaa713ac343d8d4bbe3a38de9933b6726f2 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1V)Av~VoRWqWQl7;iF1B#Zfaf$gL6@8Vo7R> zLV0FMhJw4NZ$Nk>pEyuav!{z=h{y4_6AucuD)6|-pL@@d?d5-e)`Ddf0>=dkY+fl= zH{Mh^J)0q4`>v=_M%g{Hm;cYQ-R|t!`O5jy<<&)3SFpRfPx`YVW(V^@oh95qWCD?_)^mS!_#VWz1V%e>KzXd2HS>hT|;+&tGo0?a`;9QiNSdyBe zP@Y+mq2TW68xY>eCk|AU)^mS!_#VWz1${b>0UJewJEOCt}an8@pP0cG|a4t$sEJ;mK zD90-ej?>FVdQ&MBb@ E064#Ky#N3J literal 0 HcmV?d00001 diff --git a/public/character/head/head-5.png b/public/character/head/head-5.png new file mode 100644 index 0000000000000000000000000000000000000000..a024c0cd2606579096be8c567650c65916ae8200 GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1%DLt3y-c8xWQl7;iF1B#Zfaf$gL6@8Vo7R> zLV0FMhJw4NZ$Nk>pEyv_0#6sm5Rc<;C!OYHR^)MEpVIfDb5V@&x|2H{Osuob+UF>J zRAD(?<@SU5kH^iwFC0E>&rOQAPTYUoEiEKujgzpk@MJAEhK6Z(YEu~u8#tnx%j}A4 zwd`{r7H{3@e9k9zV&DYNdw%8fBn$SbEnA%t+oNxCYU=AlvQiEvGyD51wi$#h$qk!) zNNsh*?v>Go^UgNf_n5Aet?KhGwvpj$-~WL3>}~IDUN&)_K({h@y85}Sb4q9e0Ca6_ ABme*a literal 0 HcmV?d00001 diff --git a/public/character/head/head-6.png b/public/character/head/head-6.png new file mode 100644 index 0000000000000000000000000000000000000000..0ff3b9d1fb628fbacfeec19475ab783f5ad8be6b GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1${*WT9Rn1SEOCt}an8@pP0cG|a4t$sEJ;mK zD9gnPb;&J?KkRxBS0Z;L&zZ&&i`HS+E@?Ys`wrHg0So=)Q zTDeH#_zus;_-3O?jbfIv8J8cOowmdF_AZ|T4$-;svijd*CYekTJv`In&`M*?lY)MS zr1rTUcK>fZ+o69(K*O88nhjS?Y6`X-&(HI`wC3K0b(W@Eq;r~+`ZEIV%*pSKh_#o< zSUUfI@Egu)=3ORICwf=QSax#W2s-iL-fa)fWx8A!LLYLN9(yeQ__v$Aq2WjI3MSWs Wp-rb4D{6rrVDNPHb6Mw<&;$Sy&3OX= literal 0 HcmV?d00001 diff --git a/public/character/head/head-7.png b/public/character/head/head-7.png new file mode 100644 index 0000000000000000000000000000000000000000..721749b3b794645a48ae1dff47e872ecc7cd6f7f GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1#_&wWln*E*S>hT|;+&tGo0?a`;9QiNSdyBe zP@Y+mq2TW68xY>eCk|9J$J50z#N+teNe6k4DDbd|Pw9Kn`MPsff$u*4i@80%a)BlC zVNM*$`e_UmD@1Zb94hZN-BH$BQ@pOq?WEZC+0$klo?LyziP>bgR)f}p+B1u#--=j& z=l*!IPw>bZ!=y(tHj*8R`pdqGNq#uVvx@OYz@C3QUL0A_U+q4}YNI9ZCRKfNe-E2W vM;(sUh6_D!mQ_=Hm^+nq@me7G|BCyNzK!8WrRZrucQSao`njxgN@xNAx94m3 literal 0 HcmV?d00001 diff --git a/public/character/pants/pants-1.png b/public/character/pants/pants-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0d8d741d6232eca420abc94de58befe5bcd1ccb9 GIT binary patch literal 637 zcmV-@0)qXCP)IE1AC=ci$1rY!M02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00HGmL_t(Y$L*7`OIuMC#(!x` zp$@M!5j27geR+t4P~^oRlt?x^xwX=g!O|f(6kN>MDIgT5LWUAS3|18TAGjI^DfiLA zbkJvlsi7E%L)sv8(Lvtjy}WBEZi3%*?>Xn5^PO|P3%4Et_hni)85`=O`ui8NiKm{x zlBlDHhsl@A9{U5;<9s%WW*VMABQe0o;&&f|d+v)pesVg}nnB zBMb807+IjOcc5e-6X@Y#xu*WgBBK*&G}Ayc4Mr!@^j8*p3u#(6iRnR3YKMIO@>Ur< zKexg9(XpCAeCj1Xt7ohq9Wy_-@n4TU7=Lk9H4}|c{q%}VG{W}GB-=BS9-Fd-f8rYt z6R*icBYfSrXqA^}TeeRFp;V5*qmX)y?#I`OMy1aC0Dpfp=$!u{{k)B}yh~W2c>rs9 zm$gdWKg4FMAtzrFbsv3zwq;Wib-xm`@g$w|rVIkG#VOql*y2C~FEEn5Z! zw{|X`b^W?CfM_J(Q=)^mS!_#VWz1A(G^J<}Xl4vcxr_#5q4VH#M(>!MP|ku_QG` zp**uBL&4qCHz2%`PaLRdf~SjPh{y5d1PRu~2_ijBfeg$&FPM(;@cdtB!e!CDAn?nE z1mjXQzT1~Jb{IY^zNe#cjD5z3OgrwQn@<=vL>PQvTX3XlTML78&s?*|iEM0b^55eP z|5W^$)d&O++t+9uPINVFlK@GyuQQ6N{m58uGH?HXuGPLfjs)@uJV{Eo;NCj5v61np fT8e2iHv_}(ql!Pp^bSk{x{blp)z4*}Q$iB}$+2Vz literal 0 HcmV?d00001 diff --git a/public/character/pants/pants-3.png b/public/character/pants/pants-3.png new file mode 100644 index 0000000000000000000000000000000000000000..0df9b4a105067fc308e56900187d64e3871cc2d0 GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1Au01^g9A`Vvcxr_#5q4VH#M(>!MP|ku_QG` zp**uBL&4qCHz2%`PaLS|xTlL_h{y4_lQ(iT8;H32UvWCFmt?+h|AdYIgZH2O=WlLY zcJ@w=?A&lgJ}bI#1rTU>FlN{@q(!>8s|o*1p3lk9kX_E0P!?uV z(9KX@Z_9A}-696Lb4(YG6sD!K=3ld(n4Yfc5O&EsXQo4naqt$lc(Dn$_ptg(JACzi zyfgVIgXtP+|CE}Eg+H}Evq_dSBz~^3`&hI0@_GgXJx>+(59iDJ8{*u3(mu>{j{O+g n>3jdum)xTL>#qF&*UDda%6F!%)gfS*Ffe$!`njxgN@xNAVy%8` literal 0 HcmV?d00001 diff --git a/public/character/shoes/shoes-1.png b/public/character/shoes/shoes-1.png new file mode 100644 index 0000000000000000000000000000000000000000..6bed3ef0e843a7a9aae10760ba792a180e75c8ef GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1ZgOw)hhIP;$r9Iy66gHf+|;}h2Ir#G#FEq$ zh4Rdj3)^mS!_#VWz1Ze_`R_b*ULvcxr_#5q4VH#M(>!MP|ku_QG` zp**uBL&4qCHz2%`PaLSI)YHW=#N&8!f&}a01d*PmKm)E795XJm_DD)d)Btgsk%7US z)P#fyNeKxzdQ|?)_}X%dIsX^lFhy{Ak0JBn|NjGO+8Q73Hx!xgC}i%onc*;NdZ9$! zG>PXoG&+rI_J99mKS|AP#_5ElIbM%?+~4h$ak)^mS!_#VWz1!TfNVrxj30vcxr_#5q4VH#M(>!MP|ku_QG` zp**uBL&4qCHz2%`PaLSI*we)^#N+tei5qzv40u@d9S*a2Oj$EwnyADC%ZNs!$VRbL zCa=pXlSDOTSzG;o9m?OIdNM(xEm1=L()BiWvjlb}1LpQu_pjD`e`zV_vWcnVJlCo3 z3RcEfypxhEPnYkCnBMu^Ymw8sn6N#CGw)O`mEZf3_3U1`sfoLCmIJM4@O1TaS?83{ F1OU)wSG52D literal 0 HcmV?d00001 diff --git a/public/character/skin/skin-1.png b/public/character/skin/skin-1.png index ad00028f45fa139c1da4cefc37705efecd6f6fcf..b59792993adda69d8eb7f380655b05b679244bc0 100644 GIT binary patch delta 474 zcmV<00VV##1jhuBQwSIWC?neVY4VXL_t(Y$IVs2YQjJeeVceHwP3voMMy4E ziUcDfet;dYB=vzZEv;~0a}Q_z<;g!5?@Ku$I(Wv;?B0r2TKM1KV- z$w53h0H6c#DP^JKkP8^XZ^%KM+0~qt`%Oxj>^IavuRt)qCu<-8F0FIHlxMm<0dT)s z9es6<<7m-Y1TdDYNLjMq1z4^IKemC@f-5oQEtx%4!BzhnHLFG11tjfVKah6sCJXQo zKjP`^O9bkYa|QgE+@*x%LV3wj{eN;!DRniJin|U9dLUE9u+sx)j0yydq>6@}-d=;{ z>+``jR@<59Wcpy7ygX=L<5=S_0l=(Wfr&*5#M1!FHY-;EEOr&}8GsyiD0_kQtPLHD zS8(>R?lm^-I`MPZecfbAasUR$&kNREM=2|}!Z1gJ+GnE Q4FCWD07*qoM6N<$f?+e=8UO$Q delta 471 zcmV;|0Vw{*1jGc8QwSFpAt|V%AYYMND1WR;L_t(Y$IVsEYQjJe{xT*+=mhFMWU>b1HoWl_lU+Dy2P$MGz^a&~6X9>t?h0p*^$%IXFA> zeY4-rjKE(Tk$()V7M$7%!rczdW-}HTMG*$4r=Txy2p6*?fShbn%3OtM3gFXmh<^%F zl7o1106+)eQ_4ceAr~-&-;jeiv#Yr)_nVY5*>9+UUWQ=&Ox8dETw3RXDa~|e0^okV zF8p+k<7m;@1Yj)Zn*huOSgHm;wt>}xTc84&JyyY0{~9%`McNr8?R`IxcJC$$@DM%X z>Fi4c>XK6h{FvUwgycebhfnp(Ie(?t(@-kjI>_jOL>0qM51et5^Mt&jVW)T0VD5wA;wycVVT_;mz$@=` z!KmNOO&;~Tu+5ErC*H2!a{<(t#8wV6l7k{!`B#5!e57T?(|rHOz5%@i(FtM(xoH3Z N002ovPDHLkV1l~3-If3V diff --git a/public/character/torso/torso-1.png b/public/character/torso/torso-1.png index 5a25f7f725a5329b4443a3bcc7d9bdeceb30b996..d3832932f58daf8301fc89a6513dc2178b1e0c97 100644 GIT binary patch delta 300 zcmV+{0n`440+|DlQwSIWCk=tNc=C~3Cw~C}Nkl6Om_T9VL1ne zJMi7PbMH3{I2?}Sqx=8~`cf+5N|}Bq+-UtyxY61{i1Gsfgjqm>9s*6^H;P0Qet)uB z76JHp`_eIR1!$?n$#hP#7~2Knbl=T&*GOYCEpYxlVdfRA+jS$2I=4(M8%6m6Ufze% z9*?$Zo;mw}83JOI=%2F^g>4C4V`g@Ek|DAVq|v69N(@?6E4fyGvGV@B!t=!~0FMvX ydq=o?dIum`jBOJS9R}#V-g9$Wb{Rj8xq?S{Hj7^{gGUL}VE_OC07*qoL-! zgHxK`AYB5d{sFP2EzrOv938_yLI*b&|AdAJnhJ7~3=D)$a1gzr!8qi68@l)I6l!W{ zxDPx$UtXT)dB5)u2nh-K6Er0<**vMe1FTL7 z8@~^EdAtW80#K~hF+>il(~?bK+GtDNF#(9jYnM%lQzma18}Zgt0GG8+2lC{$_)kk24XAf>B?t5uW&>xD|qcVPGKykT883pb7f z7uF5vU=QMlUDq8bmr8pM)+xKW8B}mk{_PpX_e9TT>hwfAN*}Hi_WEy(AD&j~x}a&n zqD(dqKyrHrBbFc?OR`wlyv{B9pEEvY69DfIgoOMbz5q30j?7H&2weaG002ovPDHLk FV1hA!o-68xq?S{Hj7^{gGUL}VE_OC07*qoL)^mS!_#VWz1E;&1AO+Qdbvcxr_#5q4VH#M(>!MP|ku_QG` zp**uBL&4qCHz2%`PaLS|i>HfYh{y4_lkB?>8A!Cx4|8k@khyYptM~D~?~1=9V~XUT zs!f+Z%K!LL*m1q>ySDNvu@+hsIZvK&Y)-*pUMG1D-+T7^zl%#rNJ#vUFAAtXd2}I@ zq=t!M{1GmZqqjN~N}U7O{JimI!h$8|c5dbW=Cyaqzf%rtSf1|cs6Dz?x#~*rede9> zy6?=KU@bhg{M&bi#|J{+e9;Z4%Bxw^u(5K6zOsvxnQ%eo&pDh7tGrJy+NTP2 literal 0 HcmV?d00001 diff --git a/public/character/torso/torso-4.png b/public/character/torso/torso-4.png new file mode 100644 index 0000000000000000000000000000000000000000..9ebdc7b2289c147a2050d4a789ad941fa057e09b GIT binary patch literal 419 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qfx`y?k)@<7Q@wL`<4PJ&H|6f zVg?31We{epSZZGe6l5>)^mS!_#VWz1E?>mYxeF*HS>hT|;+&tGo0?a`;9QiNSdyBe zP@Y+mq2TW68xY>eC(gjY$nELk7~*mK?Ic4#W^pSTfzQaJ9Os$4as5H z8oPUacQIS6_^gyPz6v1$R;`O!#S6YZi^%?bk>&AOu4k-|n=0kMm*0pvZtGCDSn%9( z`y`=>KQk`rP7rHn(!BJtanAN%XQ!Tj+~m2%%uvv3=If95e=w%4Y!LkZA>|}6>=-;< L{an^LB{Ts5&gP|! literal 0 HcmV?d00001