feat(arena): 아레나 모델 추가

- ArenaMatch: 아레나 매치 상태 모델
- HallOfFame: 명예의 전당 모델 추가
- MonsterCombatStats: 아레나용 스탯 생성 메서드 추가
This commit is contained in:
JiWoong Sul
2026-01-06 17:54:51 +09:00
parent be56825ef9
commit 4c68b3c7fb
3 changed files with 251 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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<EquipmentItem>? finalEquipment,
List<Map<String, String>>? 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<String, dynamic> 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<int>(
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<HallOfFameEntry> get rankedEntries {
final sorted = List<HallOfFameEntry>.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;
}
}

View File

@@ -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에서는 경험치 없음
);
}
}