Initial commit

This commit is contained in:
Allen
2026-04-30 02:14:25 +00:00
commit 7090e6f01d
243 changed files with 115122 additions and 0 deletions
@@ -0,0 +1,997 @@
/**
* AvatarSystem - Main API for Mii-style avatar system
* Coordinates data, rendering, animations, and accessories
*
* Depends on: AvatarData, AvatarAnimations, AvatarRenderer, AvatarAccessories, AccessoryRenderer
*/
(function() {
'use strict';
// ─── Private State ───────────────────────────────────
// In-memory cache of loaded avatars
var avatarCache = new Map();
// Animation state per avatar
var animationState = new Map();
// ─── Private Helpers ─────────────────────────────────
/**
* Validate avatar data structure
* @param {Object} data - Avatar data to validate
* @returns {boolean} True if valid
*/
function validateAvatar(data) {
if (!data || typeof data !== 'object') return false;
if (!data.base || typeof data.base !== 'object') return false;
// Check required base fields exist
var requiredFields = ['faceShape', 'skinTone', 'eyes', 'eyebrows', 'nose', 'mouth', 'ears', 'hair', 'body', 'clothing'];
for (var i = 0; i < requiredFields.length; i++) {
if (data.base[requiredFields[i]] === undefined) {
return false;
}
}
// Check nested objects
if (!data.base.eyes || typeof data.base.eyes !== 'object') return false;
if (!data.base.eyes.shape || !data.base.eyes.color) return false;
if (!data.base.hair || typeof data.base.hair !== 'object') return false;
if (!data.base.hair.style || !data.base.hair.color) return false;
if (!data.base.body || typeof data.base.body !== 'object') return false;
if (!data.base.body.type) return false;
if (!data.base.clothing || typeof data.base.clothing !== 'object') return false;
if (!data.base.clothing.style || !data.base.clothing.shirtColor || !data.base.clothing.pantsColor) return false;
return true;
}
/**
* Create a default avatar with standard settings
* @returns {Object} Default avatar object
*/
function createDefault() {
return {
userId: null,
base: {
faceShape: 'oval',
skinTone: '#d4a373',
eyes: { shape: 'round', color: 'brown' },
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: { style: 'short-messy', color: '#2c1810' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#3498db', pantsColor: '#2c3e50' }
},
earned: { eyewear: [], headwear: [], effects: [] },
equipped: { eyewear: null, headwear: null, effect: null },
level: 1,
xp: 0
};
}
/**
* Deep clone an avatar object
* @param {Object} avatar - Avatar to clone
* @returns {Object} Cloned avatar
*/
function cloneAvatar(avatar) {
return JSON.parse(JSON.stringify(avatar));
}
/**
* Merge customization into base avatar
* @param {Object} target - Target avatar base
* @param {Object} source - Source customization
* @returns {Object} Merged base object
*/
function deepMergeBase(target, source) {
var result = Object.assign({}, target);
for (var key in source) {
if (source.hasOwnProperty(key)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = Object.assign({}, target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
}
/**
* Get animation frame based on time
* @param {Object} anim - Animation definition
* @param {number} startTime - Animation start time
* @param {boolean} loop - Whether to loop
* @returns {Object} Frame info with index and complete flag
*/
function getAnimationFrame(anim, startTime, loop) {
var elapsed = performance.now() - startTime;
var totalDuration = anim.frameDuration * anim.frameCount;
if (!loop && elapsed >= totalDuration) {
return { frame: anim.frameCount - 1, complete: true };
}
var frameIndex = Math.floor((elapsed / anim.frameDuration) % anim.frameCount);
return { frame: frameIndex, complete: false };
}
// ─── Public API ──────────────────────────────────────
window.AvatarSystem = {
// ─── Creation ──────────────────────────────────────
/**
* Create a new avatar from customization choices
* @param {Object} customization - Full base customization object
* @returns {Object} Complete avatar object
*/
create: function(customization) {
var avatar = createDefault();
if (customization) {
avatar.base = deepMergeBase(avatar.base, customization);
}
return avatar;
},
/**
* Load user's saved avatar (from API or storage)
* @param {string} userId - User ID to load avatar for
* @returns {Promise<Object>} Avatar object
*/
loadUserAvatar: function(userId) {
var self = this;
return new Promise(function(resolve) {
// Check cache first
if (avatarCache.has(userId)) {
resolve(cloneAvatar(avatarCache.get(userId)));
return;
}
// In production, this would fetch from API
// For now, check localStorage as fallback
var storageKey = 'avatar_' + userId;
var stored = localStorage.getItem(storageKey);
if (stored) {
try {
var parsed = JSON.parse(stored);
if (validateAvatar(parsed)) {
avatarCache.set(userId, parsed);
resolve(cloneAvatar(parsed));
return;
}
} catch (e) {
console.warn('AvatarSystem: Failed to parse stored avatar', e);
}
}
// Return a guest avatar for unknown users
var guest = typeof AvatarData !== 'undefined' && AvatarData.getRandomGuestAvatar
? AvatarData.getRandomGuestAvatar()
: { base: createDefault().base };
var avatar = createDefault();
avatar.userId = userId;
avatar.base = Object.assign({}, avatar.base, guest.base);
resolve(avatar);
});
},
/**
* Save avatar to cache (and would persist to API)
* @param {Object} avatar - Avatar to save
* @returns {Object} Saved avatar
*/
saveAvatar: function(avatar) {
if (avatar.userId) {
avatarCache.set(avatar.userId, cloneAvatar(avatar));
// Also save to localStorage for persistence
var storageKey = 'avatar_' + avatar.userId;
try {
localStorage.setItem(storageKey, JSON.stringify(avatar));
} catch (e) {
console.warn('AvatarSystem: Failed to save avatar to localStorage', e);
}
}
return avatar;
},
/**
* Clear avatar from cache
* @param {string} userId - User ID to clear
*/
clearCache: function(userId) {
if (userId) {
avatarCache.delete(userId);
} else {
avatarCache.clear();
}
},
// ─── Rendering ─────────────────────────────────────
/**
* Render avatar in specified mode
* @param {Object} avatar - Avatar object
* @param {string} mode - 'HEAD_ONLY', 'HEAD_AND_SHOULDERS', 'UPPER_BODY', 'FULL_BODY'
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - Center X position
* @param {number} y - Center Y position
* @param {Object} options - { scale, animation, frame, time }
*/
render: function(avatar, mode, ctx, x, y, options) {
options = options || {};
var scale = options.scale || 1;
var time = options.time !== undefined ? options.time : performance.now() / 1000;
// Get animation transforms if animating
var transforms = {};
if (options.animation && typeof AvatarAnimations !== 'undefined') {
var anim = AvatarAnimations.getAnimation(mode, options.animation);
if (anim) {
var frame;
if (options.frame !== undefined) {
frame = options.frame;
} else {
frame = Math.floor((time * 1000 / anim.frameDuration) % anim.frameCount);
}
transforms = anim.frames[frame] || {};
}
}
// Render base avatar
if (typeof AvatarRenderer !== 'undefined') {
AvatarRenderer.render(ctx, avatar, mode, x, y, {
scale: scale,
transforms: transforms
});
} else {
console.warn('AvatarSystem: AvatarRenderer not loaded');
}
// Render accessories on top
if (typeof AccessoryRenderer !== 'undefined') {
AccessoryRenderer.render(ctx, avatar, mode, x, y, scale, time);
}
},
/**
* Render avatar to an offscreen canvas
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @param {Object} options - Render options
* @returns {HTMLCanvasElement} Canvas with rendered avatar
*/
renderToCanvas: function(avatar, mode, options) {
options = options || {};
var modeData = this.getModeSize(mode);
var scale = options.scale || 1;
var canvas = document.createElement('canvas');
canvas.width = modeData.width * scale;
canvas.height = modeData.height * scale;
var ctx = canvas.getContext('2d');
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
this.render(avatar, mode, ctx, centerX, centerY, options);
return canvas;
},
/**
* Render avatar to a data URL
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @param {Object} options - Render options
* @returns {string} Data URL of rendered avatar
*/
renderToDataURL: function(avatar, mode, options) {
var canvas = this.renderToCanvas(avatar, mode, options);
return canvas.toDataURL('image/png');
},
// ─── Animation ─────────────────────────────────────
/**
* Get current animation frame transforms
* @param {Object} avatar - Avatar object (unused but kept for API consistency)
* @param {string} animation - Animation ID
* @param {number} frame - Frame number
* @returns {Object} Transform data for the frame
*/
animate: function(avatar, animation, frame) {
if (typeof AvatarAnimations === 'undefined') {
return {};
}
var anim = AvatarAnimations.getAnimation('FULL_BODY', animation);
if (!anim) return {};
var f = frame % anim.frameCount;
return anim.frames[f] || {};
},
/**
* Start an animation for an avatar
* @param {string} avatarId - Avatar/user ID
* @param {string} animation - Animation ID
* @param {Object} options - { loop, onComplete, mode }
*/
startAnimation: function(avatarId, animation, options) {
options = options || {};
animationState.set(avatarId, {
animation: animation,
startTime: performance.now(),
loop: options.loop !== false,
mode: options.mode || 'FULL_BODY',
onComplete: options.onComplete || null
});
},
/**
* Stop animation for an avatar
* @param {string} avatarId - Avatar/user ID
*/
stopAnimation: function(avatarId) {
var state = animationState.get(avatarId);
if (state && state.onComplete) {
state.onComplete();
}
animationState.delete(avatarId);
},
/**
* Get current animation state for an avatar
* @param {string} avatarId - Avatar/user ID
* @returns {Object|null} Current animation state or null
*/
getAnimationState: function(avatarId) {
var state = animationState.get(avatarId);
if (!state) return null;
if (typeof AvatarAnimations === 'undefined') {
return { animation: state.animation, frame: 0, complete: false };
}
var anim = AvatarAnimations.getAnimation(state.mode, state.animation);
if (!anim) return null;
var frameInfo = getAnimationFrame(anim, state.startTime, state.loop);
// Handle completion
if (frameInfo.complete && !state.loop) {
if (state.onComplete) {
state.onComplete();
}
animationState.delete(avatarId);
}
return {
animation: state.animation,
frame: frameInfo.frame,
complete: frameInfo.complete,
transforms: anim.frames[frameInfo.frame] || {}
};
},
/**
* Check if avatar is currently animating
* @param {string} avatarId - Avatar/user ID
* @returns {boolean} True if animating
*/
isAnimating: function(avatarId) {
return animationState.has(avatarId);
},
// ─── Accessories ───────────────────────────────────
/**
* Equip an accessory
* @param {Object} avatar - Avatar object
* @param {string} slot - 'eyewear', 'headwear', 'effect'
* @param {string} itemId - Accessory ID or null to unequip
* @returns {boolean} True if successfully equipped
*/
equipAccessory: function(avatar, slot, itemId) {
if (!avatar.equipped) {
avatar.equipped = { eyewear: null, headwear: null, effect: null };
}
// Allow unequipping (null/undefined)
if (!itemId) {
avatar.equipped[slot] = null;
return true;
}
// Verify user has earned this item
var earnedKey = slot === 'effect' ? 'effects' : slot;
var earned = avatar.earned && avatar.earned[earnedKey];
if (!earned || !earned.includes(itemId)) {
console.warn('AvatarSystem: Cannot equip unearned item', itemId);
return false;
}
avatar.equipped[slot] = itemId;
return true;
},
/**
* Unequip an accessory from a slot
* @param {Object} avatar - Avatar object
* @param {string} slot - 'eyewear', 'headwear', 'effect'
* @returns {boolean} True if successfully unequipped
*/
unequipAccessory: function(avatar, slot) {
return this.equipAccessory(avatar, slot, null);
},
/**
* Unlock/award an accessory to a user
* @param {Object} avatar - Avatar object
* @param {string} itemId - Accessory ID to unlock
* @returns {boolean} True if newly unlocked, false if already owned
*/
unlockAccessory: function(avatar, itemId) {
if (typeof AvatarAccessories === 'undefined') {
console.warn('AvatarSystem: AvatarAccessories not loaded');
return false;
}
var item = AvatarAccessories.getById(itemId);
if (!item) {
console.warn('AvatarSystem: Unknown accessory', itemId);
return false;
}
var category = item.category === 'effect' ? 'effects' : item.category;
// Initialize earned structure if needed
if (!avatar.earned) {
avatar.earned = { eyewear: [], headwear: [], effects: [] };
}
if (!avatar.earned[category]) {
avatar.earned[category] = [];
}
// Check if already owned
if (avatar.earned[category].includes(itemId)) {
return false;
}
avatar.earned[category].push(itemId);
return true;
},
/**
* Get all accessories a user has earned
* @param {Object} avatar - Avatar object
* @returns {Object} Object with eyewear, headwear, effects arrays of accessory objects
*/
getEarnedAccessories: function(avatar) {
var result = { eyewear: [], headwear: [], effects: [] };
if (!avatar.earned) return result;
if (typeof AvatarAccessories === 'undefined') {
return result;
}
if (avatar.earned.eyewear) {
result.eyewear = avatar.earned.eyewear
.map(function(id) { return AvatarAccessories.getById(id); })
.filter(function(item) { return item !== null; });
}
if (avatar.earned.headwear) {
result.headwear = avatar.earned.headwear
.map(function(id) { return AvatarAccessories.getById(id); })
.filter(function(item) { return item !== null; });
}
if (avatar.earned.effects) {
result.effects = avatar.earned.effects
.map(function(id) { return AvatarAccessories.getById(id); })
.filter(function(item) { return item !== null; });
}
return result;
},
/**
* Get currently equipped accessories
* @param {Object} avatar - Avatar object
* @returns {Object} Object with equipped accessory objects
*/
getEquippedAccessories: function(avatar) {
var result = { eyewear: null, headwear: null, effect: null };
if (!avatar.equipped) return result;
if (typeof AvatarAccessories === 'undefined') {
return result;
}
if (avatar.equipped.eyewear) {
result.eyewear = AvatarAccessories.getById(avatar.equipped.eyewear);
}
if (avatar.equipped.headwear) {
result.headwear = AvatarAccessories.getById(avatar.equipped.headwear);
}
if (avatar.equipped.effect) {
result.effect = AvatarAccessories.getById(avatar.equipped.effect);
}
return result;
},
/**
* Check what accessories should unlock at a level
* @param {Object} avatar - Avatar object
* @param {number} newLevel - New level reached
* @returns {Array} Array of newly unlocked accessory objects
*/
checkLevelUnlocks: function(avatar, newLevel) {
if (typeof AvatarAccessories === 'undefined') {
return [];
}
var newItems = AvatarAccessories.getNewlyUnlockedAtLevel(newLevel);
var unlocked = [];
var self = this;
newItems.forEach(function(item) {
if (self.unlockAccessory(avatar, item.id)) {
unlocked.push(item);
}
});
return unlocked;
},
/**
* Get the full accessory catalog
* @returns {Object} Full accessory catalog
*/
getAccessoryCatalog: function() {
if (typeof AvatarAccessories === 'undefined') {
return { eyewear: [], headwear: [], effects: [] };
}
return AvatarAccessories.getCatalog();
},
/**
* Check if avatar has a specific accessory
* @param {Object} avatar - Avatar object
* @param {string} itemId - Accessory ID
* @returns {boolean} True if owned
*/
hasAccessory: function(avatar, itemId) {
if (!avatar.earned) return false;
for (var category in avatar.earned) {
if (avatar.earned[category] && avatar.earned[category].includes(itemId)) {
return true;
}
}
return false;
},
// ─── Export for Games ──────────────────────────────
/**
* Prepare avatar for use in a game
* Returns a simplified object optimized for game rendering
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @returns {Object} Game-ready avatar object
*/
exportForGame: function(avatar, mode) {
var self = this;
var modeData = this.getModeSize(mode);
var cloned = cloneAvatar(avatar);
var animations = {};
if (typeof AvatarAnimations !== 'undefined') {
animations = AvatarAnimations.getAnimationsForMode(mode);
}
return {
mode: mode,
width: modeData.width,
height: modeData.height,
avatar: cloned,
animations: animations,
/**
* Render the avatar
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - X position
* @param {number} y - Y position
* @param {Object} options - Render options
*/
render: function(ctx, x, y, options) {
self.render(cloned, mode, ctx, x, y, options);
},
/**
* Get animation transforms
* @param {string} animation - Animation ID
* @param {number} frame - Frame number
* @returns {Object} Transform data
*/
animate: function(animation, frame) {
return self.animate(cloned, animation, frame);
},
/**
* Get animation info
* @param {string} animationId - Animation ID
* @returns {Object|null} Animation info
*/
getAnimationInfo: function(animationId) {
if (typeof AvatarAnimations === 'undefined') return null;
return AvatarAnimations.getAnimation(mode, animationId);
}
};
},
/**
* Export avatar as minimal data for network transmission
* @param {Object} avatar - Avatar object
* @returns {Object} Minimal avatar data
*/
exportMinimal: function(avatar) {
return {
base: avatar.base,
equipped: avatar.equipped,
level: avatar.level
};
},
/**
* Import avatar from minimal data
* @param {Object} minimalData - Minimal avatar data
* @param {string} userId - User ID
* @returns {Object} Full avatar object
*/
importMinimal: function(minimalData, userId) {
var avatar = createDefault();
avatar.userId = userId;
if (minimalData.base) {
avatar.base = deepMergeBase(avatar.base, minimalData.base);
}
if (minimalData.equipped) {
avatar.equipped = Object.assign({}, avatar.equipped, minimalData.equipped);
}
if (minimalData.level !== undefined) {
avatar.level = minimalData.level;
}
return avatar;
},
// ─── Guest Avatars ─────────────────────────────────
/**
* Get a random guest avatar (for non-logged-in users)
* @returns {Object} Guest avatar object
*/
getGuestAvatar: function() {
var guest;
if (typeof AvatarData !== 'undefined' && AvatarData.getRandomGuestAvatar) {
guest = AvatarData.getRandomGuestAvatar();
} else {
guest = { base: createDefault().base };
}
var avatar = createDefault();
avatar.userId = 'guest_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
avatar.base = Object.assign({}, avatar.base, guest.base);
avatar.isGuest = true;
return avatar;
},
/**
* Check if avatar is a guest
* @param {Object} avatar - Avatar object
* @returns {boolean} True if guest avatar
*/
isGuest: function(avatar) {
return avatar.isGuest === true || (avatar.userId && avatar.userId.startsWith('guest_'));
},
/**
* Convert guest avatar to registered user avatar
* @param {Object} guestAvatar - Guest avatar
* @param {string} userId - New user ID
* @returns {Object} Converted avatar
*/
convertGuestToUser: function(guestAvatar, userId) {
var avatar = cloneAvatar(guestAvatar);
avatar.userId = userId;
delete avatar.isGuest;
return avatar;
},
// ─── Level & XP ────────────────────────────────────
/**
* Add XP to avatar and check for level ups
* @param {Object} avatar - Avatar object
* @param {number} xpAmount - XP to add
* @returns {Object} Result with newLevel, leveledUp, unlockedItems
*/
addXP: function(avatar, xpAmount) {
var oldLevel = avatar.level || 1;
avatar.xp = (avatar.xp || 0) + xpAmount;
// Calculate new level (100 XP per level, increasing by 50 each level)
var xpNeeded = 0;
var level = 0;
var remainingXP = avatar.xp;
while (remainingXP >= 0) {
level++;
var xpForLevel = 100 + (level - 1) * 50;
if (remainingXP < xpForLevel) break;
remainingXP -= xpForLevel;
}
avatar.level = level;
var result = {
newLevel: level,
leveledUp: level > oldLevel,
unlockedItems: []
};
// Check for unlocks at each new level
if (result.leveledUp) {
for (var l = oldLevel + 1; l <= level; l++) {
var unlocked = this.checkLevelUnlocks(avatar, l);
result.unlockedItems = result.unlockedItems.concat(unlocked);
}
}
return result;
},
/**
* Get XP needed for next level
* @param {Object} avatar - Avatar object
* @returns {Object} XP info
*/
getXPInfo: function(avatar) {
var level = avatar.level || 1;
var totalXP = avatar.xp || 0;
// Calculate XP used for previous levels
var xpUsed = 0;
for (var l = 1; l < level; l++) {
xpUsed += 100 + (l - 1) * 50;
}
var currentLevelXP = totalXP - xpUsed;
var xpForCurrentLevel = 100 + (level - 1) * 50;
return {
level: level,
totalXP: totalXP,
currentLevelXP: currentLevelXP,
xpNeededForLevel: xpForCurrentLevel,
progress: currentLevelXP / xpForCurrentLevel
};
},
// ─── Utilities ─────────────────────────────────────
/**
* Create a sprite sheet for an avatar
* Useful for game performance
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @param {Array} animations - Animation IDs (optional, defaults to all)
* @returns {Object} Sprite sheet data
*/
createSpriteSheet: function(avatar, mode, animations) {
var modeData = this.getModeSize(mode);
var self = this;
// Get animation list
if (!animations && typeof AvatarAnimations !== 'undefined') {
var allAnims = AvatarAnimations.getAnimationsForMode(mode);
animations = Object.keys(allAnims);
}
animations = animations || [];
// Calculate sprite sheet dimensions
var maxFrames = 0;
var animData = [];
animations.forEach(function(animId) {
if (typeof AvatarAnimations !== 'undefined') {
var anim = AvatarAnimations.getAnimation(mode, animId);
if (anim) {
maxFrames = Math.max(maxFrames, anim.frameCount);
animData.push({ id: animId, frames: anim.frameCount, duration: anim.frameDuration });
}
}
});
// Handle case with no animations
if (animData.length === 0) {
animData.push({ id: 'idle', frames: 1, duration: 100 });
maxFrames = 1;
}
// Create canvas
var canvas = document.createElement('canvas');
canvas.width = modeData.width * maxFrames;
canvas.height = modeData.height * animData.length;
var ctx = canvas.getContext('2d');
// Render each animation frame
animData.forEach(function(anim, row) {
for (var f = 0; f < anim.frames; f++) {
var x = modeData.width * f + modeData.width / 2;
var y = modeData.height * row + modeData.height / 2;
self.render(avatar, mode, ctx, x, y, { animation: anim.id, frame: f });
}
});
return {
canvas: canvas,
image: canvas,
animations: animData,
frameWidth: modeData.width,
frameHeight: modeData.height,
/**
* Get frame coordinates for an animation frame
* @param {string} animId - Animation ID
* @param {number} frame - Frame number
* @returns {Object} Source rectangle {x, y, width, height}
*/
getFrameRect: function(animId, frame) {
var row = 0;
for (var i = 0; i < animData.length; i++) {
if (animData[i].id === animId) {
row = i;
break;
}
}
var f = frame % (animData[row] ? animData[row].frames : 1);
return {
x: f * modeData.width,
y: row * modeData.height,
width: modeData.width,
height: modeData.height
};
}
};
},
/**
* Get rendering mode dimensions
* @param {string} mode - Rendering mode
* @returns {Object} Mode dimensions {width, height}
*/
getModeSize: function(mode) {
if (typeof AvatarAnimations !== 'undefined' && AvatarAnimations.getMode) {
return AvatarAnimations.getMode(mode);
}
// Fallback defaults
var defaults = {
'HEAD_ONLY': { width: 64, height: 64 },
'HEAD_AND_SHOULDERS': { width: 96, height: 96 },
'UPPER_BODY': { width: 128, height: 128 },
'FULL_BODY': { width: 128, height: 192 }
};
return defaults[mode] || defaults['HEAD_ONLY'];
},
/**
* Validate avatar data structure
* @param {Object} avatar - Avatar to validate
* @returns {boolean} True if valid
*/
validate: validateAvatar,
/**
* Create default avatar
* @returns {Object} Default avatar object
*/
createDefault: createDefault,
/**
* Clone an avatar object
* @param {Object} avatar - Avatar to clone
* @returns {Object} Cloned avatar
*/
clone: cloneAvatar,
/**
* Update avatar base properties
* @param {Object} avatar - Avatar to update
* @param {Object} updates - Properties to update
* @returns {Object} Updated avatar
*/
updateBase: function(avatar, updates) {
avatar.base = deepMergeBase(avatar.base, updates);
return avatar;
},
/**
* Get available customization options
* @returns {Object} Available options from AvatarData
*/
getCustomizationOptions: function() {
if (typeof AvatarData === 'undefined') {
return {};
}
return {
faceShapes: AvatarData.FACE_SHAPES || [],
skinTones: AvatarData.SKIN_TONES || [],
eyeShapes: AvatarData.EYE_SHAPES || [],
eyeColors: AvatarData.EYE_COLORS || [],
eyebrowShapes: AvatarData.EYEBROW_SHAPES || [],
noseShapes: AvatarData.NOSE_SHAPES || [],
mouthShapes: AvatarData.MOUTH_SHAPES || [],
earShapes: AvatarData.EAR_SHAPES || [],
hairStyles: AvatarData.HAIR_STYLES || [],
hairColors: AvatarData.HAIR_COLORS || [],
facialHair: AvatarData.FACIAL_HAIR || [],
bodyTypes: AvatarData.BODY_TYPES || [],
clothingStyles: AvatarData.CLOTHING_STYLES || []
};
},
/**
* Compare two avatars for equality
* @param {Object} a - First avatar
* @param {Object} b - Second avatar
* @returns {boolean} True if equivalent
*/
equals: function(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
},
/**
* Get version info
* @returns {Object} Version information
*/
getVersion: function() {
return {
version: '1.0.0',
dependencies: ['AvatarData', 'AvatarAnimations', 'AvatarRenderer', 'AvatarAccessories', 'AccessoryRenderer']
};
}
};
})();