feat(arena): 아레나 화면 구현
- ArenaScreen: 아레나 메인 화면 - ArenaSetupScreen: 전투 설정 화면 - ArenaBattleScreen: 전투 진행 화면 - 관련 위젯 추가
This commit is contained in:
548
lib/src/features/arena/widgets/arena_equipment_compare_list.dart
Normal file
548
lib/src/features/arena/widgets/arena_equipment_compare_list.dart
Normal file
@@ -0,0 +1,548 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/src/core/engine/item_service.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 _myEquipmentTitle = 'MY EQUIPMENT';
|
||||
const _enemyEquipmentTitle = 'ENEMY EQUIPMENT';
|
||||
const _selectSlotLabel = 'SELECT';
|
||||
const _recommendedLabel = 'BEST';
|
||||
|
||||
/// 좌우 대칭 장비 비교 리스트
|
||||
///
|
||||
/// 내 장비와 상대 장비를 나란히 표시하고,
|
||||
/// 선택 시 인라인으로 비교 정보를 확장
|
||||
class ArenaEquipmentCompareList extends StatefulWidget {
|
||||
const ArenaEquipmentCompareList({
|
||||
super.key,
|
||||
required this.myEquipment,
|
||||
required this.enemyEquipment,
|
||||
required this.selectedSlot,
|
||||
required this.onSlotSelected,
|
||||
this.recommendedSlot,
|
||||
});
|
||||
|
||||
/// 내 장비 목록
|
||||
final List<EquipmentItem>? myEquipment;
|
||||
|
||||
/// 상대 장비 목록
|
||||
final List<EquipmentItem>? enemyEquipment;
|
||||
|
||||
/// 현재 선택된 슬롯
|
||||
final EquipmentSlot? selectedSlot;
|
||||
|
||||
/// 슬롯 선택 콜백
|
||||
final ValueChanged<EquipmentSlot> onSlotSelected;
|
||||
|
||||
/// 추천 슬롯 (점수 이득이 가장 큰 슬롯)
|
||||
final EquipmentSlot? recommendedSlot;
|
||||
|
||||
@override
|
||||
State<ArenaEquipmentCompareList> createState() =>
|
||||
_ArenaEquipmentCompareListState();
|
||||
}
|
||||
|
||||
class _ArenaEquipmentCompareListState extends State<ArenaEquipmentCompareList> {
|
||||
/// 현재 확장된 슬롯 (탭하여 비교 중인 슬롯)
|
||||
EquipmentSlot? _expandedSlot;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 헤더 (좌우 타이틀)
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 8),
|
||||
// 장비 리스트
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: EquipmentSlot.values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final slot = EquipmentSlot.values[index];
|
||||
return _buildSlotRow(context, slot);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// 내 장비 타이틀
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
_myEquipmentTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: Colors.blue,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 상대 장비 타이틀
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
_enemyEquipmentTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: Colors.red,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlotRow(BuildContext context, EquipmentSlot slot) {
|
||||
final myItem = _findItem(slot, widget.myEquipment);
|
||||
final enemyItem = _findItem(slot, widget.enemyEquipment);
|
||||
final isExpanded = _expandedSlot == slot;
|
||||
final isSelected = widget.selectedSlot == slot;
|
||||
final isRecommended = widget.recommendedSlot == slot;
|
||||
|
||||
final myScore =
|
||||
myItem != null ? ItemService.calculateEquipmentScore(myItem) : 0;
|
||||
final enemyScore =
|
||||
enemyItem != null ? ItemService.calculateEquipmentScore(enemyItem) : 0;
|
||||
final scoreDiff = enemyScore - myScore;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 슬롯 행 (좌우 대칭)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expandedSlot = isExpanded ? null : slot;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? RetroColors.goldOf(context).withValues(alpha: 0.2)
|
||||
: isRecommended
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: isExpanded
|
||||
? RetroColors.panelBgOf(context)
|
||||
: Colors.transparent,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: RetroColors.borderOf(context).withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 내 장비
|
||||
Expanded(child: _buildEquipmentCell(context, myItem, myScore, Colors.blue)),
|
||||
// 슬롯 아이콘 (중앙)
|
||||
_buildSlotIndicator(context, slot, isSelected, isRecommended, scoreDiff),
|
||||
// 상대 장비
|
||||
Expanded(child: _buildEquipmentCell(context, enemyItem, enemyScore, Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 확장된 비교 패널
|
||||
if (isExpanded)
|
||||
_buildExpandedPanel(context, slot, myItem, enemyItem, scoreDiff),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 장비 셀 (한쪽)
|
||||
Widget _buildEquipmentCell(
|
||||
BuildContext context,
|
||||
EquipmentItem? item,
|
||||
int score,
|
||||
Color accentColor,
|
||||
) {
|
||||
final hasItem = item != null && item.isNotEmpty;
|
||||
final rarityColor = hasItem ? _getRarityColor(item.rarity) : Colors.grey;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// 아이템 이름
|
||||
Expanded(
|
||||
child: Text(
|
||||
hasItem ? item.name : '-',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 5,
|
||||
color: hasItem ? rarityColor : RetroColors.textMutedOf(context),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// 점수
|
||||
Text(
|
||||
'$score',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 5,
|
||||
color: hasItem
|
||||
? RetroColors.textSecondaryOf(context)
|
||||
: RetroColors.textMutedOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 슬롯 인디케이터 (중앙)
|
||||
Widget _buildSlotIndicator(
|
||||
BuildContext context,
|
||||
EquipmentSlot slot,
|
||||
bool isSelected,
|
||||
bool isRecommended,
|
||||
int scoreDiff,
|
||||
) {
|
||||
final Color borderColor;
|
||||
final Color bgColor;
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = RetroColors.goldOf(context);
|
||||
bgColor = RetroColors.goldOf(context).withValues(alpha: 0.3);
|
||||
} else if (isRecommended) {
|
||||
borderColor = Colors.green;
|
||||
bgColor = Colors.green.withValues(alpha: 0.2);
|
||||
} else {
|
||||
borderColor = RetroColors.borderOf(context);
|
||||
bgColor = RetroColors.panelBgOf(context);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 56,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: borderColor, width: isSelected ? 2 : 1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 슬롯 아이콘
|
||||
Icon(
|
||||
_getSlotIcon(slot),
|
||||
size: 12,
|
||||
color: isSelected
|
||||
? RetroColors.goldOf(context)
|
||||
: RetroColors.textSecondaryOf(context),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// 점수 변화
|
||||
_buildScoreDiffBadge(context, scoreDiff, isRecommended),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 점수 변화 뱃지
|
||||
Widget _buildScoreDiffBadge(
|
||||
BuildContext context,
|
||||
int scoreDiff,
|
||||
bool isRecommended,
|
||||
) {
|
||||
final Color diffColor;
|
||||
final String diffText;
|
||||
|
||||
if (scoreDiff > 0) {
|
||||
diffColor = Colors.green;
|
||||
diffText = '+$scoreDiff';
|
||||
} else if (scoreDiff < 0) {
|
||||
diffColor = Colors.red;
|
||||
diffText = '$scoreDiff';
|
||||
} else {
|
||||
diffColor = RetroColors.textMutedOf(context);
|
||||
diffText = '±0';
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (isRecommended) ...[
|
||||
Text(
|
||||
_recommendedLabel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 4,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
Text(
|
||||
diffText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 5,
|
||||
color: diffColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 확장된 비교 패널
|
||||
Widget _buildExpandedPanel(
|
||||
BuildContext context,
|
||||
EquipmentSlot slot,
|
||||
EquipmentItem? myItem,
|
||||
EquipmentItem? enemyItem,
|
||||
int scoreDiff,
|
||||
) {
|
||||
final Color resultColor;
|
||||
final String resultText;
|
||||
final IconData resultIcon;
|
||||
|
||||
if (scoreDiff > 0) {
|
||||
resultColor = Colors.green;
|
||||
resultText = 'You will GAIN +$scoreDiff';
|
||||
resultIcon = Icons.arrow_upward;
|
||||
} else if (scoreDiff < 0) {
|
||||
resultColor = Colors.red;
|
||||
resultText = 'You will LOSE $scoreDiff';
|
||||
resultIcon = Icons.arrow_downward;
|
||||
} else {
|
||||
resultColor = RetroColors.textMutedOf(context);
|
||||
resultText = 'Even trade';
|
||||
resultIcon = Icons.swap_horiz;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: resultColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: resultColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 상세 비교 (좌우)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 내 아이템 상세
|
||||
Expanded(child: _buildItemDetail(context, myItem, Colors.blue)),
|
||||
// VS 아이콘
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(
|
||||
Icons.swap_horiz,
|
||||
color: RetroColors.textMutedOf(context),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
// 상대 아이템 상세
|
||||
Expanded(child: _buildItemDetail(context, enemyItem, Colors.red)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 교환 결과
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: resultColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(resultIcon, color: resultColor, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
resultText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: resultColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 선택 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
widget.onSlotSelected(slot);
|
||||
setState(() => _expandedSlot = null);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: RetroColors.goldOf(context),
|
||||
foregroundColor: RetroColors.backgroundOf(context),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_selectSlotLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
color: RetroColors.backgroundOf(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 아이템 상세 정보
|
||||
Widget _buildItemDetail(
|
||||
BuildContext context,
|
||||
EquipmentItem? item,
|
||||
Color accentColor,
|
||||
) {
|
||||
final hasItem = item != null && item.isNotEmpty;
|
||||
final rarityColor = hasItem ? _getRarityColor(item.rarity) : Colors.grey;
|
||||
final score = hasItem ? ItemService.calculateEquipmentScore(item) : 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 아이템 이름
|
||||
Text(
|
||||
hasItem ? item.name : '(Empty)',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: hasItem ? rarityColor : RetroColors.textMutedOf(context),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 점수
|
||||
Text(
|
||||
'Score: $score',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 5,
|
||||
color: RetroColors.textSecondaryOf(context),
|
||||
),
|
||||
),
|
||||
// 스탯
|
||||
if (hasItem) ...[
|
||||
const SizedBox(height: 4),
|
||||
_buildItemStats(context, item),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 아이템 스탯 표시
|
||||
Widget _buildItemStats(BuildContext context, EquipmentItem item) {
|
||||
final stats = item.stats;
|
||||
final statWidgets = <Widget>[];
|
||||
|
||||
if (stats.atk > 0) {
|
||||
statWidgets.add(_buildStatChip('ATK', stats.atk, Colors.red));
|
||||
}
|
||||
if (stats.def > 0) {
|
||||
statWidgets.add(_buildStatChip('DEF', stats.def, Colors.blue));
|
||||
}
|
||||
if (stats.hpBonus > 0) {
|
||||
statWidgets.add(_buildStatChip('HP', stats.hpBonus, Colors.green));
|
||||
}
|
||||
if (stats.mpBonus > 0) {
|
||||
statWidgets.add(_buildStatChip('MP', stats.mpBonus, Colors.purple));
|
||||
}
|
||||
|
||||
if (statWidgets.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Wrap(
|
||||
spacing: 3,
|
||||
runSpacing: 3,
|
||||
children: statWidgets,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatChip(String label, int value, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: Text(
|
||||
'$label +$value',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 4,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
EquipmentItem? _findItem(EquipmentSlot slot, List<EquipmentItem>? items) {
|
||||
if (items == null) return null;
|
||||
for (final item in items) {
|
||||
if (item.slot == slot) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
IconData _getSlotIcon(EquipmentSlot slot) {
|
||||
return switch (slot) {
|
||||
EquipmentSlot.weapon => Icons.gavel,
|
||||
EquipmentSlot.shield => Icons.shield,
|
||||
EquipmentSlot.helm => Icons.sports_mma,
|
||||
EquipmentSlot.hauberk => Icons.checkroom,
|
||||
EquipmentSlot.brassairts => Icons.front_hand,
|
||||
EquipmentSlot.vambraces => Icons.back_hand,
|
||||
EquipmentSlot.gauntlets => Icons.sports_handball,
|
||||
EquipmentSlot.gambeson => Icons.dry_cleaning,
|
||||
EquipmentSlot.cuisses => Icons.airline_seat_legroom_normal,
|
||||
EquipmentSlot.greaves => Icons.snowshoeing,
|
||||
EquipmentSlot.sollerets => Icons.do_not_step,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
181
lib/src/features/arena/widgets/arena_idle_preview.dart
Normal file
181
lib/src/features/arena/widgets/arena_idle_preview.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||
import 'package:asciineverdie/src/core/animation/canvas/ascii_canvas_widget.dart';
|
||||
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart';
|
||||
import 'package:asciineverdie/src/core/animation/character_frames.dart';
|
||||
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 아레나 idle 상태 캐릭터 미리보기 위젯
|
||||
///
|
||||
/// 좌측에 도전자, 우측에 상대(좌우 반전)를 idle 상태로 표시
|
||||
class ArenaIdlePreview extends StatefulWidget {
|
||||
const ArenaIdlePreview({
|
||||
super.key,
|
||||
required this.challengerRaceId,
|
||||
required this.opponentRaceId,
|
||||
});
|
||||
|
||||
/// 도전자 종족 ID
|
||||
final String? challengerRaceId;
|
||||
|
||||
/// 상대 종족 ID
|
||||
final String? opponentRaceId;
|
||||
|
||||
@override
|
||||
State<ArenaIdlePreview> createState() => _ArenaIdlePreviewState();
|
||||
}
|
||||
|
||||
class _ArenaIdlePreviewState extends State<ArenaIdlePreview> {
|
||||
/// 현재 idle 프레임 인덱스 (0~3)
|
||||
int _frameIndex = 0;
|
||||
|
||||
/// 애니메이션 타이머
|
||||
Timer? _timer;
|
||||
|
||||
/// 레이어 버전 (변경 감지용)
|
||||
int _layerVersion = 0;
|
||||
|
||||
/// 캔버스 크기
|
||||
static const int _gridWidth = 32;
|
||||
static const int _gridHeight = 5;
|
||||
|
||||
/// 캐릭터 위치
|
||||
static const int _leftCharX = 4;
|
||||
static const int _rightCharX = 22;
|
||||
static const int _charY = 1; // 상단 여백
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAnimation() {
|
||||
// 200ms마다 프레임 업데이트 (원본 틱 속도)
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
||||
setState(() {
|
||||
_frameIndex = (_frameIndex + 1) % 4;
|
||||
_layerVersion++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final layers = _composeLayers();
|
||||
|
||||
return SizedBox(
|
||||
height: 60,
|
||||
child: AsciiCanvasWidget(
|
||||
layers: layers,
|
||||
gridWidth: _gridWidth,
|
||||
gridHeight: _gridHeight,
|
||||
backgroundOpacity: 0.3,
|
||||
isAnimating: true,
|
||||
layerVersion: _layerVersion,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 레이어 합성
|
||||
List<AsciiLayer> _composeLayers() {
|
||||
final layers = <AsciiLayer>[];
|
||||
|
||||
// 도전자 캐릭터 (좌측, 정방향)
|
||||
final challengerLayer = _createCharacterLayer(
|
||||
widget.challengerRaceId,
|
||||
_leftCharX,
|
||||
mirrored: false,
|
||||
);
|
||||
layers.add(challengerLayer);
|
||||
|
||||
// 상대 캐릭터 (우측, 좌우 반전)
|
||||
final opponentLayer = _createCharacterLayer(
|
||||
widget.opponentRaceId,
|
||||
_rightCharX,
|
||||
mirrored: true,
|
||||
);
|
||||
layers.add(opponentLayer);
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// 캐릭터 레이어 생성
|
||||
AsciiLayer _createCharacterLayer(
|
||||
String? raceId,
|
||||
int xOffset, {
|
||||
required bool mirrored,
|
||||
}) {
|
||||
// 종족별 idle 프레임 조회
|
||||
CharacterFrame frame;
|
||||
if (raceId != null && raceId.isNotEmpty) {
|
||||
final raceData = RaceCharacterFrames.get(raceId);
|
||||
if (raceData != null) {
|
||||
frame = raceData.idle[_frameIndex % raceData.idle.length];
|
||||
} else {
|
||||
frame = getCharacterFrame(BattlePhase.idle, _frameIndex);
|
||||
}
|
||||
} else {
|
||||
frame = getCharacterFrame(BattlePhase.idle, _frameIndex);
|
||||
}
|
||||
|
||||
// 미러링 적용
|
||||
final lines = mirrored ? _mirrorLines(frame.lines) : frame.lines;
|
||||
|
||||
// 셀 변환
|
||||
final cells = _spriteToCells(lines);
|
||||
|
||||
return AsciiLayer(
|
||||
cells: cells,
|
||||
zIndex: 1,
|
||||
offsetX: xOffset,
|
||||
offsetY: _charY,
|
||||
);
|
||||
}
|
||||
|
||||
/// 문자열 좌우 반전
|
||||
List<String> _mirrorLines(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
final chars = line.split('');
|
||||
final mirrored = chars.reversed.map(_mirrorChar).toList();
|
||||
return mirrored.join();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 개별 문자 미러링 (방향성 문자 변환)
|
||||
String _mirrorChar(String char) {
|
||||
return switch (char) {
|
||||
'/' => r'\',
|
||||
r'\' => '/',
|
||||
'(' => ')',
|
||||
')' => '(',
|
||||
'[' => ']',
|
||||
']' => '[',
|
||||
'{' => '}',
|
||||
'}' => '{',
|
||||
'<' => '>',
|
||||
'>' => '<',
|
||||
'┘' => '└',
|
||||
'└' => '┘',
|
||||
'┐' => '┌',
|
||||
'┌' => '┐',
|
||||
'λ' => 'λ', // 대칭
|
||||
_ => char,
|
||||
};
|
||||
}
|
||||
|
||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
return line.split('').map(AsciiCell.fromChar).toList();
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
208
lib/src/features/arena/widgets/arena_rank_card.dart
Normal file
208
lib/src/features/arena/widgets/arena_rank_card.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
|
||||
/// 아레나 순위 카드 위젯
|
||||
///
|
||||
/// 명예의 전당 캐릭터를 순위와 함께 표시
|
||||
class ArenaRankCard extends StatelessWidget {
|
||||
const ArenaRankCard({
|
||||
super.key,
|
||||
required this.entry,
|
||||
required this.rank,
|
||||
required this.score,
|
||||
this.isSelected = false,
|
||||
this.isHighlighted = false,
|
||||
this.compact = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
/// 캐릭터 엔트리
|
||||
final HallOfFameEntry entry;
|
||||
|
||||
/// 순위 (1-based)
|
||||
final int rank;
|
||||
|
||||
/// 아레나 점수
|
||||
final int score;
|
||||
|
||||
/// 선택 상태 (상대로 선택됨)
|
||||
final bool isSelected;
|
||||
|
||||
/// 하이라이트 상태 (내 캐릭터 표시)
|
||||
final bool isHighlighted;
|
||||
|
||||
/// 컴팩트 모드 (작은 사이즈)
|
||||
final bool compact;
|
||||
|
||||
/// 탭 콜백
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rankColor = _getRankColor(rank);
|
||||
final rankIcon = _getRankIcon(rank);
|
||||
|
||||
// 배경색 결정
|
||||
Color bgColor;
|
||||
Color borderColor;
|
||||
if (isSelected) {
|
||||
bgColor = Colors.red.withValues(alpha: 0.15);
|
||||
borderColor = Colors.red;
|
||||
} else if (isHighlighted) {
|
||||
bgColor = Colors.blue.withValues(alpha: 0.15);
|
||||
borderColor = Colors.blue;
|
||||
} else {
|
||||
bgColor = RetroColors.panelBgOf(context);
|
||||
borderColor = RetroColors.borderOf(context);
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: compact ? 2 : 4,
|
||||
horizontal: compact ? 0 : 8,
|
||||
),
|
||||
color: bgColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(compact ? 6 : 8),
|
||||
side: BorderSide(
|
||||
color: borderColor,
|
||||
width: (isSelected || isHighlighted) ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(compact ? 6 : 8),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(compact ? 8 : 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// 순위 배지
|
||||
_buildRankBadge(rankColor, rankIcon),
|
||||
SizedBox(width: compact ? 8 : 12),
|
||||
// 캐릭터 정보
|
||||
Expanded(child: _buildCharacterInfo(context)),
|
||||
// 점수
|
||||
if (!compact) _buildScoreColumn(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRankBadge(Color color, IconData? icon) {
|
||||
final size = compact ? 24.0 : 36.0;
|
||||
final iconSize = compact ? 12.0 : 18.0;
|
||||
final fontSize = compact ? 7.0 : 10.0;
|
||||
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color, width: compact ? 1.5 : 2),
|
||||
),
|
||||
child: Center(
|
||||
child: icon != null
|
||||
? Icon(icon, color: color, size: iconSize)
|
||||
: Text(
|
||||
'$rank',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCharacterInfo(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 이름
|
||||
Text(
|
||||
entry.characterName,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: compact ? 6 : 9,
|
||||
color: RetroColors.textPrimaryOf(context),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: compact ? 2 : 4),
|
||||
// 종족/클래스 + 레벨
|
||||
Text(
|
||||
compact
|
||||
? 'Lv.${entry.level}'
|
||||
: '${GameDataL10n.getRaceName(context, entry.race)} '
|
||||
'${GameDataL10n.getKlassName(context, entry.klass)} '
|
||||
'Lv.${entry.level}',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: compact ? 5 : 7,
|
||||
color: RetroColors.textSecondaryOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScoreColumn(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'$score',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 10,
|
||||
color: RetroColors.goldOf(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'SCORE',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
color: RetroColors.textMutedOf(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getRankColor(int rank) {
|
||||
switch (rank) {
|
||||
case 1:
|
||||
return Colors.amber.shade700;
|
||||
case 2:
|
||||
return Colors.grey.shade500;
|
||||
case 3:
|
||||
return Colors.brown.shade400;
|
||||
default:
|
||||
return Colors.blue.shade400;
|
||||
}
|
||||
}
|
||||
|
||||
IconData? _getRankIcon(int rank) {
|
||||
switch (rank) {
|
||||
case 1:
|
||||
return Icons.emoji_events;
|
||||
case 2:
|
||||
return Icons.workspace_premium;
|
||||
case 3:
|
||||
return Icons.military_tech;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
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