feat(arena): 아레나 서비스 및 아이템 서비스 개선
- ArenaService 로직 확장 - ArenaMatch 모델 업데이트 - ItemService 아레나 지원 추가
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:asciineverdie/data/skill_data.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/model/arena_match.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({
|
||||
required ArenaMatch match,
|
||||
required bool isVictory,
|
||||
}) {
|
||||
final slot = match.bettingSlot;
|
||||
|
||||
// 도전자 장비 목록 복사
|
||||
final challengerEquipment =
|
||||
List<EquipmentItem>.from(match.challenger.finalEquipment ?? []);
|
||||
@@ -688,13 +729,29 @@ class ArenaService {
|
||||
final opponentEquipment =
|
||||
List<EquipmentItem>.from(match.opponent.finalEquipment ?? []);
|
||||
|
||||
// 해당 슬롯의 장비 찾기
|
||||
final challengerItem = _findItemBySlot(challengerEquipment, slot);
|
||||
final opponentItem = _findItemBySlot(opponentEquipment, slot);
|
||||
if (isVictory) {
|
||||
// 도전자 승리: 도전자가 선택한 슬롯의 상대 장비 획득
|
||||
final winnerSlot = match.challengerBettingSlot;
|
||||
final lootedItem = _findItemBySlot(opponentEquipment, winnerSlot);
|
||||
|
||||
// 장비 교환
|
||||
_replaceItemInList(challengerEquipment, slot, opponentItem);
|
||||
_replaceItemInList(opponentEquipment, slot, challengerItem);
|
||||
// 도전자: 약탈한 장비로 교체
|
||||
_replaceItemInList(challengerEquipment, winnerSlot, lootedItem);
|
||||
|
||||
// 상대: 해당 슬롯 기본 장비로 대체
|
||||
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(
|
||||
@@ -731,4 +788,11 @@ class ArenaService {
|
||||
// 슬롯이 없으면 추가
|
||||
equipment.add(newItem);
|
||||
}
|
||||
|
||||
/// 기본 장비 생성 (Common 등급)
|
||||
///
|
||||
/// 패자가 장비를 잃었을 때 빈 슬롯 방지용
|
||||
EquipmentItem _createDefaultEquipment(EquipmentSlot slot) {
|
||||
return ItemService.createDefaultEquipmentForSlot(slot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,51 @@ class ItemService {
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 자동 장착
|
||||
// ============================================================================
|
||||
|
||||
@@ -3,12 +3,13 @@ import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||
|
||||
/// 아레나 대전 정보
|
||||
///
|
||||
/// 도전자와 상대의 정보, 베팅 슬롯을 포함
|
||||
/// 도전자와 상대의 정보, 양방향 베팅 슬롯을 포함
|
||||
class ArenaMatch {
|
||||
const ArenaMatch({
|
||||
required this.challenger,
|
||||
required this.opponent,
|
||||
required this.bettingSlot,
|
||||
required this.challengerBettingSlot,
|
||||
required this.opponentBettingSlot,
|
||||
});
|
||||
|
||||
/// 도전자 (내 캐릭터)
|
||||
@@ -17,14 +18,21 @@ class ArenaMatch {
|
||||
/// 상대 캐릭터
|
||||
final HallOfFameEntry opponent;
|
||||
|
||||
/// 베팅 슬롯 (같은 슬롯 교환)
|
||||
final EquipmentSlot bettingSlot;
|
||||
/// 도전자 베팅 슬롯 (승리 시 상대에게서 빼앗을 슬롯)
|
||||
final EquipmentSlot challengerBettingSlot;
|
||||
|
||||
/// 상대 베팅 슬롯 (상대 승리 시 도전자에게서 빼앗을 슬롯)
|
||||
final EquipmentSlot opponentBettingSlot;
|
||||
|
||||
/// 도전자 순위
|
||||
int get challengerRank => 0; // ArenaService에서 계산
|
||||
|
||||
/// 상대 순위
|
||||
int get opponentRank => 0; // ArenaService에서 계산
|
||||
|
||||
/// 기존 bettingSlot 호환용 (deprecated)
|
||||
@Deprecated('Use challengerBettingSlot instead')
|
||||
EquipmentSlot get bettingSlot => challengerBettingSlot;
|
||||
}
|
||||
|
||||
/// 아레나 대전 결과
|
||||
|
||||
Reference in New Issue
Block a user