import 'package:flutter/material.dart'; import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; /// 스탯 표시 패널 (Phase 8: 실시간 변화 표시) /// /// 장비 변경이나 버프 시 스탯 변화량을 애니메이션으로 표시 class StatsPanel extends StatefulWidget { const StatsPanel({super.key, required this.stats}); final Stats stats; @override State createState() => _StatsPanelState(); } class _StatsPanelState extends State with SingleTickerProviderStateMixin { // 변화량 맵 (스탯 이름 -> 변화량) final Map _statChanges = {}; // 변화 애니메이션 컨트롤러 late AnimationController _animationController; late Animation _fadeAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, ); _fadeAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation( parent: _animationController, curve: const Interval(0.5, 1.0, curve: Curves.easeOut), ), ); } @override void didUpdateWidget(StatsPanel oldWidget) { super.didUpdateWidget(oldWidget); // 스탯 변화 감지 _detectChanges(oldWidget.stats, widget.stats); } void _detectChanges(Stats oldStats, Stats newStats) { final changes = {}; if (newStats.str != oldStats.str) { changes['str'] = newStats.str - oldStats.str; } if (newStats.con != oldStats.con) { changes['con'] = newStats.con - oldStats.con; } if (newStats.dex != oldStats.dex) { changes['dex'] = newStats.dex - oldStats.dex; } if (newStats.intelligence != oldStats.intelligence) { changes['int'] = newStats.intelligence - oldStats.intelligence; } if (newStats.wis != oldStats.wis) { changes['wis'] = newStats.wis - oldStats.wis; } if (newStats.cha != oldStats.cha) { changes['cha'] = newStats.cha - oldStats.cha; } if (newStats.hpMax != oldStats.hpMax) { changes['hpMax'] = newStats.hpMax - oldStats.hpMax; } if (newStats.mpMax != oldStats.mpMax) { changes['mpMax'] = newStats.mpMax - oldStats.mpMax; } if (changes.isNotEmpty) { setState(() { _statChanges ..clear() ..addAll(changes); }); // 애니메이션 재시작 _animationController ..reset() ..forward().then((_) { if (mounted) { setState(() { _statChanges.clear(); }); } }); } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = L10n.of(context); final stats = [ ('str', l10n.statStr, widget.stats.str), ('con', l10n.statCon, widget.stats.con), ('dex', l10n.statDex, widget.stats.dex), ('int', l10n.statInt, widget.stats.intelligence), ('wis', l10n.statWis, widget.stats.wis), ('cha', l10n.statCha, widget.stats.cha), ('hpMax', l10n.statHpMax, widget.stats.hpMax), ('mpMax', l10n.statMpMax, widget.stats.mpMax), ]; return ListView.builder( itemCount: stats.length, padding: const EdgeInsets.symmetric(horizontal: 8), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final stat = stats[index]; final change = _statChanges[stat.$1]; return _StatRow( label: stat.$2, value: stat.$3, change: change, fadeAnimation: _fadeAnimation, ); }, ); } } /// 개별 스탯 행 위젯 class _StatRow extends StatelessWidget { const _StatRow({ required this.label, required this.value, this.change, required this.fadeAnimation, }); final String label; final int value; final int? change; final Animation fadeAnimation; @override Widget build(BuildContext context) { return Row( children: [ // 라벨 (Flexible로 오버플로우 방지) Expanded( child: Text( label, style: const TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis, ), ), // 값 Text( '$value', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), ), // 변화량 표시 if (change != null) ...[ const SizedBox(width: 2), AnimatedBuilder( animation: fadeAnimation, builder: (context, child) { return Opacity( opacity: fadeAnimation.value, child: _ChangeIndicator(change: change!), ); }, ), ], ], ); } } /// 변화량 표시 위젯 class _ChangeIndicator extends StatelessWidget { const _ChangeIndicator({required this.change}); final int change; @override Widget build(BuildContext context) { final isPositive = change > 0; final color = isPositive ? Colors.green : Colors.red; final icon = isPositive ? Icons.arrow_upward : Icons.arrow_downward; final text = isPositive ? '+$change' : '$change'; return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 10, color: color), Text( text, style: TextStyle( fontSize: 10, color: color, fontWeight: FontWeight.bold, ), ), ], ); } }