feat(arena): 아레나 서비스 및 아이템 서비스 개선

- ArenaService 로직 확장
- ArenaMatch 모델 업데이트
- ItemService 아레나 지원 추가
This commit is contained in:
JiWoong Sul
2026-01-07 20:21:50 +09:00
parent c3a8bc305a
commit 699ae3b7f3
3 changed files with 132 additions and 15 deletions

View File

@@ -1,5 +1,6 @@
import 'package:asciineverdie/data/skill_data.dart'; import 'package:asciineverdie/data/skill_data.dart';
import 'package:asciineverdie/src/core/engine/combat_calculator.dart'; import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
import 'package:asciineverdie/src/core/engine/item_service.dart';
import 'package:asciineverdie/src/core/engine/skill_service.dart'; import 'package:asciineverdie/src/core/engine/skill_service.dart';
import 'package:asciineverdie/src/core/model/arena_match.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_item.dart';
@@ -668,18 +669,58 @@ class ArenaService {
} }
} }
// ============================================================================ // ============================================================================
// 장비 교환 // AI 베팅 슬롯 선택
// ============================================================================ // ============================================================================
/// 장비 교환 (같은 슬롯끼리) /// AI가 도전자에게서 약탈할 슬롯 자동 선택
/// ///
/// 승자가 선택한 슬롯의 장비를 서로 교환 /// 도전자의 가장 좋은 장비 슬롯 선택 (무기 제외)
EquipmentSlot selectOpponentBettingSlot(HallOfFameEntry challenger) {
final equipment = challenger.finalEquipment ?? [];
if (equipment.isEmpty) {
// 장비가 없으면 기본 슬롯 (투구)
return EquipmentSlot.helm;
}
// 무기를 제외한 장비 중 가장 높은 점수의 슬롯 선택
EquipmentSlot? bestSlot;
int bestScore = -1;
for (final item in equipment) {
// 무기는 약탈 불가
if (item.slot == EquipmentSlot.weapon) continue;
if (item.isEmpty) continue;
final score = ItemService.calculateEquipmentScore(item);
if (score > bestScore) {
bestScore = score;
bestSlot = item.slot;
}
}
// 유효한 슬롯이 없으면 투구 선택
return bestSlot ?? EquipmentSlot.helm;
}
/// 베팅 가능한 슬롯 목록 반환 (무기 제외)
List<EquipmentSlot> getBettableSlots() {
return EquipmentSlot.values
.where((slot) => slot != EquipmentSlot.weapon)
.toList();
}
// ============================================================================
// 장비 약탈
// ============================================================================
/// 장비 약탈 (승자가 패자의 베팅 슬롯 장비 획득)
///
/// - 승자: 자신이 선택한 슬롯의 패자 장비 획득
/// - 패자: 해당 슬롯 장비 손실 → 기본 장비로 대체
(HallOfFameEntry, HallOfFameEntry) _exchangeEquipment({ (HallOfFameEntry, HallOfFameEntry) _exchangeEquipment({
required ArenaMatch match, required ArenaMatch match,
required bool isVictory, required bool isVictory,
}) { }) {
final slot = match.bettingSlot;
// 도전자 장비 목록 복사 // 도전자 장비 목록 복사
final challengerEquipment = final challengerEquipment =
List<EquipmentItem>.from(match.challenger.finalEquipment ?? []); List<EquipmentItem>.from(match.challenger.finalEquipment ?? []);
@@ -688,13 +729,29 @@ class ArenaService {
final opponentEquipment = final opponentEquipment =
List<EquipmentItem>.from(match.opponent.finalEquipment ?? []); List<EquipmentItem>.from(match.opponent.finalEquipment ?? []);
// 해당 슬롯의 장비 찾기 if (isVictory) {
final challengerItem = _findItemBySlot(challengerEquipment, slot); // 도전자 승리: 도전자가 선택한 슬롯의 상대 장비 획득
final opponentItem = _findItemBySlot(opponentEquipment, slot); final winnerSlot = match.challengerBettingSlot;
final lootedItem = _findItemBySlot(opponentEquipment, winnerSlot);
// 장비 교 // 도전자: 약탈한 장비
_replaceItemInList(challengerEquipment, slot, opponentItem); _replaceItemInList(challengerEquipment, winnerSlot, lootedItem);
_replaceItemInList(opponentEquipment, slot, challengerItem);
// 상대: 해당 슬롯 기본 장비로 대체
final defaultItem = _createDefaultEquipment(winnerSlot);
_replaceItemInList(opponentEquipment, winnerSlot, defaultItem);
} else {
// 상대 승리: 상대가 선택한 슬롯의 도전자 장비 획득
final winnerSlot = match.opponentBettingSlot;
final lootedItem = _findItemBySlot(challengerEquipment, winnerSlot);
// 상대: 약탈한 장비로 교체
_replaceItemInList(opponentEquipment, winnerSlot, lootedItem);
// 도전자: 해당 슬롯 기본 장비로 대체
final defaultItem = _createDefaultEquipment(winnerSlot);
_replaceItemInList(challengerEquipment, winnerSlot, defaultItem);
}
// 업데이트된 엔트리 생성 // 업데이트된 엔트리 생성
final updatedChallenger = match.challenger.copyWith( final updatedChallenger = match.challenger.copyWith(
@@ -731,4 +788,11 @@ class ArenaService {
// 슬롯이 없으면 추가 // 슬롯이 없으면 추가
equipment.add(newItem); equipment.add(newItem);
} }
/// 기본 장비 생성 (Common 등급)
///
/// 패자가 장비를 잃었을 때 빈 슬롯 방지용
EquipmentItem _createDefaultEquipment(EquipmentSlot slot) {
return ItemService.createDefaultEquipmentForSlot(slot);
}
} }

View File

@@ -279,6 +279,51 @@ class ItemService {
return score; return score;
} }
// ============================================================================
// 기본 장비 생성
// ============================================================================
/// 슬롯별 기본 장비 생성 (Common 등급, 레벨 1)
///
/// 아레나에서 패배하여 장비를 잃었을 때 빈 슬롯 방지용
static EquipmentItem createDefaultEquipmentForSlot(EquipmentSlot slot) {
final name = _getDefaultItemName(slot);
const rarity = ItemRarity.common;
// 기본 스탯 (레벨 1 기준)
final stats = switch (slot) {
EquipmentSlot.weapon => const ItemStats(atk: 2, attackSpeed: 1000),
EquipmentSlot.shield => const ItemStats(def: 1, blockRate: 0.05),
_ => const ItemStats(def: 1),
};
return EquipmentItem(
name: name,
slot: slot,
level: 1,
weight: 1,
stats: stats,
rarity: rarity,
);
}
/// 슬롯별 기본 장비 이름
static String _getDefaultItemName(EquipmentSlot slot) {
return switch (slot) {
EquipmentSlot.weapon => 'Wooden Stick',
EquipmentSlot.shield => 'Wooden Shield',
EquipmentSlot.helm => 'Cloth Cap',
EquipmentSlot.hauberk => 'Torn Shirt',
EquipmentSlot.brassairts => 'Cloth Wraps',
EquipmentSlot.vambraces => 'Worn Bracers',
EquipmentSlot.gauntlets => 'Tattered Gloves',
EquipmentSlot.gambeson => 'Ragged Tunic',
EquipmentSlot.cuisses => 'Worn Pants',
EquipmentSlot.greaves => 'Cloth Leggings',
EquipmentSlot.sollerets => 'Worn Sandals',
};
}
// ============================================================================ // ============================================================================
// 자동 장착 // 자동 장착
// ============================================================================ // ============================================================================

