Create Bitmoji-style avatars with body parts, outfits, accessories, and animated pets in Chapter’s game world
Students create personalized avatars to represent themselves in Chapter’s game world. The avatar system uses a Bitmoji-inspired design with composited body parts, outfit layers, and animated accessories.
Avatar appearance is stored in PlayerAvatarConfig:
struct PlayerAvatarConfig: Codable { let baseColor: String // Legacy: base color for circle avatar let emojiFace: String // Legacy: emoji fallback // Expanded avatar (Bitmoji-style) let skinTone: String? // "pale", "light", "medium", "tan", "dark", "deep" let hairStyle: String? // "none", "messy", "short", "long", "bun", etc. let hairColor: String? // Color hex or preset name let eyeStyle: String? // "round", "almond", "wide", "sleepy", etc. let faceShape: String? // "oval", "round", "square", "heart", etc. let topStyle: String? // "tshirt", "hoodie", "jacket", "polo", etc. let topColor: String? let bottomStyle: String? // "jeans", "joggers", "shorts", "skirt" let bottomColor: String? let shoeStyle: String? // "sneakers", "boots", "sandals" let shoeColor: String? let accessory1: String? // "glasses", "hat", "earrings", etc. let accessory2: String? let uniMerchEnabled: String? // "true" if wearing university-branded outfit}
The config uses string values that resolve to typed enums:
var resolvedSkinTone: AvatarSkinTone { AvatarSkinTone(rawValue: skinTone ?? "") ?? .medium}enum AvatarSkinTone: String, CaseIterable, Codable { case pale = "pale" case light = "light" case medium = "medium" case tan = "tan" case dark = "dark" case deep = "deep" var hexColor: String { switch self { case .pale: return "#FFEBD6" case .light: return "#F2D3B8" case .medium: return "#D4A574" case .tan: return "#B98555" case .dark: return "#8B5A3C" case .deep: return "#5C3A21" } }}
enum AvatarHairStyle: String, CaseIterable { case none = "none" case messy = "messy" case short = "short" case long = "long" case bun = "bun" case ponytail = "ponytail" case afro = "afro" case braids = "braids" var icon: String { switch self { case .none: return "🙂" case .messy: return "✨" case .short: return "💇" case .long: return "💁" case .bun: return "👱" case .ponytail: return "🎀" case .afro: return "🌀" case .braids: return "🪢" } } var displayName: String { rawValue.capitalized }}
Hair color uses a horizontal scrollable palette:
ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(AvatarHairColor.allCases, id: \.self) { color in Circle() .fill(Color(hex: color.hexColor)) .frame(width: 36, height: 36) .overlay { if hairColor == color { Circle().stroke(.white, lineWidth: 2) } } .onTapGesture { withAnimation { hairColor = color } } } }}
enum AvatarTopStyle: String, CaseIterable { case tshirt = "tshirt" case hoodie = "hoodie" case jacket = "jacket" case polo = "polo" case sweater = "sweater" case jersey = "jersey" // Uni-branded case varsity = "varsity" // Uni-branded var isUniBranded: Bool { self == .jersey || self == .varsity } var displayName: String { rawValue.capitalized }}
University Merch Toggle:For offer holders and enrolled students, uni-branded tops can display university colors:
if topStyle.isUniBranded, let status = AuthManager.shared.currentUser?.enrollmentStatus, (status == .offerHolder || status == .enrolled), let uni = AuthManager.shared.currentUser?.uni { HStack { VStack(alignment: .leading) { Text("Uni Merch") .font(.caption.bold()) Text(uni.rawValue) .font(.system(size: 10)) .foregroundStyle(.secondary) } Spacer() Circle() .fill(Color(hex: uni.hexColor)) .frame(width: 18, height: 18) Toggle("", isOn: $uniMerchEnabled) .tint(.orange) } .padding(10) .background( RoundedRectangle(cornerRadius: 10) .fill(uniMerchEnabled ? Color.orange.opacity(0.1) : Color.clear) )}
When enabled, the jersey/varsity uses the university’s brand color instead of the custom top color.
Two accessory slots for glasses, hats, jewelry, etc.:
enum AvatarAccessory: String, CaseIterable { case none = "none" case glasses = "glasses" case sunglasses = "sunglasses" case beanie = "beanie" case cap = "cap" case earrings = "earrings" case necklace = "necklace" case scarf = "scarf" var icon: String { switch self { case .none: return "⭕" case .glasses: return "👓" case .sunglasses: return "🕶️" case .beanie: return "🧢" case .cap: return "🎩" case .earrings: return "💎" case .necklace: return "📿" case .scarf: return "🧣" } } var displayName: String { rawValue.capitalized }}