import 'package:flutter/material.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:asciineverdie/src/core/model/combat_event.dart'; import 'package:asciineverdie/src/core/model/equipment_slot.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/item_stats.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; /// 사망 오버레이 위젯 /// /// 플레이어 사망 시 표시되는 전체 화면 오버레이 /// - 무료 부활: HP 50%, 아이템 희생 /// - 광고 부활: HP 100%, 아이템 복구, 10분 자동부활 버프 class DeathOverlay extends StatelessWidget { const DeathOverlay({ super.key, required this.deathInfo, required this.traits, required this.onResurrect, this.onAdRevive, this.isPaidUser = false, }); /// 사망 정보 final DeathInfo deathInfo; /// 캐릭터 특성 (이름, 직업 등) final Traits traits; /// 무료 부활 버튼 콜백 (HP 50%, 아이템 희생) final VoidCallback onResurrect; /// 광고 부활 버튼 콜백 (HP 100% + 아이템 복구 + 10분 자동부활) /// null이면 광고 부활 버튼 숨김 final VoidCallback? onAdRevive; /// 유료 유저 여부 (광고 아이콘 표시용) final bool isPaidUser; @override Widget build(BuildContext context) { // 테마 인식 색상 (Theme-aware colors) final hpColor = RetroColors.hpOf(context); final hpDark = RetroColors.hpDarkOf(context); final panelBg = RetroColors.panelBgOf(context); final borderColor = RetroColors.borderOf(context); return Material( color: Colors.black.withValues(alpha: 0.9), child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 420), margin: const EdgeInsets.all(24), decoration: BoxDecoration( color: panelBg, border: Border( top: BorderSide(color: hpColor, width: 4), left: BorderSide(color: hpColor, width: 4), bottom: BorderSide(color: borderColor, width: 4), right: BorderSide(color: borderColor, width: 4), ), boxShadow: [ BoxShadow( color: hpColor.withValues(alpha: 0.5), blurRadius: 30, spreadRadius: 5, ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 헤더 바 Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: hpColor.withValues(alpha: 0.3), border: Border(bottom: BorderSide(color: hpColor, width: 2)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('☠', style: TextStyle(fontSize: 20, color: hpColor)), const SizedBox(width: 8), Text( 'GAME OVER', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: hpColor, letterSpacing: 2, ), ), const SizedBox(width: 8), Text('☠', style: TextStyle(fontSize: 20, color: hpColor)), ], ), ), // 본문 (스크롤 가능) Flexible( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 사망 타이틀 _buildDeathTitle(context), const SizedBox(height: 16), // 캐릭터 정보 _buildCharacterInfo(context), const SizedBox(height: 16), // 사망 원인 _buildDeathCause(context), const SizedBox(height: 20), // 구분선 _buildRetroDivider(hpColor, hpDark), const SizedBox(height: 16), // 상실 정보 _buildLossInfo(context), // 전투 로그 (있는 경우만 표시) if (deathInfo.lastCombatEvents.isNotEmpty) ...[ const SizedBox(height: 16), _buildRetroDivider(hpColor, hpDark), const SizedBox(height: 8), _buildCombatLog(context), ], const SizedBox(height: 24), // 일반 부활 버튼 (HP 50%, 아이템 희생) _buildResurrectButton(context), // 광고 부활 버튼 (HP 100% + 아이템 복구 + 10분 자동부활) if (onAdRevive != null) ...[ const SizedBox(height: 12), _buildAdReviveButton(context), ], ], ), ), ), ], ), ), ), ); } /// 레트로 스타일 구분선 Widget _buildRetroDivider(Color hpColor, Color hpDark) { return Container( height: 2, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.transparent, hpDark, hpColor, hpDark, Colors.transparent, ], ), ), ); } Widget _buildDeathTitle(BuildContext context) { final hpColor = RetroColors.hpOf(context); final hpDark = RetroColors.hpDarkOf(context); return Column( children: [ // ASCII 스컬 (더 큰 버전) Text( ' _____ \n' ' / \\\n' ' | () () |\n' ' \\ ^ /\n' ' ||||| ', style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 17, color: hpColor, height: 1.0, ), textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( l10n.deathYouDied.toUpperCase(), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: hpColor, letterSpacing: 2, shadows: [ const Shadow(color: Colors.black, blurRadius: 4), Shadow(color: hpDark, blurRadius: 8), ], ), ), ], ); } Widget _buildCharacterInfo(BuildContext context) { final surface = RetroColors.surfaceOf(context); final borderLight = RetroColors.borderLightOf(context); final gold = RetroColors.goldOf(context); final textPrimary = RetroColors.textPrimaryOf(context); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: surface.withValues(alpha: 0.5), border: Border.all(color: borderLight, width: 1), ), child: Column( children: [ Text( traits.name, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: gold, ), ), const SizedBox(height: 6), Text( 'Lv.${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 13, color: textPrimary, ), ), ], ), ); } Widget _buildDeathCause(BuildContext context) { final causeText = _getDeathCauseText(); final hpColor = RetroColors.hpOf(context); final hpDark = RetroColors.hpDarkOf(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: hpDark.withValues(alpha: 0.3), border: Border.all(color: hpColor.withValues(alpha: 0.5)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text('⚔', style: TextStyle(fontSize: 18, color: hpColor)), const SizedBox(width: 8), Flexible( child: Text( causeText, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: hpColor, ), textAlign: TextAlign.center, ), ), ], ), ); } String _getDeathCauseText() { return switch (deathInfo.cause) { DeathCause.monster => l10n.deathKilledBy(deathInfo.killerName), DeathCause.selfDamage => l10n.deathSelfInflicted, DeathCause.environment => l10n.deathEnvironmentalHazard, }; } Widget _buildLossInfo(BuildContext context) { final hasLostItem = deathInfo.lostItemName != null; final hpColor = RetroColors.hpOf(context); final hpDark = RetroColors.hpDarkOf(context); final muted = RetroColors.textMutedOf(context); final expColor = RetroColors.expOf(context); final gold = RetroColors.goldOf(context); // 희귀도에 따른 아이템 이름 색상 final itemRarityColor = _getRarityColor(deathInfo.lostItemRarity); return Column( children: [ // 제물로 바친 아이템 표시 if (hasLostItem) ...[ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: hpDark.withValues(alpha: 0.2), border: Border.all(color: hpColor.withValues(alpha: 0.4)), ), child: Row( children: [ const Text('🔥', style: TextStyle(fontSize: 20)), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.deathSacrificedToResurrect.toUpperCase(), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 13, color: muted, ), ), const SizedBox(height: 4), // 슬롯명 (회색) + 아이템명 (희귀도 색상) Text.rich( TextSpan( children: [ TextSpan( text: '[${_getSlotName(deathInfo.lostItemSlot)}] ', style: TextStyle(color: muted), ), TextSpan( text: deathInfo.lostItemName!, style: TextStyle(color: itemRarityColor), ), ], ), style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 13, ), ), ], ), ), ], ), ), const SizedBox(height: 12), ] else ...[ _buildInfoRow( context, asciiIcon: '✓', label: l10n.deathEquipment, value: l10n.deathNoSacrificeNeeded, valueColor: expColor, ), const SizedBox(height: 8), ], _buildInfoRow( context, asciiIcon: '💰', label: l10n.deathCoinRemaining, value: _formatGold(deathInfo.goldAtDeath), valueColor: gold, ), ], ); } Widget _buildInfoRow( BuildContext context, { required String asciiIcon, required String label, required String value, required Color valueColor, }) { final muted = RetroColors.textMutedOf(context); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text(asciiIcon, style: TextStyle(fontSize: 18, color: valueColor)), const SizedBox(width: 8), Text( label, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 13, color: muted, ), ), ], ), Text( value, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: valueColor, ), ), ], ); } String _formatGold(int gold) { if (gold >= 1000000) { return '${(gold / 1000000).toStringAsFixed(1)}M'; } else if (gold >= 1000) { return '${(gold / 1000).toStringAsFixed(1)}K'; } return gold.toString(); } Widget _buildResurrectButton(BuildContext context) { final expColor = RetroColors.expOf(context); final expDark = RetroColors.expDarkOf(context); return GestureDetector( onTap: onResurrect, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: expColor.withValues(alpha: 0.2), border: Border( top: BorderSide(color: expColor, width: 3), left: BorderSide(color: expColor, width: 3), bottom: BorderSide(color: expDark.withValues(alpha: 0.8), width: 3), right: BorderSide(color: expDark.withValues(alpha: 0.8), width: 3), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '↺', style: TextStyle( fontSize: 20, color: expColor, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 8), Text( l10n.deathResurrect.toUpperCase(), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: expColor, letterSpacing: 1, ), ), ], ), ), ); } /// 광고 부활 버튼 (HP 100% + 아이템 복구 + 10분 자동부활) Widget _buildAdReviveButton(BuildContext context) { final gold = RetroColors.goldOf(context); final goldDark = RetroColors.goldDarkOf(context); final muted = RetroColors.textMutedOf(context); final hasLostItem = deathInfo.lostItemName != null; final itemRarityColor = _getRarityColor(deathInfo.lostItemRarity); return GestureDetector( onTap: onAdRevive, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), decoration: BoxDecoration( color: gold.withValues(alpha: 0.2), border: Border( top: BorderSide(color: gold, width: 3), left: BorderSide(color: gold, width: 3), bottom: BorderSide( color: goldDark.withValues(alpha: 0.8), width: 3, ), right: BorderSide(color: goldDark.withValues(alpha: 0.8), width: 3), ), ), child: Column( children: [ // 메인 버튼 텍스트 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('✨', style: TextStyle(fontSize: 20, color: gold)), const SizedBox(width: 8), Text( l10n.deathAdRevive.toUpperCase(), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: gold, letterSpacing: 1, ), ), // 광고 뱃지 (무료 유저만) if (!isPaidUser) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( '▶ AD', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: Colors.white, ), ), ), ], ], ), const SizedBox(height: 8), // 혜택 목록 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // HP 100% 회복 _buildBenefitRow( context, icon: '♥', text: l10n.deathAdReviveHp, color: RetroColors.hpOf(context), ), const SizedBox(height: 4), // 아이템 복구 (잃은 아이템이 있을 때만) if (hasLostItem) ...[ _buildBenefitRow( context, icon: '🔄', text: '${l10n.deathAdReviveItem}: ${deathInfo.lostItemName}', color: itemRarityColor, ), const SizedBox(height: 4), ], // 10분 자동부활 _buildBenefitRow( context, icon: '⏱', text: l10n.deathAdReviveAuto, color: RetroColors.mpOf(context), ), ], ), const SizedBox(height: 6), // 유료 유저 설명 if (isPaidUser) Text( l10n.deathAdRevivePaidDesc, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 9, color: muted, ), textAlign: TextAlign.center, ), ], ), ), ); } /// 혜택 항목 행 Widget _buildBenefitRow( BuildContext context, { required String icon, required String text, required Color color, }) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(icon, style: TextStyle(fontSize: 14, color: color)), const SizedBox(width: 6), Flexible( child: Text( text, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: color, ), overflow: TextOverflow.ellipsis, ), ), ], ); } /// 사망 직전 전투 로그 표시 Widget _buildCombatLog(BuildContext context) { final events = deathInfo.lastCombatEvents; final gold = RetroColors.goldOf(context); final background = RetroColors.backgroundOf(context); final borderColor = RetroColors.borderOf(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('📜', style: TextStyle(fontSize: 17)), const SizedBox(width: 6), Text( l10n.deathCombatLog.toUpperCase(), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: gold, ), ), ], ), const SizedBox(height: 8), Container( constraints: const BoxConstraints(maxHeight: 100), decoration: BoxDecoration( color: background, border: Border.all(color: borderColor, width: 2), ), child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(6), itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; return _buildCombatEventTile(context, event); }, ), ), ], ); } /// 개별 전투 이벤트 타일 Widget _buildCombatEventTile(BuildContext context, CombatEvent event) { final (asciiIcon, color, message) = _formatCombatEvent(context, event); return Padding( padding: const EdgeInsets.symmetric(vertical: 1), child: Row( children: [ Text(asciiIcon, style: TextStyle(fontSize: 15, color: color)), const SizedBox(width: 4), Expanded( child: Text( message, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 14, color: color, ), overflow: TextOverflow.ellipsis, ), ), ], ), ); } /// 전투 이벤트를 ASCII 아이콘, 색상, 메시지로 포맷 (String, Color, String) _formatCombatEvent( BuildContext context, CombatEvent event, ) { final target = event.targetName ?? ''; final gold = RetroColors.goldOf(context); final exp = RetroColors.expOf(context); final hp = RetroColors.hpOf(context); final mp = RetroColors.mpOf(context); return switch (event.type) { CombatEventType.playerAttack => ( event.isCritical ? '⚡' : '⚔', event.isCritical ? gold : exp, event.isCritical ? l10n.combatCritical(event.damage, target) : l10n.combatYouHit(target, event.damage), ), CombatEventType.monsterAttack => ( '💀', hp, l10n.combatMonsterHitsYou(target, event.damage), ), CombatEventType.playerEvade => ( '➤', RetroColors.asciiCyan, l10n.combatEvadedAttackFrom(target), ), CombatEventType.monsterEvade => ( '➤', const Color(0xFFFF9933), l10n.combatMonsterEvaded(target), ), CombatEventType.playerBlock => ( '🛡', mp, l10n.combatBlockedAttack(target, event.damage), ), CombatEventType.playerParry => ( '⚔', const Color(0xFF00CCCC), l10n.combatParriedAttack(target, event.damage), ), CombatEventType.playerSkill => ( '✧', const Color(0xFF9966FF), l10n.combatSkillDamage(event.skillName ?? '', event.damage), ), CombatEventType.playerHeal => ( '♥', exp, l10n.combatHealedFor(event.healAmount), ), CombatEventType.playerBuff => ( '↑', mp, l10n.combatBuffActivated(event.skillName ?? ''), ), CombatEventType.playerDebuff => ( '↓', const Color(0xFFFF6633), l10n.combatDebuffApplied(event.skillName ?? '', target), ), CombatEventType.dotTick => ( '🔥', const Color(0xFFFF6633), l10n.combatDotTick(event.skillName ?? '', event.damage), ), CombatEventType.playerPotion => ( '🧪', exp, l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target), ), CombatEventType.potionDrop => ( '🎁', gold, l10n.combatPotionDrop(event.skillName ?? ''), ), }; } /// 장비 슬롯 이름 반환 String _getSlotName(EquipmentSlot? slot) { if (slot == null) return ''; 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) { if (rarity == null) return Colors.grey; return switch (rarity) { ItemRarity.common => Colors.grey, ItemRarity.uncommon => Colors.green, ItemRarity.rare => Colors.blue, ItemRarity.epic => Colors.purple, ItemRarity.legendary => Colors.orange, }; } }