View File

@@ -3,12 +3,13 @@ import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
/// 아레나 대전 정보 /// 아레나 대전 정보
/// ///
/// 도전자와 상대의 정보, 베팅 슬롯을 포함 /// 도전자와 상대의 정보, 양방향 베팅 슬롯을 포함
class ArenaMatch { class ArenaMatch {
const ArenaMatch({ const ArenaMatch({
required this.challenger, required this.challenger,
required this.opponent, required this.opponent,
required this.bettingSlot, required this.challengerBettingSlot,
required this.opponentBettingSlot,
}); });
/// 도전자 (내 캐릭터) /// 도전자 (내 캐릭터)
@@ -17,14 +18,21 @@ class ArenaMatch {
/// 상대 캐릭터 /// 상대 캐릭터
final HallOfFameEntry opponent; final HallOfFameEntry opponent;
/// 베팅 슬롯 (같은 슬롯 교환) /// 도전자 베팅 슬롯 (승리 시 상대에게서 빼앗을 슬롯)
final EquipmentSlot bettingSlot; final EquipmentSlot challengerBettingSlot;
/// 상대 베팅 슬롯 (상대 승리 시 도전자에게서 빼앗을 슬롯)
final EquipmentSlot opponentBettingSlot;
/// 도전자 순위 /// 도전자 순위
int get challengerRank => 0; // ArenaService에서 계산 int get challengerRank => 0; // ArenaService에서 계산
/// 상대 순위 /// 상대 순위
int get opponentRank => 0; // ArenaService에서 계산 int get opponentRank => 0; // ArenaService에서 계산
/// 기존 bettingSlot 호환용 (deprecated)
@Deprecated('Use challengerBettingSlot instead')
EquipmentSlot get bettingSlot => challengerBettingSlot;
} }
/// 아레나 대전 결과 /// 아레나 대전 결과