import 'package:flutter/material.dart'; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:askiineverdie/src/core/model/combat_event.dart'; import 'package:askiineverdie/src/core/model/combat_state.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/features/game/widgets/ascii_animation_card.dart'; /// 모바일용 확장 애니메이션 패널 /// /// 캐로셀 레이아웃에서 상단 영역에 표시되는 통합 패널: /// - ASCII 애니메이션 (기존 높이 유지) /// - 플레이어 HP/MP 컴팩트 바 (플로팅 텍스트 포함) /// - 활성 버프 아이콘 (최대 3개) /// - 몬스터 HP 바 (전투 중) class EnhancedAnimationPanel extends StatefulWidget { const EnhancedAnimationPanel({ super.key, required this.progress, required this.stats, required this.skillSystem, required this.speedMultiplier, required this.onSpeedCycle, required this.isPaused, required this.onPauseToggle, this.specialAnimation, this.weaponName, this.shieldName, this.characterLevel, this.monsterLevel, this.latestCombatEvent, }); final ProgressState progress; final Stats stats; final SkillSystemState skillSystem; final int speedMultiplier; final VoidCallback onSpeedCycle; final bool isPaused; final VoidCallback onPauseToggle; final AsciiAnimationType? specialAnimation; final String? weaponName; final String? shieldName; final int? characterLevel; final int? monsterLevel; final CombatEvent? latestCombatEvent; @override State createState() => _EnhancedAnimationPanelState(); } class _EnhancedAnimationPanelState extends State with TickerProviderStateMixin { // HP/MP 변화 애니메이션 late AnimationController _hpFlashController; late AnimationController _mpFlashController; late AnimationController _monsterFlashController; late Animation _hpFlashAnimation; late Animation _mpFlashAnimation; late Animation _monsterFlashAnimation; int _hpChange = 0; int _mpChange = 0; int _monsterHpChange = 0; // 이전 값 추적 int _lastHp = 0; int _lastMp = 0; int _lastMonsterHp = 0; @override void initState() { super.initState(); _hpFlashController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _mpFlashController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _monsterFlashController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _hpFlashAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _hpFlashController, curve: Curves.easeOut), ); _mpFlashAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _mpFlashController, curve: Curves.easeOut), ); _monsterFlashAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _monsterFlashController, curve: Curves.easeOut), ); // 초기값 설정 _lastHp = _currentHp; _lastMp = _currentMp; _lastMonsterHp = _currentMonsterHp ?? 0; } @override void didUpdateWidget(EnhancedAnimationPanel oldWidget) { super.didUpdateWidget(oldWidget); // HP 변화 감지 final newHp = _currentHp; if (newHp != _lastHp) { _hpChange = newHp - _lastHp; _hpFlashController.forward(from: 0.0); _lastHp = newHp; } // MP 변화 감지 final newMp = _currentMp; if (newMp != _lastMp) { _mpChange = newMp - _lastMp; _mpFlashController.forward(from: 0.0); _lastMp = newMp; } // 몬스터 HP 변화 감지 final newMonsterHp = _currentMonsterHp; if (newMonsterHp != null && newMonsterHp != _lastMonsterHp) { _monsterHpChange = newMonsterHp - _lastMonsterHp; _monsterFlashController.forward(from: 0.0); _lastMonsterHp = newMonsterHp; } else if (newMonsterHp == null) { _lastMonsterHp = 0; } } int get _currentHp => widget.progress.currentCombat?.playerStats.hpCurrent ?? widget.stats.hp; int get _currentHpMax => widget.progress.currentCombat?.playerStats.hpMax ?? widget.stats.hpMax; int get _currentMp => widget.progress.currentCombat?.playerStats.mpCurrent ?? widget.stats.mp; int get _currentMpMax => widget.progress.currentCombat?.playerStats.mpMax ?? widget.stats.mpMax; int? get _currentMonsterHp => widget.progress.currentCombat?.monsterStats.hpCurrent; int? get _currentMonsterHpMax => widget.progress.currentCombat?.monsterStats.hpMax; @override void dispose() { _hpFlashController.dispose(); _mpFlashController.dispose(); _monsterFlashController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final combat = widget.progress.currentCombat; final isInCombat = combat != null && combat.isActive; return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, border: Border( bottom: BorderSide(color: Theme.of(context).dividerColor), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ASCII 애니메이션 (기존 높이 120 유지) SizedBox( height: 120, child: AsciiAnimationCard( taskType: widget.progress.currentTask.type, monsterBaseName: widget.progress.currentTask.monsterBaseName, specialAnimation: widget.specialAnimation, weaponName: widget.weaponName, shieldName: widget.shieldName, characterLevel: widget.characterLevel, monsterLevel: widget.monsterLevel, isPaused: widget.isPaused, latestCombatEvent: widget.latestCombatEvent, ), ), const SizedBox(height: 8), // 상태 바 영역: HP/MP + 버프 아이콘 + 몬스터 HP Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 좌측: HP/MP 바 Expanded( flex: 3, child: Column( children: [ _buildCompactHpBar(), const SizedBox(height: 4), _buildCompactMpBar(), ], ), ), const SizedBox(width: 8), // 중앙: 활성 버프 아이콘 (최대 3개) _buildBuffIcons(), const SizedBox(width: 8), // 우측: 몬스터 HP (전투 중) 또는 컨트롤 버튼 Expanded( flex: 2, child: isInCombat ? _buildMonsterHpBar(combat) : _buildControlButtons(), ), ], ), const SizedBox(height: 6), // 하단: 태스크 프로그레스 바 + 캡션 _buildTaskProgress(), ], ), ); } /// 컴팩트 HP 바 Widget _buildCompactHpBar() { final ratio = _currentHpMax > 0 ? _currentHp / _currentHpMax : 0.0; final isLow = ratio < 0.2 && ratio > 0; return AnimatedBuilder( animation: _hpFlashAnimation, builder: (context, child) { return Stack( clipBehavior: Clip.none, children: [ // HP 바 Container( height: 14, decoration: BoxDecoration( color: isLow ? Colors.red.withValues(alpha: 0.2) : Colors.grey.shade800, borderRadius: BorderRadius.circular(3), ), child: Row( children: [ // 라벨 Container( width: 28, alignment: Alignment.center, child: Text( l10n.statHp, style: const TextStyle( fontSize: 9, fontWeight: FontWeight.bold, color: Colors.white70, ), ), ), // 프로그레스 Expanded( child: ClipRRect( borderRadius: const BorderRadius.horizontal( right: Radius.circular(3), ), child: LinearProgressIndicator( value: ratio.clamp(0.0, 1.0), backgroundColor: Colors.red.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation( isLow ? Colors.red : Colors.red.shade600, ), minHeight: 14, ), ), ), // 수치 Container( width: 48, alignment: Alignment.center, child: Text( '$_currentHp/$_currentHpMax', style: const TextStyle(fontSize: 8, color: Colors.white), ), ), ], ), ), // 플로팅 변화량 if (_hpChange != 0 && _hpFlashAnimation.value > 0.05) Positioned( right: 50, top: -8, child: Transform.translate( offset: Offset(0, -10 * (1 - _hpFlashAnimation.value)), child: Opacity( opacity: _hpFlashAnimation.value, child: Text( _hpChange > 0 ? '+$_hpChange' : '$_hpChange', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: _hpChange < 0 ? Colors.red : Colors.green, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), ], ), ), ), ), ), ], ); }, ); } /// 컴팩트 MP 바 Widget _buildCompactMpBar() { final ratio = _currentMpMax > 0 ? _currentMp / _currentMpMax : 0.0; return AnimatedBuilder( animation: _mpFlashAnimation, builder: (context, child) { return Stack( clipBehavior: Clip.none, children: [ Container( height: 14, decoration: BoxDecoration( color: Colors.grey.shade800, borderRadius: BorderRadius.circular(3), ), child: Row( children: [ Container( width: 28, alignment: Alignment.center, child: Text( l10n.statMp, style: const TextStyle( fontSize: 9, fontWeight: FontWeight.bold, color: Colors.white70, ), ), ), Expanded( child: ClipRRect( borderRadius: const BorderRadius.horizontal( right: Radius.circular(3), ), child: LinearProgressIndicator( value: ratio.clamp(0.0, 1.0), backgroundColor: Colors.blue.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation( Colors.blue.shade600, ), minHeight: 14, ), ), ), Container( width: 48, alignment: Alignment.center, child: Text( '$_currentMp/$_currentMpMax', style: const TextStyle(fontSize: 8, color: Colors.white), ), ), ], ), ), if (_mpChange != 0 && _mpFlashAnimation.value > 0.05) Positioned( right: 50, top: -8, child: Transform.translate( offset: Offset(0, -10 * (1 - _mpFlashAnimation.value)), child: Opacity( opacity: _mpFlashAnimation.value, child: Text( _mpChange > 0 ? '+$_mpChange' : '$_mpChange', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: _mpChange < 0 ? Colors.orange : Colors.cyan, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), ], ), ), ), ), ), ], ); }, ); } /// 활성 버프 아이콘 (최대 3개) Widget _buildBuffIcons() { final buffs = widget.skillSystem.activeBuffs; final currentMs = widget.skillSystem.elapsedMs; if (buffs.isEmpty) { return const SizedBox(width: 60); } // 최대 3개만 표시 final displayBuffs = buffs.take(3).toList(); return SizedBox( width: 60, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: displayBuffs.map((buff) { final remainingMs = buff.remainingDuration(currentMs); final progress = remainingMs / buff.effect.durationMs; final isExpiring = remainingMs < 3000; return Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Stack( alignment: Alignment.center, children: [ // 진행률 원형 표시 SizedBox( width: 18, height: 18, child: CircularProgressIndicator( value: progress.clamp(0.0, 1.0), strokeWidth: 2, backgroundColor: Colors.grey.shade700, valueColor: AlwaysStoppedAnimation( isExpiring ? Colors.orange : Colors.lightBlue, ), ), ), // 버프 아이콘 Icon( Icons.trending_up, size: 10, color: isExpiring ? Colors.orange : Colors.lightBlue, ), ], ), ); }).toList(), ), ); } /// 몬스터 HP 바 (전투 중) Widget _buildMonsterHpBar(CombatState combat) { final max = _currentMonsterHpMax ?? 1; final current = _currentMonsterHp ?? 0; final ratio = max > 0 ? current / max : 0.0; return AnimatedBuilder( animation: _monsterFlashAnimation, builder: (context, child) { return Stack( clipBehavior: Clip.none, children: [ Container( height: 32, decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // HP 바 Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: ratio.clamp(0.0, 1.0), backgroundColor: Colors.orange.withValues(alpha: 0.2), valueColor: const AlwaysStoppedAnimation(Colors.orange), minHeight: 8, ), ), ), const SizedBox(height: 2), // 퍼센트 Text( '${(ratio * 100).toInt()}%', style: const TextStyle( fontSize: 9, color: Colors.orange, fontWeight: FontWeight.bold, ), ), ], ), ), // 플로팅 데미지 if (_monsterHpChange != 0 && _monsterFlashAnimation.value > 0.05) Positioned( right: 10, top: -10, child: Transform.translate( offset: Offset(0, -10 * (1 - _monsterFlashAnimation.value)), child: Opacity( opacity: _monsterFlashAnimation.value, child: Text( _monsterHpChange > 0 ? '+$_monsterHpChange' : '$_monsterHpChange', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: _monsterHpChange < 0 ? Colors.yellow : Colors.green, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), ], ), ), ), ), ), ], ); }, ); } /// 컨트롤 버튼 (비전투 시) Widget _buildControlButtons() { return SizedBox( height: 32, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 일시정지 버튼 SizedBox( width: 36, height: 28, child: OutlinedButton( onPressed: widget.onPauseToggle, style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, side: BorderSide( color: widget.isPaused ? Colors.orange.withValues(alpha: 0.7) : Theme.of( context, ).colorScheme.outline.withValues(alpha: 0.5), ), ), child: Icon( widget.isPaused ? Icons.play_arrow : Icons.pause, size: 14, color: widget.isPaused ? Colors.orange : null, ), ), ), const SizedBox(width: 4), // 속도 버튼 SizedBox( width: 36, height: 28, child: OutlinedButton( onPressed: widget.onSpeedCycle, style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ), child: Text( '${widget.speedMultiplier}x', style: TextStyle( fontSize: 10, fontWeight: widget.speedMultiplier > 1 ? FontWeight.bold : FontWeight.normal, color: widget.speedMultiplier > 1 ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ], ), ); } /// 태스크 프로그레스 바 Widget _buildTaskProgress() { final task = widget.progress.task; final progressValue = task.max > 0 ? (task.position / task.max).clamp(0.0, 1.0) : 0.0; return Column( children: [ // 캡션 Text( widget.progress.currentTask.caption, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), // 프로그레스 바 Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: LinearProgressIndicator( value: progressValue, backgroundColor: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), minHeight: 10, ), ), ], ); } }