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