Files
2026-04-30 02:14:25 +00:00

960 lines
30 KiB
JavaScript

/**
* 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'
};
})();