import 'package:flutter/material.dart'; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/src/shared/retro_colors.dart'; /// HP/MP 바 위젯 (레트로 RPG 스타일) /// /// - 세그먼트 스타일의 8-bit 프로그레스 바 /// - HP가 20% 미만일 때 빨간색 깜빡임 /// - HP/MP 변화 시 색상 플래시 + 변화량 표시 /// - 전투 중 몬스터 HP 바 표시 class HpMpBar extends StatefulWidget { const HpMpBar({ super.key, required this.hpCurrent, required this.hpMax, required this.mpCurrent, required this.mpMax, this.monsterHpCurrent, this.monsterHpMax, this.monsterName, }); final int hpCurrent; final int hpMax; final int mpCurrent; final int mpMax; /// 전투 중 몬스터 HP (null이면 비전투) final int? monsterHpCurrent; final int? monsterHpMax; final String? monsterName; @override State createState() => _HpMpBarState(); } class _HpMpBarState extends State with TickerProviderStateMixin { late AnimationController _blinkController; late Animation _blinkAnimation; // HP/MP 변화 애니메이션 late AnimationController _hpFlashController; late AnimationController _mpFlashController; late Animation _hpFlashAnimation; late Animation _mpFlashAnimation; // 변화량 표시용 int _hpChange = 0; int _mpChange = 0; bool _hpDamage = false; bool _mpDamage = false; // 몬스터 HP 변화 애니메이션 late AnimationController _monsterFlashController; late Animation _monsterFlashAnimation; int _monsterHpChange = 0; @override void initState() { super.initState(); // 위험 깜빡임 _blinkController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _blinkAnimation = Tween(begin: 1.0, end: 0.3).animate( CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut), ); // HP 플래시 _hpFlashController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _hpFlashAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _hpFlashController, curve: Curves.easeOut), ); // MP 플래시 _mpFlashController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _mpFlashAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _mpFlashController, curve: Curves.easeOut), ); // 몬스터 HP 플래시 _monsterFlashController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _monsterFlashAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _monsterFlashController, curve: Curves.easeOut), ); _updateBlinkState(); } @override void didUpdateWidget(HpMpBar oldWidget) { super.didUpdateWidget(oldWidget); // HP 변화 감지 if (oldWidget.hpCurrent != widget.hpCurrent) { _hpChange = widget.hpCurrent - oldWidget.hpCurrent; _hpDamage = _hpChange < 0; _hpFlashController.forward(from: 0.0); } // MP 변화 감지 if (oldWidget.mpCurrent != widget.mpCurrent) { _mpChange = widget.mpCurrent - oldWidget.mpCurrent; _mpDamage = _mpChange < 0; _mpFlashController.forward(from: 0.0); } // 몬스터 HP 변화 감지 if (oldWidget.monsterHpCurrent != widget.monsterHpCurrent && widget.monsterHpCurrent != null && oldWidget.monsterHpCurrent != null) { _monsterHpChange = widget.monsterHpCurrent! - oldWidget.monsterHpCurrent!; _monsterFlashController.forward(from: 0.0); } _updateBlinkState(); } void _updateBlinkState() { final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 1.0; if (hpRatio < 0.2 && hpRatio > 0) { if (!_blinkController.isAnimating) { _blinkController.repeat(reverse: true); } } else { _blinkController.stop(); _blinkController.reset(); } } @override void dispose() { _blinkController.dispose(); _hpFlashController.dispose(); _mpFlashController.dispose(); _monsterFlashController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 0.0; final mpRatio = widget.mpMax > 0 ? widget.mpCurrent / widget.mpMax : 0.0; final hasMonster = widget.monsterHpCurrent != null && widget.monsterHpMax != null && widget.monsterHpMax! > 0; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: RetroColors.panelBg, border: Border.all(color: RetroColors.panelBorderOuter, width: 2), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // HP 바 (플래시 효과 포함) _buildAnimatedBar( label: l10n.statHp, current: widget.hpCurrent, max: widget.hpMax, ratio: hpRatio, fillColor: RetroColors.hpRed, emptyColor: RetroColors.hpRedDark, isLow: hpRatio < 0.2 && hpRatio > 0, flashController: _hpFlashAnimation, change: _hpChange, isDamage: _hpDamage, ), const SizedBox(height: 6), // MP 바 (플래시 효과 포함) _buildAnimatedBar( label: l10n.statMp, current: widget.mpCurrent, max: widget.mpMax, ratio: mpRatio, fillColor: RetroColors.mpBlue, emptyColor: RetroColors.mpBlueDark, isLow: false, flashController: _mpFlashAnimation, change: _mpChange, isDamage: _mpDamage, ), // 몬스터 HP 바 (전투 중일 때만) if (hasMonster) ...[const SizedBox(height: 8), _buildMonsterBar()], ], ), ); } Widget _buildAnimatedBar({ required String label, required int current, required int max, required double ratio, required Color fillColor, required Color emptyColor, required bool isLow, required Animation flashController, required int change, required bool isDamage, }) { return AnimatedBuilder( animation: Listenable.merge([_blinkAnimation, flashController]), builder: (context, child) { // 플래시 색상 (데미지=빨강, 회복=녹색) final flashColor = isDamage ? RetroColors.hpRed.withValues(alpha: flashController.value * 0.4) : RetroColors.expGreen.withValues(alpha: flashController.value * 0.4); // 위험 깜빡임 배경 final lowBgColor = isLow ? RetroColors.hpRed.withValues(alpha: (1 - _blinkAnimation.value) * 0.3) : Colors.transparent; return Container( decoration: BoxDecoration( color: flashController.value > 0.1 ? flashColor : lowBgColor, ), child: Stack( children: [ _buildRetroBar( label: label, current: current, max: max, ratio: ratio, fillColor: fillColor, emptyColor: emptyColor, blinkOpacity: isLow ? _blinkAnimation.value : 1.0, ), // 플로팅 변화량 텍스트 (위로 떠오르며 사라짐) if (change != 0 && flashController.value > 0.05) Positioned( right: 70, top: 0, bottom: 0, child: Transform.translate( // 위로 떠오르는 애니메이션 (최대 15픽셀 위로) offset: Offset(0, -15 * (1 - flashController.value)), child: Opacity( opacity: flashController.value, child: Text( change > 0 ? '+$change' : '$change', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 8, fontWeight: FontWeight.bold, color: isDamage ? RetroColors.hpRed : RetroColors.expGreen, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), Shadow(color: Colors.black, blurRadius: 6), ], ), ), ), ), ), ], ), ); }, ); } /// 레트로 스타일 세그먼트 바 Widget _buildRetroBar({ required String label, required int current, required int max, required double ratio, required Color fillColor, required Color emptyColor, required double blinkOpacity, }) { const segmentCount = 15; final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round(); return Row( children: [ // 레이블 (HP/MP) SizedBox( width: 28, child: Text( label, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 7, fontWeight: FontWeight.bold, color: RetroColors.gold.withValues(alpha: blinkOpacity), ), ), ), // 세그먼트 바 Expanded( child: Container( height: 12, decoration: BoxDecoration( color: emptyColor.withValues(alpha: 0.3), border: Border.all( color: RetroColors.panelBorderOuter, width: 1, ), ), child: Row( children: List.generate(segmentCount, (index) { final isFilled = index < filledSegments; return Expanded( child: Container( decoration: BoxDecoration( color: isFilled ? fillColor.withValues(alpha: blinkOpacity) : emptyColor.withValues(alpha: 0.2), border: Border( right: index < segmentCount - 1 ? BorderSide( color: RetroColors.panelBorderOuter .withValues(alpha: 0.3), width: 1, ) : BorderSide.none, ), ), ), ); }), ), ), ), const SizedBox(width: 6), // 수치 표시 SizedBox( width: 60, child: Text( '$current/$max', style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.textLight, ), textAlign: TextAlign.right, overflow: TextOverflow.ellipsis, ), ), ], ); } /// 몬스터 HP 바 (레트로 스타일) Widget _buildMonsterBar() { final max = widget.monsterHpMax!; final ratio = max > 0 ? widget.monsterHpCurrent! / max : 0.0; const segmentCount = 10; final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round(); return AnimatedBuilder( animation: _monsterFlashAnimation, builder: (context, child) { // 데미지 플래시 (몬스터는 항상 데미지를 받음) final flashColor = RetroColors.gold.withValues( alpha: _monsterFlashAnimation.value * 0.3, ); return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: _monsterFlashAnimation.value > 0.1 ? flashColor : RetroColors.panelBgLight.withValues(alpha: 0.5), border: Border.all( color: RetroColors.gold.withValues(alpha: 0.6), width: 1, ), ), child: Stack( clipBehavior: Clip.none, children: [ Row( children: [ // 몬스터 아이콘 const Icon( Icons.pest_control, size: 12, color: RetroColors.gold, ), const SizedBox(width: 6), // 세그먼트 HP 바 Expanded( child: Container( height: 10, decoration: BoxDecoration( color: RetroColors.hpRedDark.withValues(alpha: 0.3), border: Border.all( color: RetroColors.panelBorderOuter, width: 1, ), ), child: Row( children: List.generate(segmentCount, (index) { final isFilled = index < filledSegments; return Expanded( child: Container( decoration: BoxDecoration( color: isFilled ? RetroColors.gold : RetroColors.panelBorderOuter .withValues(alpha: 0.3), border: Border( right: index < segmentCount - 1 ? BorderSide( color: RetroColors.panelBorderOuter .withValues(alpha: 0.3), width: 1, ) : BorderSide.none, ), ), ), ); }), ), ), ), const SizedBox(width: 6), // HP 퍼센트 Text( '${(ratio * 100).toInt()}%', style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.gold, ), ), ], ), // 플로팅 데미지 텍스트 if (_monsterHpChange != 0 && _monsterFlashAnimation.value > 0.05) Positioned( right: 50, top: -8, child: Transform.translate( offset: Offset(0, -12 * (1 - _monsterFlashAnimation.value)), child: Opacity( opacity: _monsterFlashAnimation.value, child: Text( _monsterHpChange > 0 ? '+$_monsterHpChange' : '$_monsterHpChange', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 7, fontWeight: FontWeight.bold, color: _monsterHpChange < 0 ? RetroColors.gold : RetroColors.expGreen, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), Shadow(color: Colors.black, blurRadius: 6), ], ), ), ), ), ), ], ), ); }, ); } }