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,762 @@
/**
* Avatar Accessories System
* Defines all unlockable accessories for the avatar customization system
*/
(function() {
'use strict';
// =============================================================================
// RARITY COLORS
// =============================================================================
var RARITY_COLORS = {
common: '#9d9d9d',
uncommon: '#1eff00',
rare: '#0070dd',
epic: '#a335ee',
legendary: '#ff8000',
mythic: '#e6cc80'
};
// =============================================================================
// EYEWEAR (15 items)
// =============================================================================
var EYEWEAR = [
{
id: 'sunglasses-classic',
name: 'Classic Sunglasses',
category: 'eyewear',
unlockLevel: 5,
rarity: 'common',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.0,
hasEffect: false,
description: 'Timeless shades for any occasion'
},
{
id: 'aviator',
name: 'Aviator Glasses',
category: 'eyewear',
unlockLevel: 10,
rarity: 'common',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -1 },
scale: 1.0,
hasEffect: false,
description: 'Top Gun approved eyewear'
},
{
id: 'nerd-glasses',
name: 'Nerd Glasses',
category: 'eyewear',
unlockLevel: 15,
rarity: 'common',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: 0 },
scale: 1.0,
hasEffect: false,
description: 'Intelligence +100 (cosmetic only)'
},
{
id: '3d-glasses',
name: '3D Glasses',
category: 'eyewear',
unlockLevel: 20,
rarity: 'uncommon',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -1 },
scale: 1.0,
hasEffect: false,
description: 'See the world in a whole new dimension'
},
{
id: 'goggles',
name: 'Goggles',
category: 'eyewear',
unlockLevel: 25,
rarity: 'uncommon',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -3 },
scale: 1.1,
hasEffect: false,
description: 'Ready for adventure or science experiments'
},
{
id: 'monocle',
name: 'Monocle',
category: 'eyewear',
unlockLevel: 30,
rarity: 'uncommon',
zIndex: 10,
anchor: 'eyes',
offset: { x: 8, y: 0 },
scale: 0.8,
hasEffect: false,
description: 'Distinguished and sophisticated'
},
{
id: 'star-glasses',
name: 'Star Glasses',
category: 'eyewear',
unlockLevel: 35,
rarity: 'rare',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: false,
description: 'You are a star!'
},
{
id: 'heart-glasses',
name: 'Heart Glasses',
category: 'eyewear',
unlockLevel: 40,
rarity: 'rare',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: false,
description: 'Spread the love wherever you go'
},
{
id: 'sport-visor',
name: 'Sport Visor',
category: 'eyewear',
unlockLevel: 45,
rarity: 'rare',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -5 },
scale: 1.2,
hasEffect: false,
description: 'Professional gamer gear'
},
{
id: 'vr-headset',
name: 'VR Headset',
category: 'eyewear',
unlockLevel: 50,
rarity: 'epic',
zIndex: 11,
anchor: 'eyes',
offset: { x: 0, y: -4 },
scale: 1.3,
hasEffect: false,
description: 'Immerse yourself in virtual worlds'
},
{
id: 'cyber-visor',
name: 'Cyber Visor',
category: 'eyewear',
unlockLevel: 55,
rarity: 'epic',
zIndex: 11,
anchor: 'eyes',
offset: { x: 0, y: -3 },
scale: 1.2,
hasEffect: false,
description: 'Sleek futuristic eye protection'
},
{
id: 'x-ray-specs',
name: 'X-Ray Specs',
category: 'eyewear',
unlockLevel: 60,
rarity: 'epic',
zIndex: 11,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: true,
description: 'See through walls... probably'
},
{
id: 'laser-eyes',
name: 'Laser Eyes',
category: 'eyewear',
unlockLevel: 70,
rarity: 'legendary',
zIndex: 12,
anchor: 'eyes',
offset: { x: 0, y: 0 },
scale: 1.0,
hasEffect: true,
description: 'Pew pew! Lasers shoot from your eyes'
},
{
id: 'diamond-glasses',
name: 'Diamond Glasses',
category: 'eyewear',
unlockLevel: 80,
rarity: 'legendary',
zIndex: 12,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: false,
description: 'Pure crystallized luxury'
},
{
id: 'cosmic-lenses',
name: 'Cosmic Lenses',
category: 'eyewear',
unlockLevel: 90,
rarity: 'mythic',
zIndex: 13,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.2,
hasEffect: true,
description: 'Gaze upon the infinite universe'
}
];
// =============================================================================
// HEADWEAR (20 items)
// =============================================================================
var HEADWEAR = [
{
id: 'baseball-cap',
name: 'Baseball Cap',
category: 'headwear',
unlockLevel: 3,
rarity: 'common',
zIndex: 8,
anchor: 'top',
offset: { x: 0, y: -15 },
scale: 1.0,
hasEffect: false,
description: 'A classic casual cap'
},
{
id: 'backwards-cap',
name: 'Backwards Cap',
category: 'headwear',
unlockLevel: 8,
rarity: 'common',
zIndex: 8,
anchor: 'top',
offset: { x: 0, y: -15 },
scale: 1.0,
hasEffect: false,
description: 'Cool kids wear it backwards'
},
{
id: 'beanie',
name: 'Beanie',
category: 'headwear',
unlockLevel: 12,
rarity: 'common',
zIndex: 8,
anchor: 'top',
offset: { x: 0, y: -12 },
scale: 1.0,
hasEffect: false,
description: 'Cozy knitted headwear'
},
{
id: 'party-hat',
name: 'Party Hat',
category: 'headwear',
unlockLevel: 18,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -20 },
scale: 1.0,
hasEffect: false,
description: 'Every day is a celebration!'
},
{
id: 'chef-hat',
name: 'Chef Hat',
category: 'headwear',
unlockLevel: 22,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -25 },
scale: 1.1,
hasEffect: false,
description: 'Master of the kitchen'
},
{
id: 'crown-bronze',
name: 'Bronze Crown',
category: 'headwear',
unlockLevel: 25,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.0,
hasEffect: false,
description: 'A humble crown for rising royalty'
},
{
id: 'hard-hat',
name: 'Hard Hat',
category: 'headwear',
unlockLevel: 26,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -14 },
scale: 1.0,
hasEffect: false,
description: 'Safety first on the job site'
},
{
id: 'wizard-hat',
name: 'Wizard Hat',
category: 'headwear',
unlockLevel: 30,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -30 },
scale: 1.2,
hasEffect: false,
description: 'Channel your inner sorcerer'
},
{
id: 'pirate-hat',
name: 'Pirate Hat',
category: 'headwear',
unlockLevel: 35,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.1,
hasEffect: false,
description: 'Arrr! Set sail for adventure'
},
{
id: 'santa-hat',
name: 'Santa Hat',
category: 'headwear',
unlockLevel: 38,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 3, y: -16 },
scale: 1.0,
hasEffect: false,
description: 'Ho ho ho! Spread holiday cheer'
},
{
id: 'crown-silver',
name: 'Silver Crown',
category: 'headwear',
unlockLevel: 40,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.0,
hasEffect: false,
description: 'A noble crown for dedicated players'
},
{
id: 'graduation-cap',
name: 'Graduation Cap',
category: 'headwear',
unlockLevel: 42,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -16 },
scale: 1.0,
hasEffect: false,
description: 'Scholar of the gaming arts'
},
{
id: 'bunny-ears',
name: 'Bunny Ears',
category: 'headwear',
unlockLevel: 45,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -22 },
scale: 1.1,
hasEffect: false,
description: 'Hop into the fun!'
},
{
id: 'devil-horns',
name: 'Devil Horns',
category: 'headwear',
unlockLevel: 48,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -14 },
scale: 1.0,
hasEffect: false,
description: 'A little mischief never hurt anyone'
},
{
id: 'halo',
name: 'Halo',
category: 'headwear',
unlockLevel: 52,
rarity: 'epic',
zIndex: 7,
anchor: 'top',
offset: { x: 0, y: -25 },
scale: 1.0,
hasEffect: true,
description: 'An angelic golden ring of light'
},
{
id: 'top-hat',
name: 'Top Hat',
category: 'headwear',
unlockLevel: 56,
rarity: 'epic',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -28 },
scale: 1.1,
hasEffect: false,
description: 'Dapper and distinguished'
},
{
id: 'crown-gold',
name: 'Gold Crown',
category: 'headwear',
unlockLevel: 60,
rarity: 'epic',
zIndex: 10,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.1,
hasEffect: false,
description: 'True royalty in golden splendor'
},
{
id: 'viking-helmet',
name: 'Viking Helmet',
category: 'headwear',
unlockLevel: 62,
rarity: 'epic',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -16 },
scale: 1.2,
hasEffect: false,
description: 'Conquer games like a Norse warrior'
},
{
id: 'crown-diamond',
name: 'Diamond Crown',
category: 'headwear',
unlockLevel: 80,
rarity: 'legendary',
zIndex: 10,
anchor: 'top',
offset: { x: 0, y: -20 },
scale: 1.2,
hasEffect: true,
description: 'A magnificent crown of pure diamonds'
},
{
id: 'crown-cosmic',
name: 'Cosmic Crown',
category: 'headwear',
unlockLevel: 100,
rarity: 'mythic',
zIndex: 11,
anchor: 'top',
offset: { x: 0, y: -22 },
scale: 1.3,
hasEffect: true,
description: 'The ultimate crown, forged from stardust'
}
];
// =============================================================================
// EFFECTS/AURAS (10 items)
// =============================================================================
var EFFECTS = [
{
id: 'sparkle-trail',
name: 'Sparkle Trail',
category: 'effect',
unlockLevel: 15,
rarity: 'uncommon',
zIndex: 1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.0,
hasEffect: true,
effectType: 'particles',
color: '#FFD700',
description: 'Leave a trail of golden sparkles'
},
{
id: 'fire-aura',
name: 'Fire Aura',
category: 'effect',
unlockLevel: 25,
rarity: 'rare',
zIndex: 0,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.2,
hasEffect: true,
effectType: 'glow',
color: '#FF4500',
description: 'Surrounded by fierce flames'
},
{
id: 'lightning-crackle',
name: 'Lightning Crackle',
category: 'effect',
unlockLevel: 35,
rarity: 'rare',
zIndex: 2,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.1,
hasEffect: true,
effectType: 'electric',
color: '#00BFFF',
description: 'Electricity crackles around you'
},
{
id: 'rainbow-glow',
name: 'Rainbow Glow',
category: 'effect',
unlockLevel: 45,
rarity: 'epic',
zIndex: 0,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.3,
hasEffect: true,
effectType: 'glow',
color: 'rainbow',
description: 'Radiate all colors of the spectrum'
},
{
id: 'star-particles',
name: 'Star Particles',
category: 'effect',
unlockLevel: 55,
rarity: 'epic',
zIndex: 2,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.2,
hasEffect: true,
effectType: 'particles',
color: '#FFFF00',
description: 'Tiny stars orbit around you'
},
{
id: 'ice-crystals',
name: 'Ice Crystals',
category: 'effect',
unlockLevel: 65,
rarity: 'legendary',
zIndex: 2,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.2,
hasEffect: true,
effectType: 'particles',
color: '#87CEEB',
description: 'Frozen crystals shimmer around you'
},
{
id: 'shadow-effect',
name: 'Shadow Effect',
category: 'effect',
unlockLevel: 70,
rarity: 'legendary',
zIndex: -1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.4,
hasEffect: true,
effectType: 'shadow',
color: '#2F2F4F',
description: 'Darkness follows your every move'
},
{
id: 'golden-glow',
name: 'Golden Glow',
category: 'effect',
unlockLevel: 75,
rarity: 'legendary',
zIndex: 0,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.3,
hasEffect: true,
effectType: 'glow',
color: '#FFD700',
description: 'Bathe in pure golden light'
},
{
id: 'void-aura',
name: 'Void Aura',
category: 'effect',
unlockLevel: 85,
rarity: 'mythic',
zIndex: -1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.5,
hasEffect: true,
effectType: 'void',
color: '#4B0082',
description: 'The void itself surrounds you'
},
{
id: 'cosmic-trail',
name: 'Cosmic Trail',
category: 'effect',
unlockLevel: 95,
rarity: 'mythic',
zIndex: 1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.4,
hasEffect: true,
effectType: 'cosmic',
color: 'cosmic',
description: 'Trail galaxies and nebulae in your wake'
}
];
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Get all accessories combined into a single array
*/
function getAll() {
return [].concat(EYEWEAR, HEADWEAR, EFFECTS);
}
/**
* Get accessories filtered by category
* @param {string} category - 'eyewear', 'headwear', or 'effect'
*/
function getByCategory(category) {
switch (category) {
case 'eyewear':
return EYEWEAR.slice();
case 'headwear':
return HEADWEAR.slice();
case 'effect':
return EFFECTS.slice();
default:
return [];
}
}
/**
* Find an accessory by its unique ID
* @param {string} id - The accessory ID
*/
function getById(id) {
var all = getAll();
for (var i = 0; i < all.length; i++) {
if (all[i].id === id) {
return all[i];
}
}
return null;
}
/**
* Get all accessories of a specific rarity
* @param {string} rarity - 'common', 'uncommon', 'rare', 'epic', 'legendary', or 'mythic'
*/
function getByRarity(rarity) {
return getAll().filter(function(item) {
return item.rarity === rarity;
});
}
/**
* Get all accessories unlockable at or before the given level
* @param {number} level - The player's current level
*/
function getUnlockedAtLevel(level) {
return getAll().filter(function(item) {
return item.unlockLevel <= level;
});
}
/**
* Get accessories that unlock exactly at the specified level
* @param {number} level - The level to check
*/
function getNewlyUnlockedAtLevel(level) {
return getAll().filter(function(item) {
return item.unlockLevel === level;
});
}
/**
* Get the color associated with a rarity
* @param {string} rarity - The rarity level
*/
function getRarityColor(rarity) {
return RARITY_COLORS[rarity] || RARITY_COLORS.common;
}
/**
* Get an organized catalog for UI display
* Returns accessories grouped by category and sorted by unlock level
*/
function getCatalog() {
var sortByLevel = function(a, b) {
return a.unlockLevel - b.unlockLevel;
};
return {
eyewear: EYEWEAR.slice().sort(sortByLevel),
headwear: HEADWEAR.slice().sort(sortByLevel),
effects: EFFECTS.slice().sort(sortByLevel),
totalCount: EYEWEAR.length + HEADWEAR.length + EFFECTS.length,
rarityColors: Object.assign({}, RARITY_COLORS)
};
}
// =============================================================================
// PUBLIC API
// =============================================================================
window.AvatarAccessories = {
// Data arrays
EYEWEAR: EYEWEAR,
HEADWEAR: HEADWEAR,
EFFECTS: EFFECTS,
RARITY_COLORS: RARITY_COLORS,
// Methods
getAll: getAll,
getByCategory: getByCategory,
getById: getById,
getByRarity: getByRarity,
getUnlockedAtLevel: getUnlockedAtLevel,
getNewlyUnlockedAtLevel: getNewlyUnlockedAtLevel,
getRarityColor: getRarityColor,
getCatalog: getCatalog
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+473
View File
@@ -0,0 +1,473 @@
(function() {
'use strict';
// Face shapes (8 options)
var FACE_SHAPES = [
{ id: 'round', name: 'Round', widthRatio: 1.0, heightRatio: 1.0, jawCurve: 0.9 },
{ id: 'oval', name: 'Oval', widthRatio: 0.85, heightRatio: 1.1, jawCurve: 0.7 },
{ id: 'square', name: 'Square', widthRatio: 1.0, heightRatio: 0.95, jawCurve: 0.3 },
{ id: 'heart', name: 'Heart', widthRatio: 0.9, heightRatio: 1.05, jawCurve: 0.8, chinPoint: 0.6 },
{ id: 'long', name: 'Long', widthRatio: 0.8, heightRatio: 1.2, jawCurve: 0.5 },
{ id: 'wide', name: 'Wide', widthRatio: 1.15, heightRatio: 0.9, jawCurve: 0.6 },
{ id: 'angular', name: 'Angular', widthRatio: 0.95, heightRatio: 1.0, jawCurve: 0.2, cheekbones: 0.7 },
{ id: 'soft', name: 'Soft', widthRatio: 1.05, heightRatio: 1.0, jawCurve: 0.95 }
];
// Skin tones (20 options - full diversity)
var SKIN_TONES = [
{ id: 'fair1', hex: '#FFECD1', name: 'Porcelain' },
{ id: 'fair2', hex: '#FFE4C4', name: 'Ivory' },
{ id: 'fair3', hex: '#FFDAB9', name: 'Peach' },
{ id: 'fair4', hex: '#FFD5B5', name: 'Cream' },
{ id: 'light1', hex: '#F5D0A9', name: 'Light Beige' },
{ id: 'light2', hex: '#E8C49A', name: 'Warm Beige' },
{ id: 'light3', hex: '#DEB887', name: 'Sand' },
{ id: 'medium1', hex: '#D4A373', name: 'Golden' },
{ id: 'medium2', hex: '#C9A06A', name: 'Honey' },
{ id: 'medium3', hex: '#BC8F5F', name: 'Caramel' },
{ id: 'medium4', hex: '#B07D4B', name: 'Toffee' },
{ id: 'tan1', hex: '#A67B5B', name: 'Tan' },
{ id: 'tan2', hex: '#996B4D', name: 'Bronze' },
{ id: 'tan3', hex: '#8B6D4C', name: 'Cinnamon' },
{ id: 'brown1', hex: '#7D5A3C', name: 'Chestnut' },
{ id: 'brown2', hex: '#6F4E37', name: 'Cocoa' },
{ id: 'brown3', hex: '#5D4532', name: 'Espresso' },
{ id: 'dark1', hex: '#4E3B2D', name: 'Mocha' },
{ id: 'dark2', hex: '#3D2B1F', name: 'Ebony' },
{ id: 'dark3', hex: '#2C1F15', name: 'Onyx' }
];
// Eye shapes (12 options)
var EYE_SHAPES = [
{ id: 'round', name: 'Round', width: 0.15, height: 0.12, curve: 1.0 },
{ id: 'almond', name: 'Almond', width: 0.18, height: 0.08, curve: 0.6 },
{ id: 'oval', name: 'Oval', width: 0.16, height: 0.10, curve: 0.8 },
{ id: 'narrow', name: 'Narrow', width: 0.20, height: 0.06, curve: 0.4 },
{ id: 'wide', name: 'Wide', width: 0.19, height: 0.11, curve: 0.7 },
{ id: 'droopy', name: 'Droopy', width: 0.17, height: 0.09, curve: 0.5, droop: 0.3 },
{ id: 'upturned', name: 'Upturned', width: 0.17, height: 0.09, curve: 0.6, upturn: 0.3 },
{ id: 'cat', name: 'Cat', width: 0.18, height: 0.07, curve: 0.4, upturn: 0.5 },
{ id: 'sleepy', name: 'Sleepy', width: 0.16, height: 0.07, curve: 0.5, lidDroop: 0.4 },
{ id: 'surprised', name: 'Surprised', width: 0.16, height: 0.14, curve: 1.0 },
{ id: 'angry', name: 'Angry', width: 0.17, height: 0.08, curve: 0.5, innerAngle: -0.2 },
{ id: 'happy', name: 'Happy', width: 0.17, height: 0.08, curve: 0.7, squint: 0.3 }
];
// Eye colors (16 options)
var EYE_COLORS = [
{ id: 'brown', hex: '#634e34', name: 'Brown' },
{ id: 'darkBrown', hex: '#3d2314', name: 'Dark Brown' },
{ id: 'hazel', hex: '#8b7355', name: 'Hazel' },
{ id: 'green', hex: '#3d7a3d', name: 'Green' },
{ id: 'blue', hex: '#4682b4', name: 'Blue' },
{ id: 'lightBlue', hex: '#87ceeb', name: 'Light Blue' },
{ id: 'gray', hex: '#708090', name: 'Gray' },
{ id: 'amber', hex: '#b8860b', name: 'Amber' },
{ id: 'violet', hex: '#8b008b', name: 'Violet' },
{ id: 'emerald', hex: '#50c878', name: 'Emerald' },
{ id: 'iceBlue', hex: '#a5d8e6', name: 'Ice Blue' },
{ id: 'gold', hex: '#daa520', name: 'Gold' },
{ id: 'red', hex: '#8b0000', name: 'Red' },
{ id: 'black', hex: '#1a1a1a', name: 'Black' },
{ id: 'honey', hex: '#c9a06a', name: 'Honey' },
{ id: 'turquoise', hex: '#40e0d0', name: 'Turquoise' }
];
// Eyebrow shapes (10 options)
var EYEBROW_SHAPES = [
{ id: 'natural', name: 'Natural', thickness: 0.5, arch: 0.3 },
{ id: 'arched', name: 'Arched', thickness: 0.5, arch: 0.6 },
{ id: 'straight', name: 'Straight', thickness: 0.5, arch: 0.0 },
{ id: 'thick', name: 'Thick', thickness: 0.8, arch: 0.3 },
{ id: 'thin', name: 'Thin', thickness: 0.25, arch: 0.3 },
{ id: 'angry', name: 'Angry', thickness: 0.5, arch: 0.2, innerAngle: -0.4 },
{ id: 'worried', name: 'Worried', thickness: 0.5, arch: 0.4, innerAngle: 0.3 },
{ id: 'bushy', name: 'Bushy', thickness: 0.9, arch: 0.2, wild: true },
{ id: 'sleek', name: 'Sleek', thickness: 0.3, arch: 0.5 },
{ id: 'curved', name: 'Curved', thickness: 0.5, arch: 0.7 }
];
// Nose shapes (8 options)
var NOSE_SHAPES = [
{ id: 'button', name: 'Button', width: 0.12, length: 0.15 },
{ id: 'pointed', name: 'Pointed', width: 0.10, length: 0.22 },
{ id: 'roman', name: 'Roman', width: 0.14, length: 0.25, bridge: 0.6 },
{ id: 'snub', name: 'Snub', width: 0.13, length: 0.12, upturn: 0.4 },
{ id: 'wide', name: 'Wide', width: 0.18, length: 0.18 },
{ id: 'narrow', name: 'Narrow', width: 0.08, length: 0.20 },
{ id: 'aquiline', name: 'Aquiline', width: 0.12, length: 0.24, bridge: 0.8 },
{ id: 'flat', name: 'Flat', width: 0.16, length: 0.10 }
];
// Mouth shapes (10 options)
var MOUTH_SHAPES = [
{ id: 'smile', name: 'Smile', width: 0.25, curve: 0.3 },
{ id: 'neutral', name: 'Neutral', width: 0.22, curve: 0 },
{ id: 'grin', name: 'Grin', width: 0.30, curve: 0.5, showTeeth: true },
{ id: 'smirk', name: 'Smirk', width: 0.22, curve: 0.2, asymmetric: true },
{ id: 'pout', name: 'Pout', width: 0.20, curve: -0.2, lipFullness: 0.8 },
{ id: 'open', name: 'Open', width: 0.24, curve: 0.1, openness: 0.3 },
{ id: 'small', name: 'Small', width: 0.16, curve: 0.1 },
{ id: 'wide', name: 'Wide', width: 0.32, curve: 0.1 },
{ id: 'thin', name: 'Thin', width: 0.24, curve: 0, lipFullness: 0.3 },
{ id: 'full', name: 'Full', width: 0.26, curve: 0.15, lipFullness: 0.9 }
];
// Ear shapes (6 options)
var EAR_SHAPES = [
{ id: 'normal', name: 'Normal', size: 1.0, angle: 0 },
{ id: 'small', name: 'Small', size: 0.7, angle: 0 },
{ id: 'large', name: 'Large', size: 1.3, angle: 0 },
{ id: 'pointed', name: 'Pointed', size: 1.0, angle: 0, pointed: true },
{ id: 'round', name: 'Round', size: 1.0, angle: 0, roundness: 0.9 },
{ id: 'flat', name: 'Flat', size: 0.9, angle: 15 }
];
// Hair styles (30 options)
var HAIR_STYLES = [
{ id: 'short-messy', name: 'Short Messy', length: 'short', coverage: 0.3 },
{ id: 'short-neat', name: 'Short Neat', length: 'short', coverage: 0.25 },
{ id: 'short-curly', name: 'Short Curly', length: 'short', coverage: 0.35 },
{ id: 'medium-straight', name: 'Medium Straight', length: 'medium', coverage: 0.5 },
{ id: 'medium-wavy', name: 'Medium Wavy', length: 'medium', coverage: 0.55 },
{ id: 'long-straight', name: 'Long Straight', length: 'long', coverage: 0.7 },
{ id: 'long-wavy', name: 'Long Wavy', length: 'long', coverage: 0.75 },
{ id: 'ponytail', name: 'Ponytail', length: 'medium', coverage: 0.4 },
{ id: 'pigtails', name: 'Pigtails', length: 'medium', coverage: 0.45 },
{ id: 'bun', name: 'Bun', length: 'long', coverage: 0.35 },
{ id: 'mohawk', name: 'Mohawk', length: 'short', coverage: 0.2 },
{ id: 'afro', name: 'Afro', length: 'medium', coverage: 0.8 },
{ id: 'buzz', name: 'Buzz Cut', length: 'short', coverage: 0.15 },
{ id: 'bald', name: 'Bald', length: 'none', coverage: 0 },
{ id: 'sidepart', name: 'Side Part', length: 'short', coverage: 0.3 },
{ id: 'spiky', name: 'Spiky', length: 'short', coverage: 0.35, spikes: true },
{ id: 'slicked', name: 'Slicked Back', length: 'short', coverage: 0.25 },
{ id: 'bob', name: 'Bob', length: 'medium', coverage: 0.5 },
{ id: 'pixie', name: 'Pixie', length: 'short', coverage: 0.35 },
{ id: 'braids', name: 'Braids', length: 'long', coverage: 0.6 },
{ id: 'dreads', name: 'Dreads', length: 'long', coverage: 0.7 },
{ id: 'undercut', name: 'Undercut', length: 'short', coverage: 0.25, sides: 'shaved' },
{ id: 'fauxhawk', name: 'Faux Hawk', length: 'short', coverage: 0.3 },
{ id: 'mullet', name: 'Mullet', length: 'medium', coverage: 0.45, backLength: 'long' },
{ id: 'bowl', name: 'Bowl Cut', length: 'medium', coverage: 0.55 },
{ id: 'combover', name: 'Comb Over', length: 'short', coverage: 0.2 },
{ id: 'bangs', name: 'Bangs', length: 'medium', coverage: 0.5, bangs: true },
{ id: 'sidesweep', name: 'Side Sweep', length: 'medium', coverage: 0.45 },
{ id: 'topknot', name: 'Top Knot', length: 'long', coverage: 0.3 },
{ id: 'cornrows', name: 'Cornrows', length: 'medium', coverage: 0.4 }
];
// Hair colors (20 options)
var HAIR_COLORS = [
{ id: 'black', hex: '#1a1a1a', name: 'Black' },
{ id: 'darkBrown', hex: '#2c1810', name: 'Dark Brown' },
{ id: 'brown', hex: '#4a3728', name: 'Brown' },
{ id: 'lightBrown', hex: '#7a5a3a', name: 'Light Brown' },
{ id: 'auburn', hex: '#8b4513', name: 'Auburn' },
{ id: 'ginger', hex: '#b5651d', name: 'Ginger' },
{ id: 'strawberry', hex: '#c67171', name: 'Strawberry' },
{ id: 'blonde', hex: '#d4b896', name: 'Blonde' },
{ id: 'platinum', hex: '#e8e4c9', name: 'Platinum' },
{ id: 'white', hex: '#f0f0f0', name: 'White' },
{ id: 'gray', hex: '#808080', name: 'Gray' },
{ id: 'red', hex: '#cc0000', name: 'Red' },
{ id: 'blue', hex: '#0066cc', name: 'Blue' },
{ id: 'purple', hex: '#7b2d8e', name: 'Purple' },
{ id: 'green', hex: '#228b22', name: 'Green' },
{ id: 'pink', hex: '#ff69b4', name: 'Pink' },
{ id: 'teal', hex: '#008b8b', name: 'Teal' },
{ id: 'orange', hex: '#ff6600', name: 'Orange' },
{ id: 'silver', hex: '#c0c0c0', name: 'Silver' },
{ id: 'rainbow', hex: 'linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff)', name: 'Rainbow', isGradient: true }
];
// Facial hair (10 options)
var FACIAL_HAIR = [
{ id: 'none', name: 'None' },
{ id: 'stubble', name: 'Stubble', density: 0.3, length: 0.1 },
{ id: 'goatee', name: 'Goatee', coverage: 'chin', length: 0.4 },
{ id: 'mustache', name: 'Mustache', coverage: 'upper-lip', length: 0.3, style: 'classic' },
{ id: 'fullBeard', name: 'Full Beard', coverage: 'full', length: 0.7 },
{ id: 'shortBeard', name: 'Short Beard', coverage: 'full', length: 0.3 },
{ id: 'soulPatch', name: 'Soul Patch', coverage: 'chin-center', length: 0.3 },
{ id: 'mutton', name: 'Mutton Chops', coverage: 'sides', length: 0.5 },
{ id: 'handlebar', name: 'Handlebar', coverage: 'upper-lip', length: 0.4, style: 'handlebar' },
{ id: 'vandyke', name: 'Van Dyke', coverage: 'chin-mustache', length: 0.5 }
];
// Body types (6 options)
var BODY_TYPES = [
{ id: 'slim', name: 'Slim', widthRatio: 0.8, heightRatio: 1.0 },
{ id: 'average', name: 'Average', widthRatio: 1.0, heightRatio: 1.0 },
{ id: 'athletic', name: 'Athletic', widthRatio: 1.1, heightRatio: 1.0 },
{ id: 'stocky', name: 'Stocky', widthRatio: 1.2, heightRatio: 0.95 },
{ id: 'tall', name: 'Tall', widthRatio: 0.95, heightRatio: 1.15 },
{ id: 'short', name: 'Short', widthRatio: 1.0, heightRatio: 0.85 }
];
// Clothing styles (4 options)
var CLOTHING_STYLES = [
{ id: 'casual', name: 'Casual', hasCollar: false, sleeveLength: 'short' },
{ id: 'sporty', name: 'Sporty', hasCollar: false, sleeveLength: 'short', hasStripes: true },
{ id: 'formal', name: 'Formal', hasCollar: true, sleeveLength: 'long' },
{ id: 'scifi', name: 'Sci-Fi', hasCollar: true, sleeveLength: 'long', hasGlow: true }
];
// Preset colors for clothing
var CLOTHING_COLORS = [
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#1abc9c',
'#3498db', '#9b59b6', '#34495e', '#95a5a6', '#ecf0f1',
'#c0392b', '#d35400', '#f39c12', '#27ae60', '#16a085',
'#2980b9', '#8e44ad', '#2c3e50', '#7f8c8d', '#bdc3c7'
];
// 10 Default guest avatars
var GUEST_AVATARS = [
{
id: 'guest_1',
name: 'Guest Blue',
base: {
faceShape: 'round',
skinTone: 'medium1',
eyes: { shape: 'round', color: 'blue' },
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: { style: 'short-messy', color: 'darkBrown' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#3498db', pantsColor: '#2c3e50' }
}
},
{
id: 'guest_2',
name: 'Guest Red',
base: {
faceShape: 'oval',
skinTone: 'light2',
eyes: { shape: 'almond', color: 'brown' },
eyebrows: 'arched',
nose: 'pointed',
mouth: 'neutral',
ears: 'normal',
hair: { style: 'long-straight', color: 'black' },
facialHair: 'none',
body: { type: 'slim' },
clothing: { style: 'casual', shirtColor: '#e74c3c', pantsColor: '#34495e' }
}
},
{
id: 'guest_3',
name: 'Guest Green',
base: {
faceShape: 'square',
skinTone: 'tan1',
eyes: { shape: 'narrow', color: 'darkBrown' },
eyebrows: 'thick',
nose: 'wide',
mouth: 'grin',
ears: 'large',
hair: { style: 'buzz', color: 'black' },
facialHair: 'stubble',
body: { type: 'athletic' },
clothing: { style: 'sporty', shirtColor: '#2ecc71', pantsColor: '#2c3e50' }
}
},
{
id: 'guest_4',
name: 'Guest Purple',
base: {
faceShape: 'heart',
skinTone: 'fair2',
eyes: { shape: 'wide', color: 'green' },
eyebrows: 'curved',
nose: 'snub',
mouth: 'small',
ears: 'small',
hair: { style: 'bob', color: 'auburn' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#9b59b6', pantsColor: '#7f8c8d' }
}
},
{
id: 'guest_5',
name: 'Guest Orange',
base: {
faceShape: 'long',
skinTone: 'brown1',
eyes: { shape: 'oval', color: 'amber' },
eyebrows: 'straight',
nose: 'roman',
mouth: 'full',
ears: 'normal',
hair: { style: 'afro', color: 'black' },
facialHair: 'goatee',
body: { type: 'tall' },
clothing: { style: 'casual', shirtColor: '#e67e22', pantsColor: '#34495e' }
}
},
{
id: 'guest_6',
name: 'Guest Teal',
base: {
faceShape: 'soft',
skinTone: 'dark1',
eyes: { shape: 'happy', color: 'darkBrown' },
eyebrows: 'natural',
nose: 'flat',
mouth: 'wide',
ears: 'round',
hair: { style: 'dreads', color: 'black' },
facialHair: 'none',
body: { type: 'stocky' },
clothing: { style: 'casual', shirtColor: '#1abc9c', pantsColor: '#2c3e50' }
}
},
{
id: 'guest_7',
name: 'Guest Gray',
base: {
faceShape: 'angular',
skinTone: 'light3',
eyes: { shape: 'cat', color: 'gray' },
eyebrows: 'sleek',
nose: 'narrow',
mouth: 'smirk',
ears: 'pointed',
hair: { style: 'undercut', color: 'platinum' },
facialHair: 'none',
body: { type: 'slim' },
clothing: { style: 'scifi', shirtColor: '#95a5a6', pantsColor: '#34495e' }
}
},
{
id: 'guest_8',
name: 'Guest Yellow',
base: {
faceShape: 'wide',
skinTone: 'medium3',
eyes: { shape: 'surprised', color: 'hazel' },
eyebrows: 'worried',
nose: 'button',
mouth: 'open',
ears: 'normal',
hair: { style: 'spiky', color: 'blonde' },
facialHair: 'none',
body: { type: 'short' },
clothing: { style: 'casual', shirtColor: '#f1c40f', pantsColor: '#7f8c8d' }
}
},
{
id: 'guest_9',
name: 'Guest Navy',
base: {
faceShape: 'oval',
skinTone: 'tan2',
eyes: { shape: 'droopy', color: 'brown' },
eyebrows: 'bushy',
nose: 'aquiline',
mouth: 'neutral',
ears: 'flat',
hair: { style: 'combover', color: 'gray' },
facialHair: 'fullBeard',
body: { type: 'stocky' },
clothing: { style: 'formal', shirtColor: '#2c3e50', pantsColor: '#1a1a2e' }
}
},
{
id: 'guest_10',
name: 'Guest Pink',
base: {
faceShape: 'round',
skinTone: 'fair3',
eyes: { shape: 'round', color: 'lightBlue' },
eyebrows: 'thin',
nose: 'snub',
mouth: 'pout',
ears: 'small',
hair: { style: 'pigtails', color: 'strawberry' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#ff69b4', pantsColor: '#bdc3c7' }
}
}
];
// Helper function to find item by ID
function findById(array, id) {
for (var i = 0; i < array.length; i++) {
if (array[i].id === id) {
return array[i];
}
}
return null;
}
// Public API
window.AvatarData = {
FACE_SHAPES: FACE_SHAPES,
SKIN_TONES: SKIN_TONES,
EYE_SHAPES: EYE_SHAPES,
EYE_COLORS: EYE_COLORS,
EYEBROW_SHAPES: EYEBROW_SHAPES,
NOSE_SHAPES: NOSE_SHAPES,
MOUTH_SHAPES: MOUTH_SHAPES,
EAR_SHAPES: EAR_SHAPES,
HAIR_STYLES: HAIR_STYLES,
HAIR_COLORS: HAIR_COLORS,
FACIAL_HAIR: FACIAL_HAIR,
BODY_TYPES: BODY_TYPES,
CLOTHING_STYLES: CLOTHING_STYLES,
CLOTHING_COLORS: CLOTHING_COLORS,
GUEST_AVATARS: GUEST_AVATARS,
// Helper methods
getFaceShape: function(id) { return findById(FACE_SHAPES, id); },
getSkinTone: function(id) { return findById(SKIN_TONES, id); },
getEyeShape: function(id) { return findById(EYE_SHAPES, id); },
getEyeColor: function(id) { return findById(EYE_COLORS, id); },
getEyebrowShape: function(id) { return findById(EYEBROW_SHAPES, id); },
getNoseShape: function(id) { return findById(NOSE_SHAPES, id); },
getMouthShape: function(id) { return findById(MOUTH_SHAPES, id); },
getEarShape: function(id) { return findById(EAR_SHAPES, id); },
getHairStyle: function(id) { return findById(HAIR_STYLES, id); },
getHairColor: function(id) { return findById(HAIR_COLORS, id); },
getFacialHair: function(id) { return findById(FACIAL_HAIR, id); },
getBodyType: function(id) { return findById(BODY_TYPES, id); },
getClothingStyle: function(id) { return findById(CLOTHING_STYLES, id); },
getRandomGuestAvatar: function() {
return GUEST_AVATARS[Math.floor(Math.random() * GUEST_AVATARS.length)];
},
createDefaultAvatar: function() {
return {
faceShape: 'round',
skinTone: 'medium1',
eyes: {
shape: 'round',
color: 'brown'
},
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: {
style: 'short-neat',
color: 'brown'
},
facialHair: 'none',
body: {
type: 'average'
},
clothing: {
style: 'casual',
shirtColor: '#3498db',
pantsColor: '#2c3e50'
}
};
}
};
})();
File diff suppressed because it is too large Load Diff
@@ -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']
};
}
};
})();
@@ -0,0 +1,262 @@
(function() {
'use strict';
// =========================================================================
// AGGREGATE ALL CONTEXTS
// =========================================================================
var ALL_CONTEXTS = [];
// Add vehicle contexts
if (window.VehicleContexts) {
VehicleContexts.getAll().forEach(function(ctx) {
ALL_CONTEXTS.push(ctx);
});
}
// Add costume contexts
if (window.CostumeContexts) {
CostumeContexts.getAll().forEach(function(ctx) {
ALL_CONTEXTS.push(ctx);
});
}
// Add pure contexts
if (window.PureContexts) {
PureContexts.getAll().forEach(function(ctx) {
ALL_CONTEXTS.push(ctx);
});
}
// Create lookup map
var CONTEXT_MAP = {};
ALL_CONTEXTS.forEach(function(ctx) {
CONTEXT_MAP[ctx.id] = ctx;
});
// =========================================================================
// CATEGORY METADATA
// =========================================================================
var CATEGORIES = {
vehicle: {
id: 'vehicle',
name: 'Vehicles',
description: 'Place your avatar in vehicles and cockpits',
icon: '🚗',
count: 0 // calculated
},
costume: {
id: 'costume',
name: 'Costumes',
description: 'Dress your avatar in themed outfits',
icon: '👔',
count: 0
},
pure: {
id: 'pure',
name: 'Standard',
description: 'Your avatar with minimal modifications',
icon: '🏃',
count: 0
}
};
// Count per category
ALL_CONTEXTS.forEach(function(ctx) {
if (CATEGORIES[ctx.category]) {
CATEGORIES[ctx.category].count++;
}
});
// =========================================================================
// GAME TYPE MAPPINGS
// =========================================================================
var GAME_TYPE_CONTEXTS = {
platformer: ['platformer-standard', 'adventure-hero', 'knight-armor', 'ninja-outfit', 'superhero-cape'],
racing: ['race-car', 'motorcycle', 'hoverboard', 'airplane'],
shooter: ['spaceship-cockpit', 'tank', 'mech-suit', 'space-suit'],
flying: ['flappy-style', 'airplane', 'jetpack', 'dragon-rider'],
arcade: ['pacman-style', 'flappy-style', 'spaceship-cockpit', 'runner'],
rpg: ['knight-armor', 'wizard-robes', 'ninja-outfit', 'pirate-outfit', 'adventure-hero'],
sports: ['sports-player', 'athlete-uniform', 'runner'],
puzzle: ['scientist-labcoat', 'wizard-robes', 'platformer-standard'],
adventure: ['adventure-hero', 'pirate-outfit', 'cowboy-gear', 'dragon-rider'],
action: ['superhero-cape', 'ninja-outfit', 'mech-suit', 'jetpack'],
underwater: ['submarine', 'space-suit'],
space: ['spaceship-cockpit', 'space-suit', 'mech-suit'],
cooking: ['chef-outfit'],
simulation: ['airplane', 'submarine', 'race-car', 'tank']
};
// =========================================================================
// MODE GROUPINGS
// =========================================================================
var MODE_CONTEXTS = {
HEAD_ONLY: [],
HEAD_AND_SHOULDERS: [],
UPPER_BODY: [],
FULL_BODY: []
};
ALL_CONTEXTS.forEach(function(ctx) {
if (MODE_CONTEXTS[ctx.avatarMode]) {
MODE_CONTEXTS[ctx.avatarMode].push(ctx.id);
}
});
// =========================================================================
// CATALOG STATISTICS
// =========================================================================
var STATS = {
totalContexts: ALL_CONTEXTS.length,
vehicleCount: CATEGORIES.vehicle.count,
costumeCount: CATEGORIES.costume.count,
pureCount: CATEGORIES.pure.count,
version: '1.0.0',
lastUpdated: '2026-02-05'
};
// =========================================================================
// PUBLIC API
// =========================================================================
window.ContextCatalog = {
// Core getters
getAll: function() {
return ALL_CONTEXTS.slice();
},
getById: function(id) {
return CONTEXT_MAP[id] || null;
},
// Category queries
getByCategory: function(category) {
return ALL_CONTEXTS.filter(function(ctx) {
return ctx.category === category;
});
},
getCategories: function() {
return Object.values(CATEGORIES);
},
getCategoryInfo: function(categoryId) {
return CATEGORIES[categoryId] || null;
},
// Mode queries
getByMode: function(mode) {
var ids = MODE_CONTEXTS[mode] || [];
return ids.map(function(id) {
return CONTEXT_MAP[id];
}).filter(Boolean);
},
getModes: function() {
return Object.keys(MODE_CONTEXTS);
},
// Game type recommendations
getRecommendedFor: function(gameType) {
var ids = GAME_TYPE_CONTEXTS[gameType] || [];
return ids.map(function(id) {
return CONTEXT_MAP[id];
}).filter(Boolean);
},
getGameTypes: function() {
return Object.keys(GAME_TYPE_CONTEXTS);
},
// Search/filter
search: function(query) {
query = query.toLowerCase();
return ALL_CONTEXTS.filter(function(ctx) {
return ctx.id.toLowerCase().includes(query) ||
ctx.name.toLowerCase().includes(query) ||
ctx.description.toLowerCase().includes(query);
});
},
filter: function(filters) {
return ALL_CONTEXTS.filter(function(ctx) {
if (filters.category && ctx.category !== filters.category) return false;
if (filters.mode && ctx.avatarMode !== filters.mode) return false;
if (filters.gameType) {
var recommended = GAME_TYPE_CONTEXTS[filters.gameType] || [];
if (recommended.indexOf(ctx.id) === -1) return false;
}
return true;
});
},
// For UI display
getCatalogForUI: function() {
return {
categories: Object.values(CATEGORIES).map(function(cat) {
return {
id: cat.id,
name: cat.name,
description: cat.description,
icon: cat.icon,
contexts: ALL_CONTEXTS.filter(function(ctx) {
return ctx.category === cat.id;
}).map(function(ctx) {
return {
id: ctx.id,
name: ctx.name,
description: ctx.description,
mode: ctx.avatarMode,
recommendedFor: ctx.recommendedFor
};
})
};
})
};
},
// Random selection
getRandom: function(filters) {
var pool = filters ? this.filter(filters) : ALL_CONTEXTS;
if (pool.length === 0) return null;
return pool[Math.floor(Math.random() * pool.length)];
},
getRandomForGameType: function(gameType) {
var recommended = this.getRecommendedFor(gameType);
if (recommended.length === 0) return null;
return recommended[Math.floor(Math.random() * recommended.length)];
},
// Statistics
getStats: function() {
return Object.assign({}, STATS);
},
// Validation
validate: function() {
var issues = [];
ALL_CONTEXTS.forEach(function(ctx) {
if (!ctx.id) issues.push('Context missing id');
if (!ctx.name) issues.push(ctx.id + ': missing name');
if (!ctx.avatarMode) issues.push(ctx.id + ': missing avatarMode');
if (!ctx.category) issues.push(ctx.id + ': missing category');
if (!ctx.animations || ctx.animations.length === 0) {
issues.push(ctx.id + ': missing animations');
}
});
return {
valid: issues.length === 0,
issues: issues
};
}
};
})();
@@ -0,0 +1,959 @@
/**
* ContextRenderer.js - Main renderer for applying contexts to avatars
*
* Handles the complete rendering pipeline for avatars with contexts,
* including vehicles, costumes, and pure animation contexts.
*
* Dependencies (must be loaded before this file):
* - window.AvatarSystem
* - window.AvatarRenderer
* - window.AvatarAnimations
* - window.AccessoryRenderer
* - window.VehicleContexts
* - window.CostumeContexts
* - window.PureContexts
*/
(function() {
'use strict';
// ============================================================
// CONTEXT APPLICATION
// ============================================================
/**
* Apply a context to an avatar, creating a renderable object
* @param {Object} avatar - The avatar object
* @param {string} contextId - The context ID to apply
* @param {Object} options - Optional configuration
* @returns {Object} Avatar with context object
*/
function apply(avatar, contextId, options) {
var context = getContextById(contextId);
if (!context) {
console.error('ContextRenderer: Unknown context ID:', contextId);
return null;
}
// Validate avatar has required mode or can be rendered in it
var mode = context.avatarMode;
if (!mode) {
console.warn('ContextRenderer: Context missing avatarMode:', contextId);
mode = 'profile'; // Default fallback
}
return {
avatar: avatar,
context: context,
mode: mode,
state: {
animation: context.defaultAnimation || 'idle',
frame: 0,
time: 0,
animationStartTime: 0,
animationOptions: {},
effects: [],
props: {},
custom: {}
},
options: options || {}
};
}
// ============================================================
// MAIN RENDER FUNCTION
// ============================================================
/**
* Render complete scene with avatar and context
* @param {Object} avatarWithContext - The avatar+context object from apply()
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - Center X position
* @param {number} y - Center Y position
* @param {Object} options - Render options (scale, time, etc)
*/
function render(avatarWithContext, ctx, x, y, options) {
if (!avatarWithContext || !avatarWithContext.context) {
console.error('ContextRenderer.render: Invalid avatarWithContext');
return;
}
options = options || {};
var scale = options.scale || 1;
var time = options.time || 0;
var awc = avatarWithContext;
var context = awc.context;
var avatar = awc.avatar;
var mode = awc.mode;
// Get context dimensions
var contextWidth = context.width || 64;
var contextHeight = context.height || 64;
var scaledWidth = contextWidth * scale;
var scaledHeight = contextHeight * scale;
// Get animation transforms
var transforms = {};
if (awc.state.animation && window.AvatarAnimations) {
var anim = window.AvatarAnimations.getAnimation(mode, awc.state.animation);
if (anim) {
var progress = anim.frameCount > 0 ? awc.state.frame / anim.frameCount : 0;
transforms = window.AvatarAnimations.interpolateFrame(anim, progress);
}
}
// Calculate avatar position within context
var avatarPosX = context.avatarPosition ? context.avatarPosition.x : 0.5;
var avatarPosY = context.avatarPosition ? context.avatarPosition.y : 0.5;
var avatarX = x + (avatarPosX - 0.5) * scaledWidth;
var avatarY = y + (avatarPosY - 0.5) * scaledHeight;
var avatarScale = scale * (context.avatarScale || 1);
// Route to appropriate render pipeline based on category
var category = context.category || 'pure';
if (category === 'vehicle') {
renderVehicleContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, scaledWidth, scaledHeight, transforms, time);
} else if (category === 'costume') {
renderCostumeContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, scaledWidth, scaledHeight, transforms, time);
} else {
// Pure context (default)
renderPureContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, scaledWidth, scaledHeight, transforms, time);
}
}
// ============================================================
// VEHICLE CONTEXT RENDERING
// ============================================================
function renderVehicleContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, width, height, transforms, time) {
// 1. Context background (vehicle body behind avatar)
if (context.renderBackground && typeof context.renderBackground === 'function') {
ctx.save();
context.renderBackground(ctx, width, height, time, awc.state);
ctx.restore();
}
// 2. Render avatar
if (window.AvatarRenderer && window.AvatarRenderer.render) {
window.AvatarRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, {
scale: avatarScale,
transforms: transforms
});
}
// 3. Render accessories respecting visibility rules
renderAccessoriesForContext(ctx, avatar, context, avatarX, avatarY, avatarScale, time);
// 4. Context foreground (steering wheel, windshield, etc)
if (context.renderForeground && typeof context.renderForeground === 'function') {
ctx.save();
context.renderForeground(ctx, width, height, time, awc.state);
ctx.restore();
}
// 5. Context effects (exhaust, speed lines, etc)
renderContextEffects(ctx, awc, x, y, scale, time);
}
// ============================================================
// COSTUME CONTEXT RENDERING
// ============================================================
function renderCostumeContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, width, height, transforms, time) {
// 1. Costume under-layer (body armor, suit body, etc)
if (context.renderCostumeUnder && typeof context.renderCostumeUnder === 'function') {
ctx.save();
context.renderCostumeUnder(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time);
ctx.restore();
}
// 2. Avatar HEAD ONLY (body is covered by costume)
renderAvatarHeadOnly(ctx, avatar, avatarX, avatarY, avatarScale, transforms);
// 3. Costume over-layer (helmet, held items, cape)
if (context.renderCostumeOver && typeof context.renderCostumeOver === 'function') {
ctx.save();
context.renderCostumeOver(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time);
ctx.restore();
}
// 4. EARNED ACCESSORIES ON TOP (crown shows above helmet, etc)
renderEarnedAccessoriesOnTop(ctx, avatar, context, avatarX, avatarY, avatarScale, time);
// 5. Effects (auras are always visible)
renderEffectsForContext(ctx, avatar, context, avatarX, avatarY, avatarScale, time);
// 6. Context-specific effects
renderContextEffects(ctx, awc, x, y, scale, time);
}
// ============================================================
// PURE CONTEXT RENDERING
// ============================================================
function renderPureContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, width, height, transforms, time) {
// 1. Behind effects (shadows, dust clouds, etc)
if (context.renderEffectsBehind && typeof context.renderEffectsBehind === 'function') {
ctx.save();
context.renderEffectsBehind(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time, awc.state);
ctx.restore();
}
// 2. Full avatar with all accessories
if (window.AvatarRenderer && window.AvatarRenderer.render) {
window.AvatarRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, {
scale: avatarScale,
transforms: transforms
});
}
if (window.AccessoryRenderer && window.AccessoryRenderer.render) {
window.AccessoryRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, avatarScale, time);
}
// 3. Over effects (speed lines, stars, sparkles)
if (context.renderEffectsOver && typeof context.renderEffectsOver === 'function') {
ctx.save();
context.renderEffectsOver(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time, awc.state);
ctx.restore();
}
// 4. Active state effects
renderContextEffects(ctx, awc, x, y, scale, time);
}
// ============================================================
// ANIMATION CONTROL
// ============================================================
/**
* Start an animation on an avatar with context
* @param {Object} avatarWithContext - The avatar+context object
* @param {string} animationId - Animation ID to play
* @param {Object} options - Animation options
* @returns {boolean} True if animation started successfully
*/
function animate(avatarWithContext, animationId, options) {
if (!avatarWithContext || !avatarWithContext.context) {
console.error('ContextRenderer.animate: Invalid avatarWithContext');
return false;
}
var awc = avatarWithContext;
var context = awc.context;
// Validate animation is available for this context
if (context.animations && Array.isArray(context.animations)) {
if (context.animations.indexOf(animationId) === -1) {
console.warn('ContextRenderer: Animation not available for context:', animationId, 'in', context.id);
return false;
}
}
// Check if animation exists in the animation system
if (window.AvatarAnimations) {
var anim = window.AvatarAnimations.getAnimation(awc.mode, animationId);
if (!anim) {
console.warn('ContextRenderer: Animation not found:', animationId, 'for mode', awc.mode);
return false;
}
}
// Set animation state
awc.state.animation = animationId;
awc.state.frame = 0;
awc.state.time = 0;
awc.state.animationStartTime = performance.now();
awc.state.animationOptions = options || {};
// Trigger context-specific effects for this animation
if (context.contextEffects && context.contextEffects[animationId]) {
triggerEffect(awc, context.contextEffects[animationId]);
}
return true;
}
/**
* Update animation state based on elapsed time
* @param {Object} avatarWithContext - The avatar+context object
* @param {number} deltaTime - Time elapsed in seconds
*/
function updateAnimation(avatarWithContext, deltaTime) {
if (!avatarWithContext) return;
var awc = avatarWithContext;
if (!awc.state.animation) return;
// Get animation definition
var anim = null;
if (window.AvatarAnimations) {
anim = window.AvatarAnimations.getAnimation(awc.mode, awc.state.animation);
}
if (!anim) return;
// Update time
awc.state.time += deltaTime;
// Calculate frame based on time
var frameDuration = (anim.frameDuration || 100) / 1000; // Convert ms to seconds
var totalFrames = anim.frameCount || 1;
if (anim.loop) {
// Looping animation
awc.state.frame = (awc.state.time / frameDuration) % totalFrames;
} else {
// One-shot animation
awc.state.frame = Math.min(awc.state.time / frameDuration, totalFrames - 1);
// Check if animation completed
if (awc.state.frame >= totalFrames - 1) {
// Trigger completion callback if set
if (awc.state.animationOptions && awc.state.animationOptions.onComplete) {
awc.state.animationOptions.onComplete();
}
}
}
// Update active effects
updateEffects(awc, deltaTime);
}
// ============================================================
// EFFECT SYSTEM
// ============================================================
/**
* Trigger a visual effect
* @param {Object} avatarWithContext - The avatar+context object
* @param {Object} effectDef - Effect definition
*/
function triggerEffect(avatarWithContext, effectDef) {
if (!avatarWithContext || !effectDef) return;
var awc = avatarWithContext;
// Handle effect definition as string (lookup) or object
var effect;
if (typeof effectDef === 'string') {
effect = {
type: effectDef,
color: '#ffffff',
startTime: performance.now(),
duration: 0.5,
intensity: 1,
data: {}
};
} else {
effect = {
type: effectDef.type || 'flash',
color: effectDef.color || '#ffffff',
startTime: performance.now(),
duration: effectDef.duration || 0.5,
intensity: effectDef.intensity || 1,
data: effectDef.data || {}
};
}
awc.state.effects.push(effect);
}
/**
* Update all active effects
* @param {Object} avatarWithContext - The avatar+context object
* @param {number} deltaTime - Time elapsed in seconds
*/
function updateEffects(avatarWithContext, deltaTime) {
if (!avatarWithContext) return;
var awc = avatarWithContext;
var now = performance.now();
// Filter out expired effects and update active ones
awc.state.effects = awc.state.effects.filter(function(effect) {
var elapsed = (now - effect.startTime) / 1000;
if (elapsed >= effect.duration) {
return false; // Remove expired effect
}
// Update effect progress
effect.progress = elapsed / effect.duration;
return true;
});
}
/**
* Clear all active effects
* @param {Object} avatarWithContext - The avatar+context object
*/
function clearEffects(avatarWithContext) {
if (!avatarWithContext) return;
avatarWithContext.state.effects = [];
}
/**
* Render context effects
*/
function renderContextEffects(ctx, awc, x, y, scale, time) {
if (!awc.state.effects || awc.state.effects.length === 0) return;
var now = performance.now();
awc.state.effects.forEach(function(effect) {
var elapsed = (now - effect.startTime) / 1000;
var progress = Math.min(elapsed / effect.duration, 1);
ctx.save();
switch (effect.type) {
case 'flash':
renderFlashEffect(ctx, x, y, scale, effect, progress);
break;
case 'burst':
renderBurstEffect(ctx, x, y, scale, effect, progress);
break;
case 'trail':
renderTrailEffect(ctx, x, y, scale, effect, progress);
break;
case 'glow':
renderGlowEffect(ctx, x, y, scale, effect, progress);
break;
case 'particles':
renderParticleEffect(ctx, x, y, scale, effect, progress, time);
break;
case 'shake':
// Shake is handled via transforms, not direct rendering
break;
case 'speedLines':
renderSpeedLinesEffect(ctx, x, y, scale, effect, progress);
break;
default:
// Unknown effect type - custom effects can be rendered by context
break;
}
ctx.restore();
});
}
// Effect renderers
function renderFlashEffect(ctx, x, y, scale, effect, progress) {
var alpha = (1 - progress) * effect.intensity;
var radius = 30 * scale * (1 + progress);
ctx.globalAlpha = alpha;
ctx.fillStyle = effect.color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
function renderBurstEffect(ctx, x, y, scale, effect, progress) {
var alpha = (1 - progress) * effect.intensity;
var rayCount = effect.data.rayCount || 8;
var maxLength = (effect.data.maxLength || 40) * scale;
var length = maxLength * progress;
ctx.globalAlpha = alpha;
ctx.strokeStyle = effect.color;
ctx.lineWidth = 2 * scale;
for (var i = 0; i < rayCount; i++) {
var angle = (i / rayCount) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(
x + Math.cos(angle) * length,
y + Math.sin(angle) * length
);
ctx.stroke();
}
}
function renderTrailEffect(ctx, x, y, scale, effect, progress) {
var alpha = (1 - progress) * effect.intensity * 0.5;
var trailLength = (effect.data.length || 20) * scale;
var direction = effect.data.direction || -1; // -1 = trailing left
var gradient = ctx.createLinearGradient(
x, y,
x + trailLength * direction, y
);
gradient.addColorStop(0, effect.color);
gradient.addColorStop(1, 'transparent');
ctx.globalAlpha = alpha;
ctx.fillStyle = gradient;
ctx.fillRect(x, y - 10 * scale, trailLength * direction, 20 * scale);
}
function renderGlowEffect(ctx, x, y, scale, effect, progress) {
var alpha = effect.intensity * (0.5 + 0.5 * Math.sin(progress * Math.PI * 2));
var radius = (effect.data.radius || 25) * scale;
var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, effect.color);
gradient.addColorStop(1, 'transparent');
ctx.globalAlpha = alpha;
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
function renderParticleEffect(ctx, x, y, scale, effect, progress, time) {
var particleCount = effect.data.count || 10;
var alpha = (1 - progress) * effect.intensity;
var spread = (effect.data.spread || 30) * scale;
ctx.globalAlpha = alpha;
ctx.fillStyle = effect.color;
// Use seeded random based on effect start time for consistency
var seed = effect.startTime;
for (var i = 0; i < particleCount; i++) {
var pseudoRandom = Math.sin(seed + i * 12.9898) * 43758.5453;
pseudoRandom = pseudoRandom - Math.floor(pseudoRandom);
var angle = pseudoRandom * Math.PI * 2;
var distance = spread * progress * (0.5 + pseudoRandom * 0.5);
var size = 2 * scale * (1 - progress);
ctx.beginPath();
ctx.arc(
x + Math.cos(angle) * distance,
y + Math.sin(angle) * distance - progress * 20 * scale, // Rise up
size,
0, Math.PI * 2
);
ctx.fill();
}
}
function renderSpeedLinesEffect(ctx, x, y, scale, effect, progress) {
var alpha = effect.intensity * (1 - progress * 0.5);
var lineCount = effect.data.lineCount || 5;
var lineLength = (effect.data.lineLength || 30) * scale;
ctx.globalAlpha = alpha;
ctx.strokeStyle = effect.color;
ctx.lineWidth = 1 * scale;
for (var i = 0; i < lineCount; i++) {
var yOffset = (i - lineCount / 2) * 8 * scale;
var xStart = x - 20 * scale - lineLength * progress;
ctx.beginPath();
ctx.moveTo(xStart, y + yOffset);
ctx.lineTo(xStart + lineLength, y + yOffset);
ctx.stroke();
}
}
// ============================================================
// HELPER RENDERING FUNCTIONS
// ============================================================
/**
* Render only the avatar's head (for costumes that cover body)
*/
function renderAvatarHeadOnly(ctx, avatar, x, y, scale, transforms) {
if (window.AvatarRenderer && window.AvatarRenderer.renderHead) {
window.AvatarRenderer.renderHead(ctx, avatar, x, y, {
scale: scale,
transforms: transforms
});
} else if (window.AvatarRenderer && window.AvatarRenderer.render) {
// Fallback: render full avatar if renderHead not available
// This is a compromise - costume should hide body anyway
ctx.save();
// Clip to head region (approximate)
var headY = y - 12 * scale; // Head is above center
var headRadius = 10 * scale;
ctx.beginPath();
ctx.arc(x, headY, headRadius * 1.5, 0, Math.PI * 2);
ctx.clip();
window.AvatarRenderer.render(ctx, avatar, 'profile', x, y, {
scale: scale,
transforms: transforms
});
ctx.restore();
}
}
/**
* Render accessories respecting context visibility rules
*/
function renderAccessoriesForContext(ctx, avatar, context, x, y, scale, time) {
if (!window.AccessoryRenderer) return;
var vis = context.accessoryVisibility || {};
// Default visibility if not specified
var showHeadwear = vis.headwear !== false;
var showHandheld = vis.handheld !== false;
var showEffects = vis.effects !== false;
var showFace = vis.face !== false;
// Use AccessoryRenderer with visibility filter
if (window.AccessoryRenderer.renderFiltered) {
window.AccessoryRenderer.renderFiltered(ctx, avatar, context.avatarMode || 'profile', x, y, scale, time, {
headwear: showHeadwear,
handheld: showHandheld,
effects: showEffects,
face: showFace
});
} else if (window.AccessoryRenderer.render) {
// Fallback to standard render if filtered not available
// Context-specific visibility would need to be handled differently
window.AccessoryRenderer.render(ctx, avatar, context.avatarMode || 'profile', x, y, scale, time);
}
}
/**
* Render EARNED accessories on top of everything (crowns above helmets, etc)
*/
function renderEarnedAccessoriesOnTop(ctx, avatar, context, x, y, scale, time) {
if (!window.AccessoryRenderer) return;
if (!avatar.accessories) return;
var vis = context.accessoryVisibility || {};
var costumeOffset = context.costumeHeadOffset || { x: 0, y: -5 }; // Crown sits higher on helmet
// Render headwear if visibility allows and avatar has it
if (vis.headwear === true && avatar.accessories.headwear) {
var headwearY = y + costumeOffset.y * scale;
var headwearX = x + costumeOffset.x * scale;
if (window.AccessoryRenderer.renderSingle) {
window.AccessoryRenderer.renderSingle(ctx, avatar, 'headwear', headwearX, headwearY, scale, time);
}
}
// Render effect accessories (auras, trails) if allowed
if (vis.effects === true && avatar.accessories.effect) {
if (window.AccessoryRenderer.renderSingle) {
window.AccessoryRenderer.renderSingle(ctx, avatar, 'effect', x, y, scale, time);
}
}
}
/**
* Render effect accessories (auras, etc) for costume context
*/
function renderEffectsForContext(ctx, avatar, context, x, y, scale, time) {
if (!window.AccessoryRenderer) return;
if (!avatar.accessories || !avatar.accessories.effect) return;
var vis = context.accessoryVisibility || {};
// Effects (auras) are almost always visible
if (vis.effects !== false) {
if (window.AccessoryRenderer.renderSingle) {
window.AccessoryRenderer.renderSingle(ctx, avatar, 'effect', x, y, scale, time);
}
}
}
// ============================================================
// CONTEXT LOOKUP
// ============================================================
/**
* Get context definition by ID
*/
function getContextById(contextId) {
// Check ContextCatalog first
if (window.ContextCatalog && window.ContextCatalog.getById) {
var context = window.ContextCatalog.getById(contextId);
if (context) return context;
}
// Fallback to individual context modules
if (window.VehicleContexts && window.VehicleContexts[contextId]) {
return window.VehicleContexts[contextId];
}
if (window.CostumeContexts && window.CostumeContexts[contextId]) {
return window.CostumeContexts[contextId];
}
if (window.PureContexts && window.PureContexts[contextId]) {
return window.PureContexts[contextId];
}
return null;
}
/**
* Get required avatar mode for a context
*/
function getRequiredMode(contextId) {
var context = getContextById(contextId);
return context ? context.avatarMode : null;
}
/**
* Get available animations for a context
*/
function getAvailableAnimations(contextId) {
var context = getContextById(contextId);
return context && context.animations ? context.animations.slice() : [];
}
// ============================================================
// STATE MANAGEMENT
// ============================================================
/**
* Set custom state value
*/
function setState(avatarWithContext, key, value) {
if (!avatarWithContext || !avatarWithContext.state) return;
avatarWithContext.state.custom[key] = value;
}
/**
* Get custom state value
*/
function getState(avatarWithContext, key) {
if (!avatarWithContext || !avatarWithContext.state) return undefined;
return avatarWithContext.state.custom[key];
}
/**
* Set prop state (for interactive elements)
*/
function setProp(avatarWithContext, propId, state) {
if (!avatarWithContext || !avatarWithContext.state) return;
avatarWithContext.state.props[propId] = state;
}
/**
* Get prop state
*/
function getProp(avatarWithContext, propId) {
if (!avatarWithContext || !avatarWithContext.state) return undefined;
return avatarWithContext.state.props[propId];
}
// ============================================================
// EXPORT UTILITIES
// ============================================================
/**
* Create a sprite sheet from avatar with context
* @param {Object} avatarWithContext - The avatar+context object
* @param {Array} animations - Array of animation IDs to include
* @param {Object} options - Sprite sheet options
* @returns {Object} Sprite sheet data { canvas, frames, meta }
*/
function createSpriteSheet(avatarWithContext, animations, options) {
if (!avatarWithContext) return null;
options = options || {};
var frameWidth = options.frameWidth || 64;
var frameHeight = options.frameHeight || 64;
var scale = options.scale || 1;
var awc = avatarWithContext;
var allFrames = [];
// Collect all frames for each animation
animations.forEach(function(animId) {
var anim = null;
if (window.AvatarAnimations) {
anim = window.AvatarAnimations.getAnimation(awc.mode, animId);
}
if (!anim) return;
for (var i = 0; i < anim.frameCount; i++) {
allFrames.push({
animation: animId,
frame: i,
frameCount: anim.frameCount
});
}
});
if (allFrames.length === 0) return null;
// Calculate sprite sheet dimensions
var cols = Math.ceil(Math.sqrt(allFrames.length));
var rows = Math.ceil(allFrames.length / cols);
var sheetWidth = cols * frameWidth;
var sheetHeight = rows * frameHeight;
// Create canvas
var canvas = document.createElement('canvas');
canvas.width = sheetWidth;
canvas.height = sheetHeight;
var ctx = canvas.getContext('2d');
// Render each frame
var frames = [];
allFrames.forEach(function(frameData, index) {
var col = index % cols;
var row = Math.floor(index / cols);
var x = col * frameWidth + frameWidth / 2;
var y = row * frameHeight + frameHeight / 2;
// Temporarily set animation state
awc.state.animation = frameData.animation;
awc.state.frame = frameData.frame;
// Render
render(awc, ctx, x, y, { scale: scale, time: 0 });
frames.push({
animation: frameData.animation,
frame: frameData.frame,
x: col * frameWidth,
y: row * frameHeight,
width: frameWidth,
height: frameHeight
});
});
return {
canvas: canvas,
frames: frames,
meta: {
width: sheetWidth,
height: sheetHeight,
frameWidth: frameWidth,
frameHeight: frameHeight,
animations: animations
}
};
}
/**
* Export avatar with context for game use
* @param {Object} avatarWithContext - The avatar+context object
* @returns {Object} Exportable data
*/
function exportForGame(avatarWithContext) {
if (!avatarWithContext) return null;
var awc = avatarWithContext;
var context = awc.context;
return {
avatarId: awc.avatar.id || null,
contextId: context.id,
mode: awc.mode,
category: context.category,
animations: context.animations || [],
defaultAnimation: context.defaultAnimation || 'idle',
dimensions: {
width: context.width || 64,
height: context.height || 64
},
avatarPosition: context.avatarPosition || { x: 0.5, y: 0.5 },
avatarScale: context.avatarScale || 1,
accessoryVisibility: context.accessoryVisibility || {},
customState: Object.assign({}, awc.state.custom)
};
}
// ============================================================
// CATALOG ACCESS (delegates to ContextCatalog)
// ============================================================
function getCatalog() {
if (window.ContextCatalog && window.ContextCatalog.getAll) {
return window.ContextCatalog.getAll();
}
// Fallback: aggregate from individual modules
var catalog = [];
if (window.VehicleContexts) {
Object.keys(window.VehicleContexts).forEach(function(id) {
if (typeof window.VehicleContexts[id] === 'object') {
catalog.push(window.VehicleContexts[id]);
}
});
}
if (window.CostumeContexts) {
Object.keys(window.CostumeContexts).forEach(function(id) {
if (typeof window.CostumeContexts[id] === 'object') {
catalog.push(window.CostumeContexts[id]);
}
});
}
if (window.PureContexts) {
Object.keys(window.PureContexts).forEach(function(id) {
if (typeof window.PureContexts[id] === 'object') {
catalog.push(window.PureContexts[id]);
}
});
}
return catalog;
}
function getContext(id) {
return getContextById(id);
}
// ============================================================
// PUBLIC API
// ============================================================
window.ContextRenderer = {
// Core methods
apply: apply,
render: render,
animate: animate,
updateAnimation: updateAnimation,
// Effect control
triggerEffect: triggerEffect,
updateEffects: updateEffects,
clearEffects: clearEffects,
// State management
setState: setState,
getState: getState,
setProp: setProp,
getProp: getProp,
// Context queries
getRequiredMode: getRequiredMode,
getAvailableAnimations: getAvailableAnimations,
// Catalog access (delegates to ContextCatalog)
getCatalog: getCatalog,
getContext: getContext,
// Utility
createSpriteSheet: createSpriteSheet,
exportForGame: exportForGame,
// Version
version: '1.0.0'
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff