import 'package:flutter/material.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; import 'package:asciineverdie/src/core/engine/item_service.dart'; import 'package:asciineverdie/src/core/model/arena_match.dart'; import 'package:asciineverdie/src/core/model/equipment_item.dart'; import 'package:asciineverdie/src/core/model/equipment_slot.dart'; import 'package:asciineverdie/src/core/model/item_stats.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; // 임시 문자열 const _victory = 'VICTORY!'; const _defeat = 'DEFEAT...'; const _exchange = 'EQUIPMENT EXCHANGE'; const _turns = 'TURNS'; /// 아레나 결과 패널 (인라인) /// /// 전투 로그 하단에 표시되는 플로팅 결과 패널 class ArenaResultPanel extends StatefulWidget { const ArenaResultPanel({ super.key, required this.result, required this.turnCount, required this.onContinue, }); /// 대전 결과 final ArenaMatchResult result; /// 총 턴 수 final int turnCount; /// Continue 콜백 final VoidCallback onContinue; @override State createState() => _ArenaResultPanelState(); } class _ArenaResultPanelState extends State with SingleTickerProviderStateMixin { late AnimationController _slideController; late Animation _slideAnimation; late Animation _fadeAnimation; @override void initState() { super.initState(); _slideController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _slideAnimation = Tween( begin: const Offset(0, 1), // 아래에서 위로 end: Offset.zero, ).animate(CurvedAnimation( parent: _slideController, curve: Curves.easeOutCubic, )); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _slideController, curve: Curves.easeOut, )); // 약간 지연 후 애니메이션 시작 (분해 애니메이션과 동기화) Future.delayed(const Duration(milliseconds: 800), () { if (mounted) { _slideController.forward(); } }); } @override void dispose() { _slideController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isVictory = widget.result.isVictory; final resultColor = isVictory ? Colors.amber : Colors.red.shade400; final panelColor = isVictory ? RetroColors.goldOf(context).withValues(alpha: 0.15) : Colors.red.withValues(alpha: 0.1); final borderColor = isVictory ? RetroColors.goldOf(context) : Colors.red.shade400; return SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: Container( margin: const EdgeInsets.all(8), decoration: BoxDecoration( color: RetroColors.panelBgOf(context), borderRadius: BorderRadius.circular(8), border: Border.all(color: borderColor, width: 2), boxShadow: [ BoxShadow( color: borderColor.withValues(alpha: 0.3), blurRadius: 8, spreadRadius: 1, ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 타이틀 배너 Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: panelColor, borderRadius: const BorderRadius.vertical( top: Radius.circular(6), ), ), child: _buildTitle(context, isVictory, resultColor), ), // 내용 Padding( padding: const EdgeInsets.all(12), child: Column( children: [ // 전투 요약 (턴 수) _buildBattleSummary(context), const SizedBox(height: 12), // 장비 교환 _buildExchangeSection(context), const SizedBox(height: 12), // Continue 버튼 _buildContinueButton(context, resultColor), ], ), ), ], ), ), ), ); } Widget _buildTitle(BuildContext context, bool isVictory, Color color) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isVictory ? Icons.emoji_events : Icons.sentiment_very_dissatisfied, color: color, size: 20, ), const SizedBox(width: 8), Text( isVictory ? _victory : _defeat, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: color, ), ), const SizedBox(width: 8), Icon( isVictory ? Icons.emoji_events : Icons.sentiment_very_dissatisfied, color: color, size: 20, ), ], ); } Widget _buildBattleSummary(BuildContext context) { final winner = widget.result.isVictory ? widget.result.match.challenger.characterName : widget.result.match.opponent.characterName; final loser = widget.result.isVictory ? widget.result.match.opponent.characterName : widget.result.match.challenger.characterName; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 승자 Text( winner, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 7, color: RetroColors.goldOf(context), ), ), Text( ' defeated ', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.textMutedOf(context), ), ), // 패자 Text( loser, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 7, color: RetroColors.textSecondaryOf(context), ), ), Text( ' in ', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.textMutedOf(context), ), ), // 턴 수 Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: RetroColors.goldOf(context).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( '${widget.turnCount} $_turns', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.goldOf(context), ), ), ), ], ); } Widget _buildExchangeSection(BuildContext context) { final isVictory = widget.result.isVictory; // 승패에 따라 교환 슬롯 결정 // 승리: 도전자가 선택한 슬롯(상대에게서 약탈) // 패배: 상대가 선택한 슬롯(도전자에게서 약탈당함) final slot = isVictory ? widget.result.match.challengerBettingSlot : widget.result.match.opponentBettingSlot; // 도전자의 교환 결과 final oldItem = _findItem( widget.result.match.challenger.finalEquipment, slot, ); final newItem = _findItem( widget.result.updatedChallenger.finalEquipment, slot, ); final oldScore = oldItem != null ? ItemService.calculateEquipmentScore(oldItem) : 0; final newScore = newItem != null ? ItemService.calculateEquipmentScore(newItem) : 0; final scoreDiff = newScore - oldScore; return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: isVictory ? Colors.green.withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( color: isVictory ? Colors.green.withValues(alpha: 0.3) : Colors.red.withValues(alpha: 0.3), ), ), child: Column( children: [ // 교환 타이틀 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.swap_horiz, color: RetroColors.goldOf(context), size: 14, ), const SizedBox(width: 4), Text( _exchange, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.goldOf(context), ), ), ], ), const SizedBox(height: 8), // 슬롯 Text( _getSlotLabel(slot), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: RetroColors.textMutedOf(context), ), ), const SizedBox(height: 8), // 교환 내용 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 이전 아이템 _buildItemBadge(context, oldItem, oldScore), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( Icons.arrow_forward, size: 14, color: RetroColors.textMutedOf(context), ), ), // 새 아이템 _buildItemBadge(context, newItem, newScore), const SizedBox(width: 8), // 점수 변화 Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: scoreDiff >= 0 ? Colors.green.withValues(alpha: 0.2) : Colors.red.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( scoreDiff >= 0 ? Icons.arrow_upward : Icons.arrow_downward, size: 10, color: scoreDiff >= 0 ? Colors.green : Colors.red, ), Text( '${scoreDiff >= 0 ? '+' : ''}$scoreDiff', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 6, color: scoreDiff >= 0 ? Colors.green : Colors.red, ), ), ], ), ), ], ), ], ), ); } Widget _buildItemBadge( BuildContext context, EquipmentItem? item, int score, ) { if (item == null || item.isEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), ), child: Text( '(empty)', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 5, color: RetroColors.textMutedOf(context), ), ), ); } final rarityColor = _getRarityColor(item.rarity); return Container( constraints: const BoxConstraints(maxWidth: 80), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), decoration: BoxDecoration( color: rarityColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), border: Border.all(color: rarityColor.withValues(alpha: 0.5)), ), child: Column( children: [ Text( item.name, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 5, color: rarityColor, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( '$score pt', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 5, color: RetroColors.textMutedOf(context), ), ), ], ), ); } Widget _buildContinueButton(BuildContext context, Color color) { return SizedBox( width: double.infinity, child: FilledButton( onPressed: widget.onContinue, style: FilledButton.styleFrom( backgroundColor: color, padding: const EdgeInsets.symmetric(vertical: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), child: Text( l10n.buttonConfirm, style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 8, color: Colors.black, ), ), ), ); } EquipmentItem? _findItem(List? equipment, EquipmentSlot slot) { if (equipment == null) return null; for (final item in equipment) { if (item.slot == slot) return item; } return null; } String _getSlotLabel(EquipmentSlot slot) { return switch (slot) { EquipmentSlot.weapon => l10n.slotWeapon, EquipmentSlot.shield => l10n.slotShield, EquipmentSlot.helm => l10n.slotHelm, EquipmentSlot.hauberk => l10n.slotHauberk, EquipmentSlot.brassairts => l10n.slotBrassairts, EquipmentSlot.vambraces => l10n.slotVambraces, EquipmentSlot.gauntlets => l10n.slotGauntlets, EquipmentSlot.gambeson => l10n.slotGambeson, EquipmentSlot.cuisses => l10n.slotCuisses, EquipmentSlot.greaves => l10n.slotGreaves, EquipmentSlot.sollerets => l10n.slotSollerets, }; } Color _getRarityColor(ItemRarity rarity) { return switch (rarity) { ItemRarity.common => Colors.grey.shade600, ItemRarity.uncommon => Colors.green.shade600, ItemRarity.rare => Colors.blue.shade600, ItemRarity.epic => Colors.purple.shade600, ItemRarity.legendary => Colors.orange.shade700, }; } }