@@ -110,6 +110,8 @@ class Live2DManager {
110110 this . isLocked = true ;
111111 this . onModelLoaded = null ;
112112 this . onStatusUpdate = null ;
113+ this . modelName = null ; // 记录当前模型目录名
114+ this . modelRootPath = null ; // 记录当前模型根路径,如 /static/<modelName>
113115 }
114116
115117 // 初始化 PIXI 应用
@@ -201,41 +203,31 @@ class Live2DManager {
201203 return ;
202204 }
203205
204- const expressions = this . emotionMapping . Expressions . filter ( e => e . Name . startsWith ( emotion ) ) ;
206+ const expressions = this . emotionMapping . Expressions . filter ( e => ( e . Name || '' ) . startsWith ( emotion ) ) ;
205207 if ( ! expressions || expressions . length === 0 ) {
206208 console . log ( `未找到情感 ${ emotion } 对应的表情,将跳过表情播放` ) ;
207209 return ; // Gracefully exit if no expression is found
208210 }
209211
210- const expressionFile = this . getRandomElement ( expressions ) . File . split ( '/' ) . pop ( ) ;
211- if ( ! expressionFile ) return ;
212+ const choice = this . getRandomElement ( expressions ) ;
213+ if ( ! choice || ! choice . File ) return ;
212214
213215 try {
214- // 获取模型名称(从模型路径中提取)
215- let modelName = 'mao_pro' ; // 默认模型名称
216-
217- // 尝试从模型路径中提取模型名称
218- if ( this . currentModel . internalModel && this . currentModel . internalModel . settings && this . currentModel . internalModel . settings . model ) {
219- const modelPath = this . currentModel . internalModel . settings . model ;
220- const pathParts = modelPath . split ( '/' ) ;
221- modelName = pathParts [ pathParts . length - 2 ] || pathParts [ pathParts . length - 1 ] . replace ( '.model3.json' , '' ) ;
222- }
223-
224- // 加载表情文件并应用参数
225- const expressionPath = `/static/${ modelName } /expressions/${ expressionFile } ` ;
216+ // 计算表达文件路径(相对模型根目录)
217+ const expressionPath = this . resolveAssetPath ( choice . File ) ;
226218 const response = await fetch ( expressionPath ) ;
227219 if ( ! response . ok ) {
228220 throw new Error ( `Failed to load expression: ${ response . statusText } ` ) ;
229221 }
230222
231223 const expressionData = await response . json ( ) ;
232- console . log ( `加载表情文件: ${ expressionFile } ` , expressionData ) ;
224+ console . log ( `加载表情文件: ${ choice . File } ` , expressionData ) ;
233225
234226 // 方法1: 尝试使用原生expression API
235227 if ( this . currentModel . expression ) {
236228 try {
237- // 从文件名中提取expression名称(去掉.exp3.json后缀 )
238- const expressionName = expressionFile . replace ( '.exp3.json' , '' ) ;
229+ // 直接使用配置中的 Name(我们保存时已包含情感前缀 )
230+ const expressionName = choice . Name || choice . File . replace ( '.exp3.json' , '' ) ;
239231 console . log ( `尝试使用原生API播放expression: ${ expressionName } ` ) ;
240232
241233 const expression = await this . currentModel . expression ( expressionName ) ;
@@ -286,20 +278,10 @@ class Live2DManager {
286278 return ;
287279 }
288280
289- const motionFile = this . getRandomElement ( motions ) . File . split ( '/' ) . pop ( ) ;
290- if ( ! motionFile ) return ;
281+ const choice = this . getRandomElement ( motions ) ;
282+ if ( ! choice || ! choice . File ) return ;
291283
292284 try {
293- // 获取模型名称(从模型路径中提取)
294- let modelName = 'mao_pro' ; // 默认模型名称
295-
296- // 尝试从模型路径中提取模型名称
297- if ( this . currentModel . internalModel && this . currentModel . internalModel . settings && this . currentModel . internalModel . settings . model ) {
298- const modelPath = this . currentModel . internalModel . settings . model ;
299- const pathParts = modelPath . split ( '/' ) ;
300- modelName = pathParts [ pathParts . length - 2 ] || pathParts [ pathParts . length - 1 ] . replace ( '.model3.json' , '' ) ;
301- }
302-
303285 // 清除之前的动作定时器
304286 if ( this . motionTimer ) {
305287 console . log ( '检测到前一个motion正在播放,正在停止...' ) ;
@@ -326,14 +308,14 @@ class Live2DManager {
326308
327309 // 尝试使用Live2D模型的原生motion播放功能
328310 try {
329- // 构建完整的motion路径
330- const motionPath = `/static/ ${ modelName } /motions/ ${ motionFile } ` ;
311+ // 构建完整的motion路径(相对模型根目录)
312+ const motionPath = this . resolveAssetPath ( choice . File ) ;
331313 console . log ( `尝试播放motion: ${ motionPath } ` ) ;
332314
333315 // 方法1: 直接使用模型的motion播放功能
334316 if ( this . currentModel . motion ) {
335317 try {
336- console . log ( `尝试播放motion: ${ motionFile } ` ) ;
318+ console . log ( `尝试播放motion: ${ choice . File } ` ) ;
337319
338320 // 使用情感名称作为motion组名,这样可以确保播放正确的motion
339321 console . log ( `尝试使用情感组播放motion: ${ emotion } ` ) ;
@@ -363,7 +345,7 @@ class Live2DManager {
363345
364346 // 设置定时器在motion结束后清理
365347 this . motionTimer = setTimeout ( ( ) => {
366- console . log ( `motion播放完成(预期文件: ${ motionFile } )` ) ;
348+ console . log ( `motion播放完成(预期文件: ${ choice . File } )` ) ;
367349 this . motionTimer = null ;
368350 this . clearEmotionEffects ( ) ;
369351 } , motionDuration ) ;
@@ -387,7 +369,7 @@ class Live2DManager {
387369 }
388370
389371 // 如果所有方法都失败,回退到简单动作
390- console . warn ( `无法播放motion: ${ motionFile } ,回退到简单动作` ) ;
372+ console . warn ( `无法播放motion: ${ choice . File } ,回退到简单动作` ) ;
391373 this . playSimpleMotion ( emotion ) ;
392374
393375 } catch ( error ) {
@@ -605,6 +587,21 @@ class Live2DManager {
605587 const model = await Live2DModel . from ( modelPath , { autoInteract : false } ) ;
606588 this . currentModel = model ;
607589
590+ // 解析模型目录名与根路径,供资源解析使用
591+ try {
592+ const cleanPath = ( modelPath || '' ) . split ( '#' ) [ 0 ] . split ( '?' ) [ 0 ] ;
593+ const lastSlash = cleanPath . lastIndexOf ( '/' ) ;
594+ const rootDir = lastSlash >= 0 ? cleanPath . substring ( 0 , lastSlash ) : '/static' ;
595+ this . modelRootPath = rootDir ; // e.g. /static/mao_pro or /static/some/deeper/dir
596+ const parts = rootDir . split ( '/' ) . filter ( Boolean ) ;
597+ this . modelName = parts . length > 0 ? parts [ parts . length - 1 ] : null ;
598+ console . log ( '模型根路径解析:' , { modelPath, modelName : this . modelName , modelRootPath : this . modelRootPath } ) ;
599+ } catch ( e ) {
600+ console . warn ( '解析模型根路径失败,将使用默认值' , e ) ;
601+ this . modelRootPath = '/static' ;
602+ this . modelName = null ;
603+ }
604+
608605 // 配置渲染纹理数量以支持更多蒙版
609606 if ( model . internalModel && model . internalModel . renderer && model . internalModel . renderer . _clippingManager ) {
610607 model . internalModel . renderer . _clippingManager . _renderTextureCount = 3 ;
@@ -666,6 +663,19 @@ class Live2DManager {
666663 }
667664 }
668665
666+ // 解析资源相对路径(基于当前模型根目录)
667+ resolveAssetPath ( relativePath ) {
668+ if ( ! relativePath ) return '' ;
669+ let rel = String ( relativePath ) . replace ( / ^ [ \\ / ] + / , '' ) ;
670+ if ( rel . startsWith ( 'static/' ) ) {
671+ return `/${ rel } ` ;
672+ }
673+ if ( rel . startsWith ( '/static/' ) ) {
674+ return rel ;
675+ }
676+ return `${ this . modelRootPath } /${ rel } ` ;
677+ }
678+
669679 // 应用模型设置
670680 applyModelSettings ( model , options ) {
671681 const { preferences, isMobile = false } = options ;
0 commit comments