+ /**
+ * @fileoverview Multi-scheme animation name mapper for Owen Animation System
+ * @module animation/AnimationNameMapper
+ */
+
+/**
+ * Multi-scheme animation name mapper for Owen Animation System
+ * Supports legacy, artist-friendly, and hierarchical naming schemes
+ * @class
+ */
+export class AnimationNameMapper {
+ constructor () {
+ // Mapping between different naming schemes
+ this.schemeMappings = new Map()
+ this.reverseMappings = new Map()
+ this.patterns = new Map()
+
+ this.initializeMappings()
+ }
+
+ /**
+ * Initialize all naming scheme mappings and patterns
+ * @private
+ */
+ initializeMappings () {
+ // Core animation definitions with all naming scheme variants
+ const animations = [
+ // Wait state animations
+ {
+ legacy: 'wait_idle_L',
+ artist: 'Owen_WaitIdle',
+ hierarchical: 'owen.state.wait.idle.loop',
+ semantic: 'OwenWaitIdleLoop',
+ state: 'wait',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'wait_pickNose_Q',
+ artist: 'Owen_PickNose',
+ hierarchical: 'owen.quirk.wait.picknose',
+ semantic: 'OwenQuirkPickNose',
+ state: 'wait',
+ emotion: '',
+ type: 'quirk',
+ category: 'quirk'
+ },
+ {
+ legacy: 'wait_wave_Q',
+ artist: 'Owen_Wave',
+ hierarchical: 'owen.quirk.wait.wave',
+ semantic: 'OwenQuirkWave',
+ state: 'wait',
+ emotion: '',
+ type: 'quirk',
+ category: 'quirk'
+ },
+ // React state animations
+ {
+ legacy: 'react_idle_L',
+ artist: 'Owen_ReactIdle',
+ hierarchical: 'owen.state.react.idle.loop',
+ semantic: 'OwenReactIdleLoop',
+ state: 'react',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_an_L',
+ artist: 'Owen_ReactAngry',
+ hierarchical: 'owen.state.react.emotion.angry.loop',
+ semantic: 'OwenReactAngryLoop',
+ state: 'react',
+ emotion: 'angry',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_sh_L',
+ artist: 'Owen_ReactShocked',
+ hierarchical: 'owen.state.react.emotion.shocked.loop',
+ semantic: 'OwenReactShockedLoop',
+ state: 'react',
+ emotion: 'shocked',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_ha_L',
+ artist: 'Owen_ReactHappy',
+ hierarchical: 'owen.state.react.emotion.happy.loop',
+ semantic: 'OwenReactHappyLoop',
+ state: 'react',
+ emotion: 'happy',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_sa_L',
+ artist: 'Owen_ReactSad',
+ hierarchical: 'owen.state.react.emotion.sad.loop',
+ semantic: 'OwenReactSadLoop',
+ state: 'react',
+ emotion: 'sad',
+ type: 'loop',
+ category: 'state'
+ },
+ // Type state animations
+ {
+ legacy: 'type_idle_L',
+ artist: 'Owen_TypeIdle',
+ hierarchical: 'owen.state.type.idle.loop',
+ semantic: 'OwenTypeIdleLoop',
+ state: 'type',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'type_an_L',
+ artist: 'Owen_TypeAngry',
+ hierarchical: 'owen.state.type.emotion.angry.loop',
+ semantic: 'OwenTypeAngryLoop',
+ state: 'type',
+ emotion: 'angry',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'type_sh_L',
+ artist: 'Owen_TypeShocked',
+ hierarchical: 'owen.state.type.emotion.shocked.loop',
+ semantic: 'OwenTypeShockedLoop',
+ state: 'type',
+ emotion: 'shocked',
+ type: 'loop',
+ category: 'state'
+ },
+ // Sleep state animations
+ {
+ legacy: 'sleep_idle_L',
+ artist: 'Owen_SleepIdle',
+ hierarchical: 'owen.state.sleep.idle.loop',
+ semantic: 'OwenSleepIdleLoop',
+ state: 'sleep',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ // Transition animations
+ {
+ legacy: 'wait_2react_T',
+ artist: 'Owen_WaitToReact',
+ hierarchical: 'owen.transition.wait.to.react',
+ semantic: 'OwenTransitionWaitToReact',
+ fromState: 'wait',
+ toState: 'react',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'react_2type_T',
+ artist: 'Owen_ReactToType',
+ hierarchical: 'owen.transition.react.to.type',
+ semantic: 'OwenTransitionReactToType',
+ fromState: 'react',
+ toState: 'type',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'react_an2type_T',
+ artist: 'Owen_ReactAngryToType',
+ hierarchical: 'owen.transition.react.to.type.emotion.angry',
+ semantic: 'OwenTransitionReactToTypeAngry',
+ fromState: 'react',
+ toState: 'type',
+ emotion: 'angry',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'type_2wait_T',
+ artist: 'Owen_TypeToWait',
+ hierarchical: 'owen.transition.type.to.wait',
+ semantic: 'OwenTransitionTypeToWait',
+ fromState: 'type',
+ toState: 'wait',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'sleep_2wait_T',
+ artist: 'Owen_SleepToWait',
+ hierarchical: 'owen.transition.sleep.to.wait',
+ semantic: 'OwenTransitionSleepToWait',
+ fromState: 'sleep',
+ toState: 'wait',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ }
+ ]
+
+ // Build bidirectional mappings
+ animations.forEach(anim => {
+ const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
+
+ schemes.forEach(scheme1 => {
+ schemes.forEach(scheme2 => {
+ if (scheme1 !== scheme2) {
+ this.schemeMappings.set(anim[scheme1], anim[scheme2])
+ }
+ })
+
+ // Also map to animation definition
+ this.schemeMappings.set(anim[scheme1], anim)
+ })
+ })
+
+ // Initialize pattern matchers for auto-detection
+ this.initializePatterns()
+ }
+
+ /**
+ * Initialize pattern matchers for different naming schemes
+ * @private
+ */
+ initializePatterns () {
+ // Pattern matchers for different naming schemes
+ this.patterns.set('legacy', [
+ {
+ regex: /^(\w+)_(\w+)_([LTQ])$/,
+ extract: (match) => ({
+ state: match[1],
+ action: match[2],
+ type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk'
+ })
+ },
+ {
+ regex: /^(\w+)_(\w{2})_([LTQ])$/,
+ extract: (match) => ({
+ state: match[1],
+ emotion: this.mapEmotionCode(match[2]),
+ type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk'
+ })
+ },
+ {
+ regex: /^(\w+)_(\w{2})?2(\w+)_T$/,
+ extract: (match) => ({
+ fromState: match[1],
+ emotion: match[2] ? this.mapEmotionCode(match[2]) : '',
+ toState: match[3],
+ type: 'transition'
+ })
+ },
+ {
+ regex: /^(\w+)_2(\w+)_T$/,
+ extract: (match) => ({
+ fromState: match[1],
+ toState: match[2],
+ type: 'transition'
+ })
+ }
+ ])
+
+ this.patterns.set('artist', [
+ {
+ regex: /^Owen_(\w+)$/,
+ extract: (match) => ({
+ action: match[1],
+ scheme: 'artist'
+ })
+ },
+ {
+ regex: /^Owen_(\w+)To(\w+)$/,
+ extract: (match) => ({
+ fromState: match[1].toLowerCase(),
+ toState: match[2].toLowerCase(),
+ type: 'transition'
+ })
+ },
+ {
+ regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)$/,
+ extract: (match) => ({
+ state: match[1].toLowerCase(),
+ emotion: match[2].toLowerCase(),
+ type: 'loop'
+ })
+ },
+ {
+ regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)To(\w+)$/,
+ extract: (match) => ({
+ fromState: match[1].toLowerCase(),
+ emotion: match[2].toLowerCase(),
+ toState: match[3].toLowerCase(),
+ type: 'transition'
+ })
+ }
+ ])
+
+ this.patterns.set('hierarchical', [
+ {
+ regex: /^owen\.(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?(?:\.(\w+))?$/,
+ extract: (match) => ({
+ category: match[1],
+ subcategory: match[2],
+ action: match[3],
+ modifier: match[4],
+ type: match[5] || match[4]
+ })
+ }
+ ])
+
+ this.patterns.set('semantic', [
+ {
+ regex: /^Owen(\w+)(\w+)(\w+)$/,
+ extract: (match) => ({
+ category: match[1].toLowerCase(),
+ action: match[2].toLowerCase(),
+ type: match[3].toLowerCase()
+ })
+ }
+ ])
+ }
+
+ /**
+ * Map emotion codes to full names
+ * @private
+ * @param {string} code - Emotion code
+ * @returns {string} Full emotion name
+ */
+ mapEmotionCode (code) {
+ const emotionMap = {
+ an: 'angry',
+ sh: 'shocked',
+ ha: 'happy',
+ sa: 'sad',
+ '': 'neutral'
+ }
+ return emotionMap[code] || code
+ }
+
+ /**
+ * Convert any animation name to any other scheme
+ * @param {string} fromName - Source animation name
+ * @param {string} targetScheme - Target naming scheme ('legacy', 'artist', 'hierarchical', 'semantic')
+ * @returns {string} Converted animation name
+ */
+ convert (fromName, targetScheme = 'hierarchical') {
+ // Direct lookup first
+ const directMapping = this.schemeMappings.get(fromName)
+ if (directMapping && typeof directMapping === 'object') {
+ return directMapping[targetScheme] || fromName
+ }
+
+ // Pattern-based conversion
+ const detected = this.detectScheme(fromName)
+ if (detected) {
+ return this.generateName(detected.info, targetScheme)
+ }
+
+ console.warn(`Could not convert animation name: ${fromName}`)
+ return fromName
+ }
+
+ /**
+ * Detect which naming scheme is being used
+ * @param {string} name - Animation name to analyze
+ * @returns {Object|null} Detection result with scheme and extracted info
+ */
+ detectScheme (name) {
+ for (const [scheme, patterns] of this.patterns) {
+ for (const pattern of patterns) {
+ const match = name.match(pattern.regex)
+ if (match) {
+ return {
+ scheme,
+ info: pattern.extract(match),
+ originalName: name
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ /**
+ * Generate animation name in target scheme
+ * @private
+ * @param {Object} info - Animation information
+ * @param {string} targetScheme - Target naming scheme
+ * @returns {string} Generated animation name
+ */
+ generateName (info, targetScheme) {
+ switch (targetScheme) {
+ case 'legacy':
+ return this.generateLegacyName(info)
+ case 'artist':
+ return this.generateArtistName(info)
+ case 'hierarchical':
+ return this.generateHierarchicalName(info)
+ case 'semantic':
+ return this.generateSemanticName(info)
+ default:
+ return null
+ }
+ }
+
+ /**
+ * Generate legacy format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Legacy format name
+ */
+ generateLegacyName (info) {
+ const typeMap = { loop: 'L', transition: 'T', quirk: 'Q' }
+ const emotionMap = { angry: 'an', shocked: 'sh', happy: 'ha', sad: 'sa' }
+
+ if (info.type === 'transition' && info.fromState && info.toState) {
+ const emotionPart = info.emotion ? emotionMap[info.emotion] || '' : ''
+ return emotionPart
+ ? `${info.fromState}_${emotionPart}2${info.toState}_T`
+ : `${info.fromState}_2${info.toState}_T`
+ }
+
+ const state = info.state || info.fromState || 'wait'
+ const action = info.action || (info.emotion ? emotionMap[info.emotion] : 'idle')
+ const type = typeMap[info.type] || 'L'
+
+ return `${state}_${action}_${type}`
+ }
+
+ /**
+ * Generate artist-friendly format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Artist format name
+ */
+ generateArtistName (info) {
+ const parts = ['Owen']
+
+ if (info.type === 'transition') {
+ const from = this.capitalize(info.fromState || info.state)
+ const to = this.capitalize(info.toState)
+ if (info.emotion) {
+ parts.push(`${from}${this.capitalize(info.emotion)}To${to}`)
+ } else {
+ parts.push(`${from}To${to}`)
+ }
+ } else {
+ if (info.state) parts.push(this.capitalize(info.state))
+ if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action))
+ if (info.emotion) parts.push(this.capitalize(info.emotion))
+ }
+
+ return parts.join('_')
+ }
+
+ /**
+ * Generate hierarchical format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Hierarchical format name
+ */
+ generateHierarchicalName (info) {
+ const parts = ['owen']
+
+ if (info.category) {
+ parts.push(info.category)
+ } else if (info.type === 'transition') {
+ parts.push('transition')
+ } else if (info.type === 'quirk') {
+ parts.push('quirk')
+ } else {
+ parts.push('state')
+ }
+
+ if (info.fromState && info.toState) {
+ // Transition
+ parts.push(info.fromState, 'to', info.toState)
+ } else if (info.state) {
+ parts.push(info.state)
+ }
+
+ if (info.action && info.action !== 'idle') parts.push(info.action)
+ if (info.emotion) parts.push('emotion', info.emotion)
+ if (info.type) parts.push(info.type)
+
+ return parts.join('.')
+ }
+
+ /**
+ * Generate semantic format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Semantic format name
+ */
+ generateSemanticName (info) {
+ const parts = ['Owen']
+
+ if (info.type === 'transition') {
+ parts.push('Transition')
+ if (info.fromState) parts.push(this.capitalize(info.fromState))
+ parts.push('To')
+ if (info.toState) parts.push(this.capitalize(info.toState))
+ if (info.emotion) parts.push(this.capitalize(info.emotion))
+ } else {
+ if (info.type === 'quirk') parts.push('Quirk')
+ if (info.state) parts.push(this.capitalize(info.state))
+ if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action))
+ if (info.emotion) parts.push(this.capitalize(info.emotion))
+ if (info.type && info.type !== 'quirk') parts.push(this.capitalize(info.type))
+ }
+
+ return parts.join('')
+ }
+
+ /**
+ * Capitalize first letter of string
+ * @private
+ * @param {string} str - String to capitalize
+ * @returns {string} Capitalized string
+ */
+ capitalize (str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+ }
+
+ /**
+ * Get all possible names for an animation
+ * @param {string} animationName - Source animation name
+ * @returns {Object} Object with all naming scheme variants
+ */
+ getAllNames (animationName) {
+ const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
+ const names = {}
+
+ schemes.forEach(scheme => {
+ names[scheme] = this.convert(animationName, scheme)
+ })
+
+ return names
+ }
+
+ /**
+ * Batch convert multiple animations
+ * @param {string[]} animations - Array of animation names
+ * @param {string} targetScheme - Target naming scheme
+ * @returns {Object} Mapping of original names to converted names
+ */
+ convertBatch (animations, targetScheme) {
+ const converted = {}
+ animations.forEach(name => {
+ converted[name] = this.convert(name, targetScheme)
+ })
+ return converted
+ }
+
+ /**
+ * Validate animation name format
+ * @param {string} name - Animation name to validate
+ * @returns {Object} Validation result with issues and suggestions
+ */
+ validateAnimationName (name) {
+ const issues = []
+ const suggestions = []
+
+ // Check for common issues
+ if (name.includes(' ')) {
+ issues.push(`❌ "${name}" contains spaces - may cause issues`)
+ suggestions.push(`💡 Suggestion: "${name.replace(/ /g, '_')}"`)
+ }
+
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
+ issues.push(`❌ "${name}" contains invalid characters`)
+ suggestions.push('💡 Use only letters, numbers, dots, underscores, and hyphens')
+ }
+
+ if (name.length > 50) {
+ issues.push(`⚠️ "${name}" is very long (${name.length} chars)`)
+ suggestions.push('💡 Consider shortening the name')
+ }
+
+ const detected = this.detectScheme(name)
+ if (!detected) {
+ issues.push(`⚠️ "${name}" doesn't match any known naming pattern`)
+ suggestions.push('💡 Consider using one of: legacy, artist, hierarchical, or semantic format')
+ } else {
+ suggestions.push(`✅ Detected as ${detected.scheme} scheme`)
+ }
+
+ return { issues, suggestions, detected }
+ }
+}
+
+
+