diff --git a/lib/src/features/arena/arena_setup_screen.dart b/lib/src/features/arena/arena_setup_screen.dart index 427e71c..ebbec40 100644 --- a/lib/src/features/arena/arena_setup_screen.dart +++ b/lib/src/features/arena/arena_setup_screen.dart @@ -55,9 +55,12 @@ class _ArenaSetupScreenState extends State { /// 자동 결정된 상대 HallOfFameEntry? _opponent; - /// 선택된 베팅 슬롯 + /// 선택된 베팅 슬롯 (도전자가 상대에게서 뺏을 슬롯) EquipmentSlot? _selectedSlot; + /// 상대가 선택한 베팅 슬롯 (패배 시 뺏길 슬롯) + EquipmentSlot? _opponentBettingSlot; + @override void initState() { super.initState(); @@ -71,9 +74,13 @@ class _ArenaSetupScreenState extends State { void _selectChallenger(HallOfFameEntry entry) { final opponent = _arenaService.findOpponent(widget.hallOfFame, entry.id); + // AI가 도전자에게서 약탈할 슬롯 미리 계산 + final opponentSlot = _arenaService.selectOpponentBettingSlot(entry); + setState(() { _challenger = entry; _opponent = opponent; + _opponentBettingSlot = opponentSlot; _step = 1; }); } @@ -81,14 +88,16 @@ class _ArenaSetupScreenState extends State { void _startBattle() { if (_challenger == null || _opponent == null || - _selectedSlot == null) { + _selectedSlot == null || + _opponentBettingSlot == null) { return; } final match = ArenaMatch( challenger: _challenger!, opponent: _opponent!, - bettingSlot: _selectedSlot!, + challengerBettingSlot: _selectedSlot!, + opponentBettingSlot: _opponentBettingSlot!, ); final navigator = Navigator.of(context); @@ -188,7 +197,7 @@ class _ArenaSetupScreenState extends State { ), // 상단 캐릭터 정보 (좌우 대칭) _buildCharacterHeaders(), - // 장비 비교 리스트 + // 장비 비교 리스트 (양방향 베팅 표시 포함) Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), @@ -197,6 +206,7 @@ class _ArenaSetupScreenState extends State { enemyEquipment: _opponent?.finalEquipment, selectedSlot: _selectedSlot, recommendedSlot: recommendedSlot, + opponentBettingSlot: _opponentBettingSlot, onSlotSelected: (slot) { setState(() => _selectedSlot = slot); }, @@ -209,14 +219,16 @@ class _ArenaSetupScreenState extends State { ); } - /// 추천 슬롯 계산 (점수 이득이 가장 큰 슬롯) + /// 추천 슬롯 계산 (점수 이득이 가장 큰 슬롯, 무기 제외) EquipmentSlot? _calculateRecommendedSlot() { if (_challenger == null || _opponent == null) return null; EquipmentSlot? bestSlot; int maxGain = 0; - for (final slot in EquipmentSlot.values) { + // 베팅 가능한 슬롯만 검사 (무기 제외) + final bettableSlots = _arenaService.getBettableSlots(); + for (final slot in bettableSlots) { final myItem = _findItem(slot, _challenger!.finalEquipment); final enemyItem = _findItem(slot, _opponent!.finalEquipment); diff --git a/lib/src/features/arena/widgets/arena_equipment_compare_list.dart b/lib/src/features/arena/widgets/arena_equipment_compare_list.dart index 203b46a..0ee59a8 100644 --- a/lib/src/features/arena/widgets/arena_equipment_compare_list.dart +++ b/lib/src/features/arena/widgets/arena_equipment_compare_list.dart @@ -11,6 +11,7 @@ const _myEquipmentTitle = 'MY EQUIPMENT'; const _enemyEquipmentTitle = 'ENEMY EQUIPMENT'; const _selectedLabel = 'SELECTED'; const _recommendedLabel = 'BEST'; +const _weaponLockedLabel = 'LOCKED'; /// 좌우 대칭 장비 비교 리스트 /// @@ -24,6 +25,7 @@ class ArenaEquipmentCompareList extends StatefulWidget { required this.selectedSlot, required this.onSlotSelected, this.recommendedSlot, + this.opponentBettingSlot, }); /// 내 장비 목록 @@ -32,7 +34,7 @@ class ArenaEquipmentCompareList extends StatefulWidget { /// 상대 장비 목록 final List? enemyEquipment; - /// 현재 선택된 슬롯 + /// 현재 선택된 슬롯 (내가 상대에게서 뺏을 슬롯) final EquipmentSlot? selectedSlot; /// 슬롯 선택 콜백 @@ -41,6 +43,9 @@ class ArenaEquipmentCompareList extends StatefulWidget { /// 추천 슬롯 (점수 이득이 가장 큰 슬롯) final EquipmentSlot? recommendedSlot; + /// 상대가 선택한 슬롯 (패배 시 내가 잃을 슬롯) + final EquipmentSlot? opponentBettingSlot; + @override State createState() => _ArenaEquipmentCompareListState(); @@ -162,6 +167,14 @@ class _ArenaEquipmentCompareListState extends State { final isSelected = widget.selectedSlot == slot; final isRecommended = widget.recommendedSlot == slot; + // 무기 슬롯은 선택 불가 (보호됨) + final isLocked = slot == EquipmentSlot.weapon; + + // 양방향 베팅 상태 + final isMyTarget = isSelected; // 내가 선택 = 상대 장비 획득 예정 + final isOpponentTarget = + widget.opponentBettingSlot == slot; // 상대가 선택 = 내 장비 손실 예정 + final myScore = myItem != null ? ItemService.calculateEquipmentScore(myItem) : 0; final enemyScore = @@ -172,29 +185,29 @@ class _ArenaEquipmentCompareListState extends State { children: [ // 슬롯 행 (좌우 대칭) GestureDetector( - onTap: () { - // 탭하면 즉시 선택 + 확장 + 자동 스크롤 - widget.onSlotSelected(slot); - setState(() { - _expandedSlot = isExpanded ? null : slot; - }); - // 확장될 때만 스크롤 (다음 프레임에서 실행) - if (!isExpanded) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToSlot(slot); - }); - } - }, + onTap: isLocked + ? null + : () { + // 탭하면 즉시 선택 + 확장 + 자동 스크롤 + widget.onSlotSelected(slot); + setState(() { + _expandedSlot = isExpanded ? null : slot; + }); + // 확장될 때만 스크롤 (다음 프레임에서 실행) + if (!isExpanded) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToSlot(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, + color: isLocked + ? RetroColors.borderOf(context).withValues(alpha: 0.1) + : isExpanded + ? RetroColors.panelBgOf(context) + : Colors.transparent, border: Border( bottom: BorderSide( color: RetroColors.borderOf(context).withValues(alpha: 0.3), @@ -203,60 +216,127 @@ class _ArenaEquipmentCompareListState extends State { ), child: Row( children: [ - // 내 장비 - Expanded(child: _buildEquipmentCell(context, myItem, myScore, Colors.blue)), + // 내 장비 (상대가 노리면 빨간 배경) + Expanded( + child: _buildEquipmentCell( + context, + myItem, + myScore, + Colors.blue, + isLocked: isLocked, + isTargetedByOpponent: isOpponentTarget, + ), + ), // 슬롯 아이콘 (중앙) - _buildSlotIndicator(context, slot, isSelected, isRecommended, scoreDiff), - // 상대 장비 - Expanded(child: _buildEquipmentCell(context, enemyItem, enemyScore, Colors.red)), + _buildSlotIndicator( + context, + slot, + isSelected, + isRecommended, + scoreDiff, + isLocked: isLocked, + isOpponentTarget: isOpponentTarget, + ), + // 상대 장비 (내가 선택하거나 추천이면 녹색 배경) + Expanded( + child: _buildEquipmentCell( + context, + enemyItem, + enemyScore, + Colors.red, + isLocked: isLocked, + isMyTarget: isMyTarget, + isRecommended: isRecommended && !isMyTarget, + ), + ), ], ), ), ), - // 확장된 비교 패널 - if (isExpanded) + // 확장된 비교 패널 (잠긴 슬롯은 확장 불가) + if (isExpanded && !isLocked) _buildExpandedPanel(context, slot, myItem, enemyItem, scoreDiff), ], ); } /// 장비 셀 (한쪽) + /// + /// [isTargetedByOpponent] 상대가 이 슬롯을 노림 (좌측 - 내 장비 손실) + /// [isMyTarget] 내가 이 슬롯을 선택함 (우측 - 상대 장비 획득) + /// [isRecommended] 추천 슬롯 (우측 - 획득 추천) Widget _buildEquipmentCell( BuildContext context, EquipmentItem? item, int score, - Color accentColor, - ) { + Color accentColor, { + bool isLocked = false, + bool isTargetedByOpponent = false, + bool isMyTarget = false, + bool isRecommended = false, + }) { final hasItem = item != null && item.isNotEmpty; final rarityColor = hasItem ? _getRarityColor(item.rarity) : Colors.grey; - return Row( - children: [ - // 아이템 이름 - Expanded( - child: Text( - hasItem ? item.name : '-', + // 배경색 결정 + Color? bgColor; + Color? borderColor; + if (!isLocked) { + if (isTargetedByOpponent) { + bgColor = Colors.red.withValues(alpha: 0.2); // 손실 예정 + borderColor = Colors.red.withValues(alpha: 0.5); + } else if (isMyTarget) { + bgColor = Colors.green.withValues(alpha: 0.25); // 획득 예정 + borderColor = Colors.green.withValues(alpha: 0.6); + } else if (isRecommended) { + bgColor = Colors.green.withValues(alpha: 0.15); // 추천 + borderColor = Colors.green.withValues(alpha: 0.4); + } + } + + final textColor = isLocked + ? RetroColors.textMutedOf(context) + : hasItem + ? rarityColor + : RetroColors.textMutedOf(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(4), + border: borderColor != null ? Border.all(color: borderColor) : null, + ), + child: Row( + children: [ + // 아이템 이름 + Expanded( + child: Text( + hasItem ? item.name : '-', + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: textColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + // 점수 + Text( + '$score', style: TextStyle( fontFamily: 'PressStart2P', - fontSize: 5, - color: hasItem ? rarityColor : RetroColors.textMutedOf(context), + fontSize: 6, + color: isLocked + ? RetroColors.textMutedOf(context) + : hasItem + ? RetroColors.textSecondaryOf(context) + : RetroColors.textMutedOf(context), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - // 점수 - Text( - '$score', - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 5, - color: hasItem - ? RetroColors.textSecondaryOf(context) - : RetroColors.textMutedOf(context), - ), - ), - ], + ], + ), ); } @@ -266,14 +346,27 @@ class _ArenaEquipmentCompareListState extends State { EquipmentSlot slot, bool isSelected, bool isRecommended, - int scoreDiff, - ) { + int scoreDiff, { + bool isLocked = false, + bool isOpponentTarget = false, + }) { final Color borderColor; final Color bgColor; - if (isSelected) { + if (isLocked) { + borderColor = RetroColors.borderOf(context).withValues(alpha: 0.5); + bgColor = RetroColors.borderOf(context).withValues(alpha: 0.1); + } else if (isSelected && isOpponentTarget) { + // 양쪽 모두 선택 - 금색 테두리 유지 borderColor = RetroColors.goldOf(context); bgColor = RetroColors.goldOf(context).withValues(alpha: 0.3); + } else if (isSelected) { + borderColor = RetroColors.goldOf(context); + bgColor = RetroColors.goldOf(context).withValues(alpha: 0.3); + } else if (isOpponentTarget) { + // 상대만 선택 - 빨간 표시 + borderColor = Colors.red.withValues(alpha: 0.7); + bgColor = Colors.red.withValues(alpha: 0.15); } else if (isRecommended) { borderColor = Colors.green; bgColor = Colors.green.withValues(alpha: 0.2); @@ -295,15 +388,27 @@ class _ArenaEquipmentCompareListState extends State { children: [ // 슬롯 아이콘 Icon( - _getSlotIcon(slot), + isLocked ? Icons.lock : _getSlotIcon(slot), size: 12, - color: isSelected - ? RetroColors.goldOf(context) - : RetroColors.textSecondaryOf(context), + color: isLocked + ? RetroColors.textMutedOf(context) + : isSelected + ? RetroColors.goldOf(context) + : RetroColors.textSecondaryOf(context), ), const SizedBox(height: 2), - // 점수 변화 - _buildScoreDiffBadge(context, scoreDiff, isRecommended), + // 잠금 표시 또는 점수 변화 + if (isLocked) + Text( + _weaponLockedLabel, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 4, + color: RetroColors.textMutedOf(context), + ), + ) + else + _buildScoreDiffBadge(context, scoreDiff, isRecommended), ], ), ); diff --git a/lib/src/features/arena/widgets/arena_result_dialog.dart b/lib/src/features/arena/widgets/arena_result_dialog.dart index e026812..2c6d87c 100644 --- a/lib/src/features/arena/widgets/arena_result_dialog.dart +++ b/lib/src/features/arena/widgets/arena_result_dialog.dart @@ -33,7 +33,10 @@ class ArenaResultDialog extends StatelessWidget { Widget build(BuildContext context) { final isVictory = result.isVictory; final resultColor = isVictory ? Colors.amber : Colors.red; - final slot = result.match.bettingSlot; + // 승패에 따라 교환 슬롯 결정 + final slot = isVictory + ? result.match.challengerBettingSlot + : result.match.opponentBettingSlot; return AlertDialog( backgroundColor: RetroColors.panelBgOf(context), diff --git a/lib/src/features/arena/widgets/arena_result_panel.dart b/lib/src/features/arena/widgets/arena_result_panel.dart index b3ea5d2..9aa2ddb 100644 --- a/lib/src/features/arena/widgets/arena_result_panel.dart +++ b/lib/src/features/arena/widgets/arena_result_panel.dart @@ -243,9 +243,15 @@ class _ArenaResultPanelState extends State } Widget _buildExchangeSection(BuildContext context) { - final slot = widget.result.match.bettingSlot; final isVictory = widget.result.isVictory; + // 승패에 따라 교환 슬롯 결정 + // 승리: 도전자가 선택한 슬롯(상대에게서 약탈) + // 패배: 상대가 선택한 슬롯(도전자에게서 약탈당함) + final slot = isVictory + ? widget.result.match.challengerBettingSlot + : widget.result.match.opponentBettingSlot; + // 도전자의 교환 결과 final oldItem = _findItem( widget.result.match.challenger.finalEquipment,