@@ -349,9 +349,11 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
349349 const index = Math . floor ( Math . random ( ) * groupList . length ) ;
350350
351351 try {
352- const motion = await this . currentModel . motion ( group , index ) ;
352+ // 使用低优先级 IDLE=1 播放动作,这样不会覆盖对话等高优先级动作
353+ // pixi-live2d-display 的 motion(group, index, priority) 支持优先级参数
354+ const motion = await this . currentModel . motion ( group , index , CLICK_MOTION_PRIORITY ) ;
353355 if ( motion ) {
354- console . log ( `[Interaction] 教程模式 - 播放动作: ${ group } [${ index } ]` ) ;
356+ console . log ( `[Interaction] 教程模式 - 播放动作: ${ group } [${ index } ](优先级: ${ CLICK_MOTION_PRIORITY } ) ` ) ;
355357 return true ;
356358 }
357359 } catch ( error ) {
@@ -361,11 +363,21 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
361363 return false ;
362364 } ;
363365
364- // 点击触发随机表情和动作
366+ // 点击触发随机表情和动作(低优先级,会自动恢复)
367+ // 使用最低优先级 IDLE=1,确保不会覆盖对话等高优先级动作
368+ const CLICK_MOTION_PRIORITY = 1 ; // IDLE priority
369+ const CLICK_EFFECT_DURATION = 3000 ; // 点击效果持续时间(毫秒)
370+
365371 const triggerRandomEmotion = async ( ) => {
372+ // 清除之前的点击效果恢复定时器
373+ if ( this . _clickEffectRestoreTimer ) {
374+ clearTimeout ( this . _clickEffectRestoreTimer ) ;
375+ this . _clickEffectRestoreTimer = null ;
376+ }
377+
366378 // 教程模式:直接随机播放表情
367379 if ( window . isInTutorial ) {
368- console . log ( '[Interaction] 教程模式 - 随机播放表情' ) ;
380+ console . log ( '[Interaction] 教程模式 - 随机播放表情(低优先级,将自动恢复) ' ) ;
369381 try {
370382 // 获取表情列表
371383 let expressionNames = [ ] ;
@@ -376,7 +388,7 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
376388 // 随机播放表情
377389 if ( expressionNames . length > 0 ) {
378390 const randomExpression = expressionNames [ Math . floor ( Math . random ( ) * expressionNames . length ) ] ;
379- console . log ( `[Interaction] 教程模式 - 播放表情: ${ randomExpression } ` ) ;
391+ console . log ( `[Interaction] 教程模式 - 播放表情: ${ randomExpression } (将在 ${ CLICK_EFFECT_DURATION } ms 后恢复) ` ) ;
380392 await this . currentModel . expression ( randomExpression ) ;
381393
382394 const playedMotion = await playTutorialMotion ( ) ;
@@ -425,14 +437,36 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
425437 console . log ( '[Interaction] 教程模式 - 播放参数动画' ) ;
426438 }
427439 }
440+
441+ // 设置恢复定时器:在效果持续时间后清除表情,恢复到常驻/默认状态
442+ // 使用唯一 ID 标记此次点击效果,用于判断是否应该恢复
443+ const clickEffectId = Date . now ( ) ;
444+ this . _currentClickEffectId = clickEffectId ;
445+
446+ this . _clickEffectRestoreTimer = setTimeout ( ( ) => {
447+ this . _clickEffectRestoreTimer = null ;
448+
449+ // 检查是否仍然是此次点击效果(没有被新的情感/点击覆盖)
450+ if ( this . _currentClickEffectId !== clickEffectId ) {
451+ console . log ( '[Interaction] 点击效果已被新的情感覆盖,跳过恢复' ) ;
452+ return ;
453+ }
454+
455+ console . log ( '[Interaction] 点击效果持续时间结束,恢复到默认状态' ) ;
456+ this . _currentClickEffectId = null ;
457+ // 清除表情,恢复到常驻表情或默认状态
458+ if ( this . clearExpression ) {
459+ this . clearExpression ( ) ;
460+ }
461+ } , CLICK_EFFECT_DURATION ) ;
428462 }
429463 } catch ( error ) {
430464 console . warn ( '[Interaction] 教程模式播放表情失败:' , error ) ;
431465 }
432466 return ;
433467 }
434468
435- // 正常模式:使用情感系统
469+ // 正常模式:使用情感系统(但使用临时触发,会自动恢复)
436470 if ( ! this . emotionMapping ) {
437471 console . log ( '[Interaction] 没有情感映射配置,跳过点击触发' ) ;
438472 return ;
@@ -453,11 +487,12 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
453487
454488 // 随机选择一个情感
455489 const randomEmotion = availableEmotions [ Math . floor ( Math . random ( ) * availableEmotions . length ) ] ;
456- console . log ( `[Interaction] 点击触发随机情感: ${ randomEmotion } ` ) ;
490+ console . log ( `[Interaction] 点击触发随机情感: ${ randomEmotion } (低优先级,将自动恢复) ` ) ;
457491
458- // 触发情感
492+ // 触发临时情感效果
459493 try {
460- await this . setEmotion ( randomEmotion ) ;
494+ // 播放低优先级的表情和动作
495+ await this . _playTemporaryClickEffect ( randomEmotion , CLICK_MOTION_PRIORITY , CLICK_EFFECT_DURATION ) ;
461496 } catch ( error ) {
462497 console . warn ( '[Interaction] 触发情感失败:' , error ) ;
463498 }
@@ -1015,6 +1050,124 @@ Live2DManager.prototype.enableMouseTracking = function (model, options = {}) {
10151050 } , 100 ) ;
10161051} ;
10171052
1053+ /**
1054+ * 播放临时点击效果(低优先级,会自动恢复)
1055+ * @param {string } emotion - 情感名称
1056+ * @param {number } priority - 动作优先级 (1=IDLE, 2=NORMAL, 3=FORCE)
1057+ * @param {number } duration - 效果持续时间(毫秒)
1058+ */
1059+ Live2DManager . prototype . _playTemporaryClickEffect = async function ( emotion , priority = 1 , duration = 3000 ) {
1060+ if ( ! this . currentModel ) {
1061+ console . warn ( '[ClickEffect] 无法播放:模型未加载' ) ;
1062+ return ;
1063+ }
1064+
1065+ // 清除之前的点击效果恢复定时器
1066+ if ( this . _clickEffectRestoreTimer ) {
1067+ clearTimeout ( this . _clickEffectRestoreTimer ) ;
1068+ this . _clickEffectRestoreTimer = null ;
1069+ }
1070+
1071+ try {
1072+ // 1. 播放表情(如果有配置)
1073+ let expressionFiles = [ ] ;
1074+ if ( this . emotionMapping && this . emotionMapping . expressions && this . emotionMapping . expressions [ emotion ] ) {
1075+ expressionFiles = this . emotionMapping . expressions [ emotion ] ;
1076+ }
1077+
1078+ // 兼容旧结构
1079+ if ( expressionFiles . length === 0 && this . fileReferences && Array . isArray ( this . fileReferences . Expressions ) ) {
1080+ const candidates = this . fileReferences . Expressions . filter ( e => ( e . Name || '' ) . startsWith ( emotion ) ) ;
1081+ expressionFiles = candidates . map ( e => e . File ) . filter ( Boolean ) ;
1082+ }
1083+
1084+ if ( expressionFiles . length > 0 ) {
1085+ const choiceFile = this . getRandomElement ( expressionFiles ) ;
1086+ if ( choiceFile ) {
1087+ // 在 FileReferences 中查找匹配的表情名称
1088+ let expressionName = null ;
1089+ if ( this . fileReferences && this . fileReferences . Expressions ) {
1090+ for ( const expr of this . fileReferences . Expressions ) {
1091+ if ( expr . File === choiceFile ) {
1092+ expressionName = expr . Name ;
1093+ break ;
1094+ }
1095+ }
1096+ }
1097+
1098+ if ( ! expressionName ) {
1099+ const base = String ( choiceFile ) . split ( '/' ) . pop ( ) || '' ;
1100+ expressionName = base . replace ( '.exp3.json' , '' ) ;
1101+ }
1102+
1103+ console . log ( `[ClickEffect] 播放临时表情: ${ expressionName } ` ) ;
1104+ await this . currentModel . expression ( expressionName ) ;
1105+ }
1106+ }
1107+
1108+ // 2. 播放低优先级动作
1109+ let motions = null ;
1110+ if ( this . fileReferences && this . fileReferences . Motions && this . fileReferences . Motions [ emotion ] ) {
1111+ motions = this . fileReferences . Motions [ emotion ] ;
1112+ } else if ( this . emotionMapping && this . emotionMapping . motions && this . emotionMapping . motions [ emotion ] ) {
1113+ const emotionMotions = this . emotionMapping . motions [ emotion ] ;
1114+ if ( Array . isArray ( emotionMotions ) && emotionMotions . length > 0 ) {
1115+ if ( typeof emotionMotions [ 0 ] === 'string' ) {
1116+ motions = emotionMotions . map ( f => ( { File : f } ) ) ;
1117+ } else {
1118+ motions = emotionMotions ;
1119+ }
1120+ }
1121+ }
1122+
1123+ if ( motions && motions . length > 0 ) {
1124+ // 使用低优先级播放动作
1125+ // pixi-live2d-display 的 motion(group, index, priority) 支持优先级参数
1126+ try {
1127+ const motion = await this . currentModel . motion ( emotion , undefined , priority ) ;
1128+ if ( motion ) {
1129+ console . log ( `[ClickEffect] 播放临时动作: ${ emotion } (优先级: ${ priority } )` ) ;
1130+ }
1131+ } catch ( motionError ) {
1132+ console . warn ( '[ClickEffect] 动作播放失败:' , motionError ) ;
1133+ }
1134+ }
1135+
1136+ // 3. 设置恢复定时器
1137+ // 使用唯一 ID 标记此次点击效果,用于判断是否应该恢复
1138+ const clickEffectId = Date . now ( ) ;
1139+ this . _currentClickEffectId = clickEffectId ;
1140+
1141+ this . _clickEffectRestoreTimer = setTimeout ( ( ) => {
1142+ this . _clickEffectRestoreTimer = null ;
1143+
1144+ // 检查是否仍然是此次点击效果(没有被新的情感/点击覆盖)
1145+ if ( this . _currentClickEffectId !== clickEffectId ) {
1146+ console . log ( '[ClickEffect] 临时效果已被新的情感覆盖,跳过恢复' ) ;
1147+ return ;
1148+ }
1149+
1150+ console . log ( '[ClickEffect] 临时效果结束,恢复到默认状态' ) ;
1151+ this . _currentClickEffectId = null ;
1152+
1153+ // 清除表情效果,恢复到常驻表情或默认状态
1154+ if ( this . clearExpression ) {
1155+ this . clearExpression ( ) ;
1156+ }
1157+
1158+ // 清除动作相关参数
1159+ if ( this . clearEmotionEffects ) {
1160+ this . clearEmotionEffects ( ) ;
1161+ }
1162+ } , duration ) ;
1163+
1164+ console . log ( `[ClickEffect] 临时效果将在 ${ duration } ms 后恢复` ) ;
1165+
1166+ } catch ( error ) {
1167+ console . error ( '[ClickEffect] 播放临时效果失败:' , error ) ;
1168+ }
1169+ } ;
1170+
10181171// 交互后保存位置和缩放的辅助函数
10191172Live2DManager . prototype . _savePositionAfterInteraction = async function ( ) {
10201173 if ( ! this . currentModel || ! this . _lastLoadedModelPath ) {
@@ -1378,6 +1531,13 @@ Live2DManager.prototype.cleanupEventListeners = function () {
13781531 this . _savePositionDebounceTimer = null ;
13791532 }
13801533
1534+ // 清理点击效果恢复定时器和 ID
1535+ if ( this . _clickEffectRestoreTimer ) {
1536+ clearTimeout ( this . _clickEffectRestoreTimer ) ;
1537+ this . _clickEffectRestoreTimer = null ;
1538+ }
1539+ this . _currentClickEffectId = null ;
1540+
13811541 // 清理页面卸载监听器(如果存在)
13821542 if ( this . _unloadListener ) {
13831543 window . removeEventListener ( 'beforeunload' , this . _unloadListener ) ;
0 commit comments