feat(arena): 아레나 모델 추가
- ArenaMatch: 아레나 매치 상태 모델 - HallOfFame: 명예의 전당 모델 추가 - MonsterCombatStats: 아레나용 스탯 생성 메서드 추가
This commit is contained in:
111
lib/src/core/model/arena_match.dart
Normal file
111
lib/src/core/model/arena_match.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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/combat_stats.dart';
|
||||||
import 'package:asciineverdie/src/core/model/equipment_item.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';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
/// 명예의 전당 엔트리 (Phase 10: Hall of Fame Entry)
|
/// 명예의 전당 엔트리 (Phase 10: Hall of Fame Entry)
|
||||||
@@ -80,6 +82,39 @@ class HallOfFameEntry {
|
|||||||
'${clearedAt.day.toString().padLeft(2, '0')}';
|
'${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 생성
|
/// GameState에서 HallOfFameEntry 생성
|
||||||
factory HallOfFameEntry.fromGameState({
|
factory HallOfFameEntry.fromGameState({
|
||||||
required GameState state,
|
required GameState state,
|
||||||
@@ -181,6 +216,22 @@ class HallOfFame {
|
|||||||
/// 비어있는지 확인
|
/// 비어있는지 확인
|
||||||
bool get isEmpty => entries.isEmpty;
|
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으로 직렬화
|
/// JSON으로 직렬화
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {'entries': entries.map((e) => e.toJson()).toList()};
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||||
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
|
|
||||||
/// 몬스터 공격 속도 타입
|
/// 몬스터 공격 속도 타입
|
||||||
@@ -238,4 +239,25 @@ class MonsterCombatStats {
|
|||||||
attackDelayMs: 1000,
|
attackDelayMs: 1000,
|
||||||
expReward: 15,
|
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에서는 경험치 없음
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user