Files
gamercomp/frontend/public/assets/assetManager.js
T
2026-04-30 02:14:25 +00:00

1273 lines
44 KiB
JavaScript

/**
* AssetManager - Main Coordinator for All Asset Systems
*
* Coordinates music, backgrounds, avatars, and contexts into unified
* asset management with recommendations based on game descriptions.
*
* Dependencies (must be loaded before this file):
* - window.MusicEngine (from /assets/music/)
* - window.BackgroundEngine (from /assets/backgrounds/)
* - window.AvatarSystem (from /assets/avatar/)
* - window.ContextRenderer (from /assets/avatar/)
* - window.ContextCatalog (from /assets/avatar/)
* - window.AssetCatalog (unified catalog)
* - window.AssetPresets (preset combinations)
* - window.AssetCompatibility (compatibility rules)
*/
(function() {
'use strict';
var initialized = false;
var loadedAssets = {
music: null,
background: null,
avatar: null,
context: null
};
var cache = new Map();
// =========================================================================
// INITIALIZATION
// =========================================================================
/**
* Initialize the AssetManager and verify all dependencies
* @param {Object} options - Configuration options
* @returns {Promise} Resolves with system stats on success
*/
function initialize(options) {
options = options || {};
return new Promise(function(resolve, reject) {
try {
// Verify all dependencies loaded
var missing = [];
if (!window.MusicEngine) missing.push('MusicEngine');
if (!window.BackgroundEngine) missing.push('BackgroundEngine');
if (!window.AvatarSystem) missing.push('AvatarSystem');
if (!window.ContextRenderer) missing.push('ContextRenderer');
if (!window.ContextCatalog) missing.push('ContextCatalog');
if (!window.AssetCatalog) missing.push('AssetCatalog');
if (!window.AssetPresets) missing.push('AssetPresets');
if (!window.AssetCompatibility) missing.push('AssetCompatibility');
if (missing.length > 0) {
reject(new Error('Missing dependencies: ' + missing.join(', ')));
return;
}
// Initialize subsystems if they have init methods
var initPromises = [];
// MusicEngine needs Tone.js - may be async
if (window.MusicEngine.initialize && !window.MusicEngine.isInitialized()) {
initPromises.push(window.MusicEngine.initialize());
}
// BackgroundEngine - typically sync
if (window.BackgroundEngine.initialize && !window.BackgroundEngine.isInitialized()) {
var bgInit = window.BackgroundEngine.initialize();
if (bgInit && bgInit.then) {
initPromises.push(bgInit);
}
}
// AvatarSystem - typically sync
if (window.AvatarSystem.initialize && !window.AvatarSystem.isInitialized()) {
var avInit = window.AvatarSystem.initialize();
if (avInit && avInit.then) {
initPromises.push(avInit);
}
}
// Wait for all subsystem initializations
Promise.all(initPromises).then(function() {
initialized = true;
// Gather stats
var stats = {
music: {
songs: window.MusicEngine.getCatalog ? window.MusicEngine.getCatalog().length : 0
},
backgrounds: {
count: window.BackgroundEngine.getTotalCount ? window.BackgroundEngine.getTotalCount() : 0
},
avatarContexts: {
count: window.ContextCatalog.getAll ? window.ContextCatalog.getAll().length : 0
},
presets: {
count: window.AssetPresets.getAll ? window.AssetPresets.getAll().length : 0
}
};
resolve(stats);
}).catch(function(err) {
reject(new Error('Subsystem initialization failed: ' + err.message));
});
} catch (err) {
reject(err);
}
});
}
// =========================================================================
// KEYWORD PARSING HELPERS
// =========================================================================
/**
* Parse keywords from string or array input
* @param {string|Array} input - Keywords as string or array
* @returns {Array} Normalized array of keywords
*/
function parseKeywords(input) {
if (Array.isArray(input)) {
return input.map(function(w) {
return String(w).toLowerCase().trim();
}).filter(function(w) {
return w.length > 2;
});
}
if (typeof input !== 'string') return [];
return input.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.split(/\s+/)
.filter(function(w) { return w.length > 2; });
}
// =========================================================================
// THEME DETECTION
// =========================================================================
/**
* Detect theme from keywords
* @param {Array} keywords - Parsed keywords
* @returns {string} Detected theme name
*/
function detectTheme(keywords) {
var themeKeywords = {
space: ['space', 'star', 'planet', 'galaxy', 'alien', 'rocket', 'asteroid', 'nebula', 'cosmic', 'orbit', 'moon', 'mars', 'saturn', 'comet', 'meteor', 'ufo', 'astronaut', 'spaceship'],
nature: ['forest', 'jungle', 'tree', 'mountain', 'ocean', 'river', 'nature', 'animal', 'garden', 'flower', 'grass', 'wildlife', 'deer', 'bear', 'wolf', 'leaf', 'plant', 'wilderness'],
urban: ['city', 'street', 'building', 'urban', 'neon', 'downtown', 'traffic', 'skyscraper', 'rooftop', 'subway', 'metro', 'alley', 'nightlife', 'cyberpunk', 'futuristic'],
fantasy: ['magic', 'wizard', 'dragon', 'castle', 'knight', 'fairy', 'enchanted', 'mythical', 'unicorn', 'elf', 'dwarf', 'potion', 'spell', 'medieval', 'kingdom', 'sorcerer', 'quest'],
spooky: ['ghost', 'haunted', 'scary', 'zombie', 'halloween', 'dark', 'creepy', 'monster', 'vampire', 'skeleton', 'graveyard', 'horror', 'nightmare', 'witch', 'pumpkin', 'bat'],
underwater: ['ocean', 'fish', 'coral', 'submarine', 'underwater', 'sea', 'diving', 'aquatic', 'whale', 'shark', 'dolphin', 'reef', 'seaweed', 'mermaid', 'treasure', 'deep'],
retro: ['retro', 'pixel', 'arcade', '8bit', 'classic', 'vintage', 'oldschool', 'nostalgia', 'gameboy', 'nes', 'snes', 'atari', 'commodore', 'synthwave'],
sports: ['soccer', 'basketball', 'football', 'tennis', 'sports', 'athlete', 'stadium', 'baseball', 'hockey', 'golf', 'racing', 'olympics', 'gym', 'fitness', 'championship'],
sky: ['sky', 'cloud', 'flying', 'bird', 'airplane', 'air', 'balloon', 'wind', 'glider', 'parachute', 'kite', 'helicopter', 'jet', 'eagle', 'hawk', 'soar'],
colorful: ['rainbow', 'colorful', 'bright', 'candy', 'party', 'disco', 'fun', 'vibrant', 'neon', 'carnival', 'festival', 'celebration', 'confetti', 'balloon', 'fireworks'],
desert: ['desert', 'sand', 'pyramid', 'egypt', 'cactus', 'oasis', 'camel', 'dune', 'sahara', 'temple', 'ancient', 'scorpion', 'mummy'],
arctic: ['ice', 'snow', 'arctic', 'polar', 'penguin', 'frozen', 'winter', 'glacier', 'cold', 'blizzard', 'igloo', 'eskimo', 'north', 'south', 'pole'],
western: ['western', 'cowboy', 'desert', 'saloon', 'sheriff', 'outlaw', 'horse', 'rodeo', 'wild', 'west', 'cactus', 'tumbleweed']
};
var scores = {};
for (var theme in themeKeywords) {
scores[theme] = 0;
themeKeywords[theme].forEach(function(kw) {
keywords.forEach(function(word) {
// Exact match
if (word === kw) {
scores[theme] += 3;
}
// Partial match
else if (word.includes(kw) || kw.includes(word)) {
scores[theme] += 1;
}
});
});
}
// Find highest scoring theme
var best = 'nature'; // default
var bestScore = 0;
for (var t in scores) {
if (scores[t] > bestScore) {
best = t;
bestScore = scores[t];
}
}
return best;
}
// =========================================================================
// MOOD DETECTION
// =========================================================================
/**
* Detect mood from keywords
* @param {Array} keywords - Parsed keywords
* @returns {string} Detected mood name
*/
function detectMood(keywords) {
var moodKeywords = {
epic: ['epic', 'battle', 'hero', 'adventure', 'quest', 'warrior', 'legendary', 'conquest', 'victory', 'triumph', 'glory', 'champion', 'mighty', 'powerful'],
chill: ['chill', 'relax', 'calm', 'peaceful', 'zen', 'meditation', 'serene', 'tranquil', 'mellow', 'easy', 'gentle', 'soothing', 'quiet'],
intense: ['intense', 'fast', 'action', 'rush', 'speed', 'chase', 'urgent', 'adrenaline', 'extreme', 'rapid', 'frantic', 'wild', 'crazy'],
playful: ['fun', 'playful', 'silly', 'cute', 'happy', 'joy', 'cheerful', 'whimsical', 'bouncy', 'bubbly', 'friendly', 'goofy', 'cartoon'],
mysterious: ['mystery', 'scary', 'dark', 'haunted', 'ghost', 'creepy', 'strange', 'enigma', 'secret', 'hidden', 'shadow', 'fog', 'eerie'],
heroic: ['hero', 'brave', 'save', 'rescue', 'champion', 'courage', 'noble', 'defender', 'protect', 'guardian', 'justice', 'valor'],
quirky: ['weird', 'quirky', 'strange', 'unusual', 'unique', 'odd', 'wacky', 'bizarre', 'eccentric', 'offbeat', 'random'],
ambient: ['ambient', 'atmosphere', 'background', 'subtle', 'minimal', 'soft', 'dreamy', 'floating', 'ethereal', 'space'],
dramatic: ['dramatic', 'tension', 'suspense', 'climax', 'serious', 'grave', 'tense', 'thriller', 'danger'],
upbeat: ['upbeat', 'energetic', 'lively', 'peppy', 'dynamic', 'exciting', 'vibrant', 'active', 'bouncy'],
nostalgic: ['nostalgic', 'retro', 'classic', 'vintage', 'memories', 'throwback', 'oldschool', 'reminiscent']
};
var scores = {};
for (var mood in moodKeywords) {
scores[mood] = 0;
moodKeywords[mood].forEach(function(kw) {
keywords.forEach(function(word) {
// Exact match
if (word === kw) {
scores[mood] += 3;
}
// Partial match
else if (word.includes(kw) || kw.includes(word)) {
scores[mood] += 1;
}
});
});
}
// Find highest scoring mood
var best = 'playful'; // default for games
var bestScore = 0;
for (var m in scores) {
if (scores[m] > bestScore) {
best = m;
bestScore = scores[m];
}
}
return best;
}
// =========================================================================
// MECHANICS DETECTION
// =========================================================================
/**
* Detect game mechanics from keywords to determine avatar display mode
* @param {Array} keywords - Parsed keywords
* @returns {Object} { mode: string, context: string|null }
*/
function detectMechanics(keywords) {
var result = { mode: 'FULL_BODY', context: null };
// Flying mechanics → HEAD_ONLY with flappy-style
var flyingKeywords = ['fly', 'flying', 'flap', 'bird', 'wings', 'soar', 'glide', 'hover', 'flutter', 'airborne'];
if (keywords.some(function(w) { return flyingKeywords.includes(w); })) {
result.mode = 'HEAD_ONLY';
result.context = 'flappy-style';
return result;
}
// Racing mechanics → HEAD_AND_SHOULDERS with race-car
var racingKeywords = ['race', 'racing', 'car', 'drive', 'kart', 'drift', 'speed', 'vehicle', 'steering', 'track'];
if (keywords.some(function(w) { return racingKeywords.includes(w); })) {
result.mode = 'HEAD_AND_SHOULDERS';
result.context = 'race-car';
return result;
}
// Space combat → HEAD_ONLY with spaceship-cockpit
var spaceKeywords = ['spaceship', 'spacecraft', 'cockpit', 'pilot', 'starship', 'fighter', 'turret'];
if (keywords.some(function(w) { return spaceKeywords.includes(w); })) {
result.mode = 'HEAD_ONLY';
result.context = 'spaceship-cockpit';
return result;
}
// Submarine/underwater vehicle → HEAD_AND_SHOULDERS
var subKeywords = ['submarine', 'sub', 'bathysphere', 'underwater', 'diving'];
if (keywords.some(function(w) { return subKeywords.includes(w); })) {
result.mode = 'HEAD_AND_SHOULDERS';
result.context = 'submarine-cockpit';
return result;
}
// Platformer → FULL_BODY
var platformerKeywords = ['jump', 'platform', 'run', 'runner', 'side', 'scrolling', 'platformer', 'hop', 'bounce'];
if (keywords.some(function(w) { return platformerKeywords.includes(w); })) {
result.mode = 'FULL_BODY';
result.context = 'platformer-standard';
return result;
}
// Puzzle/static → HEAD_AND_SHOULDERS
var puzzleKeywords = ['puzzle', 'match', 'block', 'tetris', 'tile', 'grid', 'board'];
if (keywords.some(function(w) { return puzzleKeywords.includes(w); })) {
result.mode = 'HEAD_AND_SHOULDERS';
result.context = 'puzzle-frame';
return result;
}
// Shooter → HEAD_ONLY with targeting-hud
var shooterKeywords = ['shoot', 'shooter', 'gun', 'blast', 'laser', 'fire', 'bullet', 'aim'];
if (keywords.some(function(w) { return shooterKeywords.includes(w); })) {
result.mode = 'HEAD_ONLY';
result.context = 'targeting-hud';
return result;
}
// Sports → FULL_BODY
var sportsKeywords = ['soccer', 'basketball', 'football', 'tennis', 'sports', 'athlete', 'ball'];
if (keywords.some(function(w) { return sportsKeywords.includes(w); })) {
result.mode = 'FULL_BODY';
result.context = 'sports-arena';
return result;
}
// Swimming → FULL_BODY with swim context
var swimKeywords = ['swim', 'swimming', 'diver', 'snorkel', 'fins'];
if (keywords.some(function(w) { return swimKeywords.includes(w); })) {
result.mode = 'FULL_BODY';
result.context = 'swimmer-underwater';
return result;
}
return result;
}
// =========================================================================
// RECOMMENDATION ENGINE
// =========================================================================
/**
* Get asset recommendations based on game description
* @param {string} gameStyle - One of the 11 game styles
* @param {string|Array} keywords - Description keywords or array of keywords
* @param {string} mood - Optional mood override
* @returns {Array} Top 3 recommendations with explanations
*/
function getRecommendations(gameStyle, keywords, mood) {
// Parse keywords if string
var parsedKeywords = parseKeywords(keywords);
// Detect theme from keywords
var detectedTheme = detectTheme(parsedKeywords);
var detectedMood = mood || detectMood(parsedKeywords);
var detectedMechanics = detectMechanics(parsedKeywords);
// Get compatible combinations from AssetCompatibility
var compatibleMusic = [];
var compatibleBackgrounds = [];
var compatibleContexts = [];
if (window.AssetCompatibility) {
if (window.AssetCompatibility.getMusicForMood) {
compatibleMusic = window.AssetCompatibility.getMusicForMood(detectedMood);
}
if (window.AssetCompatibility.getBackgroundsForTheme) {
compatibleBackgrounds = window.AssetCompatibility.getBackgroundsForTheme(detectedTheme);
}
if (window.AssetCompatibility.getContextsForMechanics) {
compatibleContexts = window.AssetCompatibility.getContextsForMechanics(detectedMechanics);
}
}
// Score and rank combinations
var candidates = [];
// Use presets as base candidates
var presets = [];
if (window.AssetPresets && window.AssetPresets.getByGameStyle) {
presets = window.AssetPresets.getByGameStyle(gameStyle);
}
// If no presets for this game style, get all presets
if (presets.length === 0 && window.AssetPresets && window.AssetPresets.getAll) {
presets = window.AssetPresets.getAll();
}
presets.forEach(function(preset) {
var score = scorePreset(preset, detectedTheme, detectedMood, parsedKeywords, compatibleMusic, compatibleBackgrounds, compatibleContexts);
candidates.push({
preset: preset,
score: score,
explanation: generateExplanation(preset, parsedKeywords, detectedTheme, detectedMood)
});
});
// Also create dynamic combinations from compatible assets if we have few presets
if (candidates.length < 3 && compatibleMusic.length > 0 && compatibleBackgrounds.length > 0) {
for (var i = 0; i < Math.min(3, compatibleMusic.length); i++) {
for (var j = 0; j < Math.min(3, compatibleBackgrounds.length); j++) {
var dynamicPreset = {
id: 'dynamic-' + i + '-' + j,
name: detectedTheme + ' ' + detectedMood + ' combo',
description: 'Auto-generated combination',
music: compatibleMusic[i],
background: compatibleBackgrounds[j],
avatarContext: compatibleContexts[0] || { mode: detectedMechanics.mode, context: detectedMechanics.context }
};
var score = scorePreset(dynamicPreset, detectedTheme, detectedMood, parsedKeywords, compatibleMusic, compatibleBackgrounds, compatibleContexts);
// Slight penalty for dynamic presets
score = score * 0.9;
candidates.push({
preset: dynamicPreset,
score: score,
explanation: generateExplanation(dynamicPreset, parsedKeywords, detectedTheme, detectedMood)
});
}
}
}
// Sort by score descending
candidates.sort(function(a, b) { return b.score - a.score; });
// Remove duplicates based on preset ID
var seen = {};
var uniqueCandidates = candidates.filter(function(c) {
if (seen[c.preset.id]) return false;
seen[c.preset.id] = true;
return true;
});
// Return top 3
return uniqueCandidates.slice(0, 3).map(function(c) {
return {
config: presetToConfig(c.preset),
score: c.score,
explanation: c.explanation,
presetId: c.preset.id,
detectedTheme: detectedTheme,
detectedMood: detectedMood,
detectedMechanics: detectedMechanics
};
});
}
// =========================================================================
// SCORING
// =========================================================================
/**
* Score a preset based on detected attributes
* @param {Object} preset - The preset to score
* @param {string} theme - Detected theme
* @param {string} mood - Detected mood
* @param {Array} keywords - Parsed keywords
* @param {Array} compatibleMusic - Compatible music from AssetCompatibility
* @param {Array} compatibleBackgrounds - Compatible backgrounds
* @param {Array} compatibleContexts - Compatible contexts
* @returns {number} Score value
*/
function scorePreset(preset, theme, mood, keywords, compatibleMusic, compatibleBackgrounds, compatibleContexts) {
var score = 0;
// Theme match with background (high weight)
if (preset.background) {
var bgTheme = (preset.background.theme || '').toLowerCase();
if (bgTheme === theme.toLowerCase()) {
score += 15;
} else if (bgTheme.includes(theme.toLowerCase()) || theme.toLowerCase().includes(bgTheme)) {
score += 8;
}
// Check if background is in compatible list
if (compatibleBackgrounds && compatibleBackgrounds.length > 0) {
var isCompatibleBg = compatibleBackgrounds.some(function(bg) {
return bg && bg.theme === preset.background.theme;
});
if (isCompatibleBg) score += 5;
}
}
// Mood match with music (high weight)
if (preset.music) {
var musicMood = (preset.music.mood || '').toLowerCase();
if (musicMood === mood.toLowerCase()) {
score += 15;
} else if (musicMood.includes(mood.toLowerCase()) || mood.toLowerCase().includes(musicMood)) {
score += 8;
}
// Check if music is in compatible list
if (compatibleMusic && compatibleMusic.length > 0) {
var isCompatibleMusic = compatibleMusic.some(function(m) {
return m && m.mood === preset.music.mood;
});
if (isCompatibleMusic) score += 5;
}
}
// Context compatibility
if (preset.avatarContext && compatibleContexts && compatibleContexts.length > 0) {
var isCompatibleContext = compatibleContexts.some(function(ctx) {
return ctx && ctx.context === preset.avatarContext.context;
});
if (isCompatibleContext) score += 10;
}
// Keyword matches in description (medium weight)
if (preset.description) {
var desc = preset.description.toLowerCase();
keywords.forEach(function(kw) {
if (desc.includes(kw)) {
score += 2;
}
});
}
// Keyword matches in name (high weight for direct matches)
if (preset.name) {
var name = preset.name.toLowerCase();
keywords.forEach(function(kw) {
if (name.includes(kw)) {
score += 4;
}
});
}
// Keyword matches in tags
if (preset.tags && Array.isArray(preset.tags)) {
var presetTags = preset.tags.map(function(t) { return t.toLowerCase(); });
keywords.forEach(function(kw) {
if (presetTags.includes(kw)) {
score += 3;
}
});
}
// Energy level considerations (if present)
if (preset.music && preset.music.energy !== undefined) {
// Intense moods prefer high energy
if (mood === 'intense' || mood === 'epic' || mood === 'dramatic') {
if (preset.music.energy >= 7) score += 3;
}
// Chill moods prefer low energy
else if (mood === 'chill' || mood === 'ambient') {
if (preset.music.energy <= 4) score += 3;
}
// Playful prefers medium energy
else if (mood === 'playful' || mood === 'upbeat') {
if (preset.music.energy >= 5 && preset.music.energy <= 8) score += 3;
}
}
return score;
}
// =========================================================================
// EXPLANATION GENERATION
// =========================================================================
/**
* Generate human-readable explanation for why a preset was recommended
* @param {Object} preset - The recommended preset
* @param {Array} keywords - User's keywords
* @param {string} theme - Detected theme
* @param {string} mood - Detected mood
* @returns {string} Explanation text
*/
function generateExplanation(preset, keywords, theme, mood) {
var reasons = [];
// Music reasoning
if (preset.music) {
var musicMood = preset.music.mood || 'dynamic';
var energy = preset.music.energy || 5;
var energyDesc = energy >= 7 ? 'high-energy' : (energy <= 3 ? 'calm' : 'balanced');
reasons.push(musicMood + ' ' + energyDesc + ' music sets the mood');
}
// Background reasoning
if (preset.background) {
var bgTheme = preset.background.theme || 'themed';
var variant = preset.background.variant ? ' (' + preset.background.variant + ')' : '';
if (bgTheme.toLowerCase() === theme.toLowerCase()) {
reasons.push(bgTheme + variant + ' background matches your ' + theme + ' theme');
} else {
reasons.push(bgTheme + variant + ' background provides atmosphere');
}
}
// Context reasoning
if (preset.avatarContext) {
var contextName = preset.avatarContext.context || preset.avatarContext.mode;
reasons.push(contextName + ' avatar context complements the gameplay');
}
// Add keyword matches if found
var matchedKeywords = [];
if (preset.name) {
keywords.forEach(function(kw) {
if (preset.name.toLowerCase().includes(kw)) {
matchedKeywords.push(kw);
}
});
}
if (matchedKeywords.length > 0) {
reasons.push('matches keywords: ' + matchedKeywords.slice(0, 3).join(', '));
}
return reasons.join('; ') || 'Good general-purpose combination';
}
// =========================================================================
// PRESET LOOKUP
// =========================================================================
/**
* Find a preset by game style with variety index
* @param {string} gameStyle - Game style to search for
* @param {number} variety - Index for variation (cycles through available presets)
* @returns {Object|null} Preset object or null
*/
function findPreset(gameStyle, variety) {
variety = variety || 0;
if (!window.AssetPresets || !window.AssetPresets.getByGameStyle) {
return null;
}
var presets = window.AssetPresets.getByGameStyle(gameStyle);
if (presets.length === 0) {
// Fallback to all presets
presets = window.AssetPresets.getAll ? window.AssetPresets.getAll() : [];
}
if (presets.length === 0) return null;
var index = Math.abs(variety) % presets.length;
return presets[index];
}
/**
* Convert a preset to a configuration object
* @param {Object} preset - Preset to convert
* @returns {Object} Configuration object
*/
function presetToConfig(preset) {
if (!preset) return null;
return {
music: preset.music ? {
mood: preset.music.mood,
energy: preset.music.energy,
songId: preset.music.songId || preset.music.id,
tags: preset.music.tags
} : null,
background: preset.background ? {
theme: preset.background.theme,
variant: preset.background.variant,
animated: preset.background.animated !== false
} : null,
avatarContext: preset.avatarContext ? {
mode: preset.avatarContext.mode,
context: preset.avatarContext.context,
props: preset.avatarContext.props
} : null,
colorPalette: preset.colorPalette || null,
presetId: preset.id,
name: preset.name,
description: preset.description
};
}
// =========================================================================
// ASSET LOADING
// =========================================================================
/**
* Load game assets based on configuration
* @param {Object} config - Asset configuration
* @returns {Promise} Resolves with loaded assets
*/
function loadGameAssets(config) {
return new Promise(function(resolve, reject) {
try {
var results = {};
var loadPromises = [];
// Load music
if (config.music && window.MusicEngine) {
var songQuery = {
mood: config.music.mood,
energy: config.music.energy
};
var songs = [];
if (window.MusicEngine.findSongs) {
songs = window.MusicEngine.findSongs(songQuery);
}
if (songs.length > 0) {
results.music = songs[0];
// If MusicEngine has async load method
if (window.MusicEngine.loadSong) {
loadPromises.push(
window.MusicEngine.loadSong(songs[0].id || songs[0].songId).then(function(loaded) {
results.music = loaded;
}).catch(function() {
// Keep the basic song info even if full load fails
})
);
}
}
}
// Load background
if (config.background && window.BackgroundEngine) {
results.background = {
theme: config.background.theme,
variant: config.background.variant,
animated: config.background.animated !== false,
render: function(ctx, width, height, time) {
if (window.BackgroundEngine.render) {
window.BackgroundEngine.render(
ctx,
config.background.theme,
config.background.variant,
width,
height,
time
);
}
}
};
// Preload background if method exists
if (window.BackgroundEngine.preload) {
loadPromises.push(
Promise.resolve(window.BackgroundEngine.preload(config.background.theme, config.background.variant))
);
}
}
// Load avatar context
if (config.avatarContext && config.avatar && window.ContextRenderer) {
if (window.ContextRenderer.apply) {
results.avatarWithContext = window.ContextRenderer.apply(
config.avatar,
config.avatarContext.context,
config.avatarContext.props
);
}
results.avatarContext = config.avatarContext;
}
// Wait for all async loads
Promise.all(loadPromises).then(function() {
loadedAssets = results;
// Cache the loaded assets
if (config.presetId) {
cache.set(config.presetId, results);
}
resolve(results);
}).catch(function(err) {
// Still return what we have even if some loads failed
loadedAssets = results;
resolve(results);
});
} catch (err) {
reject(err);
}
});
}
// =========================================================================
// VALIDATION
// =========================================================================
/**
* Validate an asset combination for compatibility
* @param {Object} config - Configuration to validate
* @returns {Object} { valid: boolean, issues: Array, warnings: Array }
*/
function validateCombination(config) {
// Use AssetCompatibility if available
if (window.AssetCompatibility && window.AssetCompatibility.validate) {
return window.AssetCompatibility.validate(config);
}
// Basic validation fallback
var result = {
valid: true,
issues: [],
warnings: []
};
// Check required fields
if (!config) {
result.valid = false;
result.issues.push('No configuration provided');
return result;
}
// Validate music config
if (config.music) {
if (!config.music.mood) {
result.warnings.push('Music mood not specified');
}
if (config.music.energy !== undefined) {
if (config.music.energy < 1 || config.music.energy > 10) {
result.issues.push('Music energy must be between 1 and 10');
result.valid = false;
}
}
}
// Validate background config
if (config.background) {
if (!config.background.theme) {
result.warnings.push('Background theme not specified');
}
}
// Validate avatar context
if (config.avatarContext) {
var validModes = ['FULL_BODY', 'HEAD_AND_SHOULDERS', 'HEAD_ONLY'];
if (config.avatarContext.mode && validModes.indexOf(config.avatarContext.mode) === -1) {
result.issues.push('Invalid avatar mode: ' + config.avatarContext.mode);
result.valid = false;
}
}
return result;
}
// =========================================================================
// DESCRIPTION
// =========================================================================
/**
* Generate a human-readable description of asset configuration
* @param {Object} config - Configuration to describe
* @returns {string} Description text
*/
function describeAssets(config) {
if (!config) return 'No assets configured';
var parts = [];
if (config.music) {
var musicDesc = 'Music: ';
if (config.music.mood) {
musicDesc += config.music.mood + ' mood';
}
if (config.music.energy !== undefined) {
musicDesc += ' (energy ' + config.music.energy + '/10)';
}
if (config.music.songId) {
musicDesc += ' [' + config.music.songId + ']';
}
parts.push(musicDesc);
}
if (config.background) {
var bgDesc = 'Background: ';
if (config.background.theme) {
bgDesc += config.background.theme;
}
if (config.background.variant) {
bgDesc += ' - ' + config.background.variant;
}
if (config.background.animated === false) {
bgDesc += ' (static)';
}
parts.push(bgDesc);
}
if (config.avatarContext) {
var avDesc = 'Avatar: ';
if (config.avatarContext.mode) {
avDesc += config.avatarContext.mode;
}
if (config.avatarContext.context) {
avDesc += ' in ' + config.avatarContext.context;
}
parts.push(avDesc);
}
if (config.name) {
parts.unshift('Preset: ' + config.name);
}
return parts.join(' | ') || 'Default configuration';
}
// =========================================================================
// VARIATIONS
// =========================================================================
/**
* Get a variation of the current configuration
* @param {Object} config - Current configuration
* @param {string} variationType - Type of variation: 'music', 'background', 'context', or 'all'
* @returns {Object} New configuration with variation
*/
function getVariation(config, variationType) {
if (!config) return null;
// Deep clone the config
var newConfig = JSON.parse(JSON.stringify(config));
switch (variationType) {
case 'music':
if (newConfig.music && window.MusicEngine && window.MusicEngine.findSongs) {
var altSongs = window.MusicEngine.findSongs({ mood: config.music.mood });
if (altSongs.length > 1) {
// Pick a random different song
var filtered = altSongs.filter(function(s) {
return s.id !== config.music.songId && s.songId !== config.music.songId;
});
if (filtered.length > 0) {
var picked = filtered[Math.floor(Math.random() * filtered.length)];
newConfig.music = {
mood: picked.mood || config.music.mood,
energy: picked.energy || config.music.energy,
songId: picked.id || picked.songId,
tags: picked.tags
};
}
}
}
break;
case 'background':
if (newConfig.background && window.BackgroundEngine && window.BackgroundEngine.getVariants) {
var variants = window.BackgroundEngine.getVariants(config.background.theme);
if (variants && variants.length > 1) {
var currentIdx = variants.indexOf(config.background.variant);
var nextIdx = (currentIdx + 1) % variants.length;
newConfig.background = Object.assign({}, config.background, {
variant: variants[nextIdx]
});
}
}
break;
case 'context':
if (newConfig.avatarContext && window.ContextCatalog && window.ContextCatalog.getByMode) {
var contexts = window.ContextCatalog.getByMode(config.avatarContext.mode);
if (contexts && contexts.length > 1) {
var filtered = contexts.filter(function(c) {
return c.id !== config.avatarContext.context;
});
if (filtered.length > 0) {
var alt = filtered[Math.floor(Math.random() * filtered.length)];
newConfig.avatarContext = Object.assign({}, config.avatarContext, {
context: alt.id
});
}
}
}
break;
case 'all':
default:
// Apply all variations
newConfig = getVariation(newConfig, 'music');
newConfig = getVariation(newConfig, 'background');
newConfig = getVariation(newConfig, 'context');
break;
}
// Clear preset info since this is now a custom combination
if (variationType) {
newConfig.presetId = null;
newConfig.name = (config.name || 'Custom') + ' (variation)';
}
return newConfig;
}
// =========================================================================
// SEARCH
// =========================================================================
/**
* Search for assets across all subsystems
* @param {string} query - Search query
* @returns {Object} Search results by category
*/
function searchAssets(query) {
if (!query || typeof query !== 'string') {
return { presets: [], music: [], backgrounds: [], contexts: [] };
}
var normalizedQuery = query.toLowerCase().trim();
var results = {
presets: [],
music: [],
backgrounds: [],
contexts: []
};
// Search presets
if (window.AssetPresets) {
if (window.AssetPresets.search) {
results.presets = window.AssetPresets.search(normalizedQuery);
} else if (window.AssetPresets.getAll) {
// Manual search fallback
results.presets = window.AssetPresets.getAll().filter(function(p) {
return (p.name && p.name.toLowerCase().includes(normalizedQuery)) ||
(p.description && p.description.toLowerCase().includes(normalizedQuery)) ||
(p.tags && p.tags.some(function(t) { return t.toLowerCase().includes(normalizedQuery); }));
});
}
}
// Search music
if (window.MusicEngine) {
if (window.MusicEngine.findSongs) {
results.music = window.MusicEngine.findSongs({ tags: [normalizedQuery] });
}
// Also search by mood
if (results.music.length === 0 && window.MusicEngine.findSongs) {
results.music = window.MusicEngine.findSongs({ mood: normalizedQuery });
}
}
// Search backgrounds
if (window.BackgroundEngine) {
if (window.BackgroundEngine.find) {
results.backgrounds = window.BackgroundEngine.find({ tags: [normalizedQuery] });
} else if (window.BackgroundEngine.getThemes) {
// Manual search fallback
var themes = window.BackgroundEngine.getThemes();
results.backgrounds = themes.filter(function(t) {
return t.toLowerCase().includes(normalizedQuery);
}).map(function(t) {
return { theme: t };
});
}
}
// Search contexts
if (window.ContextCatalog) {
if (window.ContextCatalog.search) {
results.contexts = window.ContextCatalog.search(normalizedQuery);
} else if (window.ContextCatalog.getAll) {
// Manual search fallback
results.contexts = window.ContextCatalog.getAll().filter(function(c) {
return (c.id && c.id.toLowerCase().includes(normalizedQuery)) ||
(c.name && c.name.toLowerCase().includes(normalizedQuery)) ||
(c.tags && c.tags.some(function(t) { return t.toLowerCase().includes(normalizedQuery); }));
});
}
}
return results;
}
// =========================================================================
// RANDOM COMBINATION
// =========================================================================
/**
* Get a random asset combination, optionally filtered by game style
* @param {string} gameStyle - Optional game style filter
* @returns {Object} Random configuration
*/
function getRandomCombination(gameStyle) {
var presets = [];
if (window.AssetPresets) {
if (gameStyle && window.AssetPresets.getByGameStyle) {
presets = window.AssetPresets.getByGameStyle(gameStyle);
}
if (presets.length === 0 && window.AssetPresets.getAll) {
presets = window.AssetPresets.getAll();
}
}
if (presets.length > 0) {
var preset = presets[Math.floor(Math.random() * presets.length)];
return presetToConfig(preset);
}
// Fallback: build random combination from individual subsystems
var config = {};
// Random music
if (window.MusicEngine && window.MusicEngine.getCatalog) {
var songs = window.MusicEngine.getCatalog();
if (songs.length > 0) {
var song = songs[Math.floor(Math.random() * songs.length)];
config.music = {
mood: song.mood,
energy: song.energy,
songId: song.id || song.songId
};
}
}
// Random background
if (window.BackgroundEngine && window.BackgroundEngine.getThemes) {
var themes = window.BackgroundEngine.getThemes();
if (themes.length > 0) {
var theme = themes[Math.floor(Math.random() * themes.length)];
var variants = window.BackgroundEngine.getVariants ? window.BackgroundEngine.getVariants(theme) : ['default'];
var variant = variants[Math.floor(Math.random() * variants.length)];
config.background = {
theme: theme,
variant: variant
};
}
}
// Random context
if (window.ContextCatalog && window.ContextCatalog.getAll) {
var contexts = window.ContextCatalog.getAll();
if (contexts.length > 0) {
var context = contexts[Math.floor(Math.random() * contexts.length)];
config.avatarContext = {
mode: context.mode || 'FULL_BODY',
context: context.id
};
}
}
config.name = 'Random Combination';
config.description = 'Randomly generated asset combination';
return config;
}
// =========================================================================
// CACHE MANAGEMENT
// =========================================================================
/**
* Clear the asset cache
* @param {string} key - Optional specific key to clear
*/
function clearCache(key) {
if (key) {
cache.delete(key);
} else {
cache.clear();
}
}
/**
* Get cached assets
* @param {string} key - Cache key (usually presetId)
* @returns {Object|undefined} Cached assets
*/
function getCached(key) {
return cache.get(key);
}
// =========================================================================
// UTILITY FUNCTIONS
// =========================================================================
/**
* Get statistics about available assets
* @returns {Object} Asset statistics
*/
function getStats() {
var stats = {
initialized: initialized,
cache: {
size: cache.size
},
subsystems: {}
};
if (window.MusicEngine) {
stats.subsystems.music = {
available: true,
songs: window.MusicEngine.getCatalog ? window.MusicEngine.getCatalog().length : 'unknown'
};
}
if (window.BackgroundEngine) {
stats.subsystems.backgrounds = {
available: true,
count: window.BackgroundEngine.getTotalCount ? window.BackgroundEngine.getTotalCount() : 'unknown'
};
}
if (window.AvatarSystem) {
stats.subsystems.avatars = {
available: true
};
}
if (window.ContextCatalog) {
stats.subsystems.contexts = {
available: true,
count: window.ContextCatalog.getAll ? window.ContextCatalog.getAll().length : 'unknown'
};
}
if (window.AssetPresets) {
stats.subsystems.presets = {
available: true,
count: window.AssetPresets.getAll ? window.AssetPresets.getAll().length : 'unknown'
};
}
return stats;
}
// =========================================================================
// PUBLIC API
// =========================================================================
window.AssetManager = {
// Initialization
initialize: initialize,
isInitialized: function() { return initialized; },
// Recommendations
getRecommendations: getRecommendations,
// Detection helpers (exposed for testing/debugging)
detectTheme: detectTheme,
detectMood: detectMood,
detectMechanics: detectMechanics,
parseKeywords: parseKeywords,
// Preset lookup
findPreset: findPreset,
presetToConfig: presetToConfig,
// Asset loading
loadGameAssets: loadGameAssets,
getLoadedAssets: function() { return loadedAssets; },
// Validation
validateCombination: validateCombination,
// Description
describeAssets: describeAssets,
// Variations
getVariation: getVariation,
// Search
searchAssets: searchAssets,
// Random
getRandomCombination: getRandomCombination,
// Cache management
clearCache: clearCache,
getCached: getCached,
// Statistics
getStats: getStats,
// Direct access to subsystems
music: function() { return window.MusicEngine; },
backgrounds: function() { return window.BackgroundEngine; },
avatars: function() { return window.AvatarSystem; },
contexts: function() { return window.ContextRenderer; },
catalog: function() { return window.AssetCatalog; },
presets: function() { return window.AssetPresets; },
compatibility: function() { return window.AssetCompatibility; }
};
})();