diff --git a/lib/src/core/model/arena_match.dart b/lib/src/core/model/arena_match.dart new file mode 100644 index 0000000..98e401f --- /dev/null +++ b/lib/src/core/model/arena_match.dart @@ -0,0 +1,111 @@ +import 'package:asciineverdie/src/core/model/equipment_slot.dart'; +import 'package:asciineverdie/src/core/model/hall_of_fame.dart'; + +/// 아레나 대전 정보 +/// +/// 도전자와 상대의 정보, 베팅 슬롯을 포함 +class ArenaMatch { + const ArenaMatch({ + required this.challenger, + required this.opponent, + required this.bettingSlot, + }); + + /// 도전자 (내 캐릭터) + final HallOfFameEntry challenger; + + /// 상대 캐릭터 + final HallOfFameEntry opponent; + + /// 베팅 슬롯 (같은 슬롯 교환) + final EquipmentSlot bettingSlot; + + /// 도전자 순위 + int get challengerRank => 0; // ArenaService에서 계산 + + /// 상대 순위 + int get opponentRank => 0; // ArenaService에서 계산 +} + +/// 아레나 대전 결과 +class ArenaMatchResult { + const ArenaMatchResult({ + required this.match, + required this.isVictory, + required this.turns, + required this.updatedChallenger, + required this.updatedOpponent, + }); + + /// 대전 정보 + final ArenaMatch match; + + /// 도전자 승리 여부 + final bool isVictory; + + /// 전투 턴 수 + final int turns; + + /// 장비 교환 후 업데이트된 도전자 + final HallOfFameEntry updatedChallenger; + + /// 장비 교환 후 업데이트된 상대 + final HallOfFameEntry updatedOpponent; +} + +/// 아레나 전투 턴 (애니메이션용) +class ArenaCombatTurn { + ArenaCombatTurn({ + this.challengerDamage, + this.opponentDamage, + required this.challengerHp, + required this.opponentHp, + required this.challengerHpMax, + required this.opponentHpMax, + this.isChallengerCritical = false, + this.isOpponentCritical = false, + this.isChallengerEvaded = false, + this.isOpponentEvaded = false, + this.isChallengerBlocked = false, + this.isOpponentBlocked = false, + }) : timestamp = DateTime.now().microsecondsSinceEpoch; + + /// 턴 식별용 타임스탬프 + final int timestamp; + + /// 도전자가 입힌 데미지 (null이면 공격 안 함) + final int? challengerDamage; + + /// 상대가 입힌 데미지 (null이면 공격 안 함) + final int? opponentDamage; + + /// 도전자 현재 HP + final int challengerHp; + + /// 상대 현재 HP + final int opponentHp; + + /// 도전자 최대 HP + final int challengerHpMax; + + /// 상대 최대 HP + final int opponentHpMax; + + /// 도전자 크리티컬 여부 + final bool isChallengerCritical; + + /// 상대 크리티컬 여부 + final bool isOpponentCritical; + + /// 도전자 회피 여부 + final bool isChallengerEvaded; + + /// 상대 회피 여부 + final bool isOpponentEvaded; + + /// 도전자 블록 여부 + final bool isChallengerBlocked; + + /// 상대 블록 여부 + final bool isOpponentBlocked; +} diff --git a/lib/src/core/model/hall_of_fame.dart b/lib/src/core/model/hall_of_fame.dart index 28855b6..9ee0b38 100644 --- a/lib/src/core/model/hall_of_fame.dart +++ b/lib/src/core/model/hall_of_fame.dart @@ -1,5 +1,7 @@ +import 'package:asciineverdie/src/core/engine/item_service.dart'; import 'package:asciineverdie/src/core/model/combat_stats.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/game_state.dart'; /// 명예의 전당 엔트리 (Phase 10: Hall of Fame Entry) @@ -80,6 +82,39 @@ class HallOfFameEntry { '${clearedAt.day.toString().padLeft(2, '0')}'; } + /// copyWith 메서드 (아레나 장비 교환용) + HallOfFameEntry copyWith({ + String? id, + String? characterName, + String? race, + String? klass, + int? level, + int? totalPlayTimeMs, + int? totalDeaths, + int? monstersKilled, + int? questsCompleted, + DateTime? clearedAt, + CombatStats? finalStats, + List? finalEquipment, + List>? finalSpells, + }) { + return HallOfFameEntry( + id: id ?? this.id, + characterName: characterName ?? this.characterName, + race: race ?? this.race, + klass: klass ?? this.klass, + level: level ?? this.level, + totalPlayTimeMs: totalPlayTimeMs ?? this.totalPlayTimeMs, + totalDeaths: totalDeaths ?? this.totalDeaths, + monstersKilled: monstersKilled ?? this.monstersKilled, + questsCompleted: questsCompleted ?? this.questsCompleted, + clearedAt: clearedAt ?? this.clearedAt, + finalStats: finalStats ?? this.finalStats, + finalEquipment: finalEquipment ?? this.finalEquipment, + finalSpells: finalSpells ?? this.finalSpells, + ); + } + /// GameState에서 HallOfFameEntry 생성 factory HallOfFameEntry.fromGameState({ required GameState state, @@ -181,6 +216,22 @@ class HallOfFame { /// 비어있는지 확인 bool get isEmpty => entries.isEmpty; + /// ID로 엔트리 조회 + HallOfFameEntry? findById(String id) { + for (final entry in entries) { + if (entry.id == id) return entry; + } + return null; + } + + /// 엔트리 업데이트 (아레나 장비 교환 후) + HallOfFame updateEntry(HallOfFameEntry updated) { + final newEntries = entries.map((e) { + return e.id == updated.id ? updated : e; + }).toList(); + return HallOfFame(entries: newEntries); + } + /// JSON으로 직렬화 Map toJson() { return {'entries': entries.map((e) => e.toJson()).toList()}; @@ -196,3 +247,70 @@ class HallOfFame { ); } } + +/// 아레나 순위 관련 extension +extension HallOfFameArenaX on HallOfFame { + /// 아레나 점수 계산 + /// + /// 점수 = (레벨 × 100) + 장비점수 + (전투력 / 10) + static int calculateArenaScore(HallOfFameEntry entry) { + // 1. 레벨 점수 (주요 지표) + final levelScore = entry.level * 100; + + // 2. 장비 점수 (전체 슬롯 합계) + final equipScore = entry.finalEquipment?.fold( + 0, + (sum, item) => sum + ItemService.calculateEquipmentScore(item), + ) ?? + 0; + + // 3. 전투력 점수 (ATK + DEF + HP/10) + final combatScore = entry.finalStats != null + ? (entry.finalStats!.atk + + entry.finalStats!.def + + entry.finalStats!.hpMax ~/ 10) + : 0; + + return levelScore + equipScore + (combatScore ~/ 10); + } + + /// 아레나 점수 기준 정렬된 엔트리 반환 + List get rankedEntries { + final sorted = List.from(entries) + ..sort((a, b) { + final scoreA = calculateArenaScore(a); + final scoreB = calculateArenaScore(b); + if (scoreA != scoreB) return scoreB.compareTo(scoreA); + // 동점일 경우 먼저 클리어한 캐릭터가 상위 + return a.clearedAt.compareTo(b.clearedAt); + }); + return sorted; + } + + /// 특정 캐릭터의 순위 반환 (1-based) + int getRank(String characterId) { + final ranked = rankedEntries; + for (var i = 0; i < ranked.length; i++) { + if (ranked[i].id == characterId) return i + 1; + } + return -1; + } + + /// 상대의 베팅 슬롯 결정 (최저 장비 점수 슬롯) + static EquipmentSlot findWeakestSlot(HallOfFameEntry entry) { + final equipment = entry.finalEquipment ?? []; + if (equipment.isEmpty) return EquipmentSlot.weapon; + + EquipmentItem weakest = equipment.first; + int lowestScore = ItemService.calculateEquipmentScore(weakest); + + for (final item in equipment) { + final score = ItemService.calculateEquipmentScore(item); + if (score < lowestScore) { + lowestScore = score; + weakest = item; + } + } + return weakest.slot; + } +} diff --git a/lib/src/core/model/monster_combat_stats.dart b/lib/src/core/model/monster_combat_stats.dart index 8eef0d6..5dcc0c2 100644 --- a/lib/src/core/model/monster_combat_stats.dart +++ b/lib/src/core/model/monster_combat_stats.dart @@ -1,3 +1,4 @@ +import 'package:asciineverdie/src/core/model/combat_stats.dart'; import 'package:asciineverdie/src/core/util/balance_constants.dart'; /// 몬스터 공격 속도 타입 @@ -238,4 +239,25 @@ class MonsterCombatStats { attackDelayMs: 1000, expReward: 15, ); + + /// CombatStats에서 MonsterCombatStats 생성 (아레나 PvP용) + /// + /// 플레이어의 CombatStats를 몬스터 형태로 변환하여 + /// 기존 CombatCalculator를 재사용할 수 있게 함. + factory MonsterCombatStats.fromCombatStats(CombatStats stats, String name) { + return MonsterCombatStats( + name: name, + level: 0, // PvP에서는 레벨 페널티 없음 + atk: stats.atk, + def: stats.def, + hpMax: stats.hpMax, + hpCurrent: stats.hpMax, // 풀 HP로 시작 + criRate: stats.criRate, + criDamage: stats.criDamage, + evasion: stats.evasion, + accuracy: stats.accuracy, + attackDelayMs: stats.attackDelayMs, + expReward: 0, // PvP에서는 경험치 없음 + ); + } }