feat(arena): 아레나 화면 구현
- ArenaScreen: 아레나 메인 화면 - ArenaSetupScreen: 전투 설정 화면 - ArenaBattleScreen: 전투 진행 화면 - 관련 위젯 추가
This commit is contained in:
437
lib/src/features/arena/widgets/arena_result_dialog.dart
Normal file
437
lib/src/features/arena/widgets/arena_result_dialog.dart
Normal file
@@ -0,0 +1,437 @@
|
||||
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';
|
||||
|
||||
// 아레나 관련 임시 문자열 (추후 l10n으로 이동)
|
||||
const _arenaVictory = 'VICTORY!';
|
||||
const _arenaDefeat = 'DEFEAT...';
|
||||
const _arenaExchange = 'EQUIPMENT EXCHANGE';
|
||||
|
||||
/// 아레나 결과 다이얼로그
|
||||
///
|
||||
/// 전투 승패 및 장비 교환 결과 표시
|
||||
class ArenaResultDialog extends StatelessWidget {
|
||||
const ArenaResultDialog({
|
||||
super.key,
|
||||
required this.result,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
/// 대전 결과
|
||||
final ArenaMatchResult result;
|
||||
|
||||
/// 닫기 콜백
|
||||
final VoidCallback onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isVictory = result.isVictory;
|
||||
final resultColor = isVictory ? Colors.amber : Colors.red;
|
||||
final slot = result.match.bettingSlot;
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: RetroColors.panelBgOf(context),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: resultColor, width: 2),
|
||||
),
|
||||
title: _buildTitle(context, isVictory, resultColor),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 350),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 전투 정보
|
||||
_buildBattleInfo(context),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
// 장비 교환 결과
|
||||
_buildExchangeResult(context, slot),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: onClose,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: resultColor,
|
||||
),
|
||||
child: Text(
|
||||
l10n.buttonConfirm,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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: 28,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isVictory ? _arenaVictory : _arenaDefeat,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
isVictory ? Icons.emoji_events : Icons.sentiment_very_dissatisfied,
|
||||
color: color,
|
||||
size: 28,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBattleInfo(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 도전자 정보
|
||||
_buildFighterInfo(
|
||||
context,
|
||||
result.match.challenger.characterName,
|
||||
result.isVictory,
|
||||
),
|
||||
// VS
|
||||
Text(
|
||||
'VS',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 10,
|
||||
color: RetroColors.textMutedOf(context),
|
||||
),
|
||||
),
|
||||
// 상대 정보
|
||||
_buildFighterInfo(
|
||||
context,
|
||||
result.match.opponent.characterName,
|
||||
!result.isVictory,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFighterInfo(BuildContext context, String name, bool isWinner) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
isWinner ? Icons.emoji_events : Icons.close,
|
||||
color: isWinner ? Colors.amber : Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: isWinner
|
||||
? RetroColors.goldOf(context)
|
||||
: RetroColors.textMutedOf(context),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
isWinner ? 'WINNER' : 'LOSER',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: isWinner ? Colors.amber : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExchangeResult(BuildContext context, EquipmentSlot slot) {
|
||||
// 교환된 장비 찾기
|
||||
final challengerOldItem = _findItem(
|
||||
result.match.challenger.finalEquipment,
|
||||
slot,
|
||||
);
|
||||
final opponentOldItem = _findItem(
|
||||
result.match.opponent.finalEquipment,
|
||||
slot,
|
||||
);
|
||||
|
||||
final challengerNewItem = _findItem(
|
||||
result.updatedChallenger.finalEquipment,
|
||||
slot,
|
||||
);
|
||||
final opponentNewItem = _findItem(
|
||||
result.updatedOpponent.finalEquipment,
|
||||
slot,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 교환 타이틀
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.swap_horiz,
|
||||
color: RetroColors.goldOf(context),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_arenaExchange,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 8,
|
||||
color: RetroColors.goldOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 슬롯 정보
|
||||
Text(
|
||||
_getSlotLabel(slot),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: RetroColors.textSecondaryOf(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 내 캐릭터 장비 변경
|
||||
_buildExchangeRow(
|
||||
context,
|
||||
result.match.challenger.characterName,
|
||||
challengerOldItem,
|
||||
challengerNewItem,
|
||||
result.isVictory,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 상대 장비 변경
|
||||
_buildExchangeRow(
|
||||
context,
|
||||
result.match.opponent.characterName,
|
||||
opponentOldItem,
|
||||
opponentNewItem,
|
||||
!result.isVictory,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExchangeRow(
|
||||
BuildContext context,
|
||||
String name,
|
||||
EquipmentItem? oldItem,
|
||||
EquipmentItem? newItem,
|
||||
bool isWinner,
|
||||
) {
|
||||
final oldScore =
|
||||
oldItem != null ? ItemService.calculateEquipmentScore(oldItem) : 0;
|
||||
final newScore =
|
||||
newItem != null ? ItemService.calculateEquipmentScore(newItem) : 0;
|
||||
final scoreDiff = newScore - oldScore;
|
||||
final isGain = scoreDiff > 0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isWinner
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isWinner
|
||||
? Colors.green.withValues(alpha: 0.3)
|
||||
: Colors.red.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 이름
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: RetroColors.textPrimaryOf(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 장비 변경
|
||||
Row(
|
||||
children: [
|
||||
// 이전 장비
|
||||
Expanded(
|
||||
child: _buildItemChip(
|
||||
context,
|
||||
oldItem,
|
||||
oldScore,
|
||||
isOld: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 14,
|
||||
color: RetroColors.textMutedOf(context),
|
||||
),
|
||||
),
|
||||
// 새 장비
|
||||
Expanded(
|
||||
child: _buildItemChip(
|
||||
context,
|
||||
newItem,
|
||||
newScore,
|
||||
isOld: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 점수 변화
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'${isGain ? '+' : ''}$scoreDiff pt',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: isGain ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemChip(
|
||||
BuildContext context,
|
||||
EquipmentItem? item,
|
||||
int score, {
|
||||
required bool isOld,
|
||||
}) {
|
||||
if (item == null || item.isEmpty) {
|
||||
return Text(
|
||||
'(empty)',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: RetroColors.textMutedOf(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final rarityColor = _getRarityColor(item.rarity);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: rarityColor.withValues(alpha: isOld ? 0.1 : 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: rarityColor.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
EquipmentItem? _findItem(List<EquipmentItem>? 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 아레나 결과 다이얼로그 표시
|
||||
Future<void> showArenaResultDialog(
|
||||
BuildContext context, {
|
||||
required ArenaMatchResult result,
|
||||
required VoidCallback onClose,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => ArenaResultDialog(
|
||||
result: result,
|
||||
onClose: () {
|
||||
Navigator.of(context).pop();
|
||||
onClose();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user