feat(arena): 아레나 서비스 추가
- 아레나 전투 로직 처리 - 명예의 전당 연동
This commit is contained in:
314
lib/src/core/engine/arena_service.dart
Normal file
314
lib/src/core/engine/arena_service.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
import 'package:asciineverdie/src/core/engine/combat_calculator.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_slot.dart';
|
||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// 아레나 서비스
|
||||
///
|
||||
/// 로컬 아레나 대전 시스템의 핵심 로직 담당:
|
||||
/// - 순위 계산 및 상대 결정
|
||||
/// - 전투 실행
|
||||
/// - 장비 교환
|
||||
class ArenaService {
|
||||
ArenaService({DeterministicRandom? rng})
|
||||
: _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
final DeterministicRandom _rng;
|
||||
|
||||
// ============================================================================
|
||||
// 상대 결정
|
||||
// ============================================================================
|
||||
|
||||
/// 상대 결정 (바로 위 순위, 1위면 2위와 대결)
|
||||
///
|
||||
/// [hallOfFame] 명예의 전당
|
||||
/// [challengerId] 도전자 캐릭터 ID
|
||||
/// Returns: 상대 캐릭터 (없으면 null)
|
||||
HallOfFameEntry? findOpponent(HallOfFame hallOfFame, String challengerId) {
|
||||
final ranked = hallOfFame.rankedEntries;
|
||||
if (ranked.length < 2) return null;
|
||||
|
||||
final currentRank = hallOfFame.getRank(challengerId);
|
||||
if (currentRank <= 0) return null;
|
||||
|
||||
// 1위면 2위와 대결
|
||||
if (currentRank == 1) {
|
||||
return ranked[1];
|
||||
}
|
||||
|
||||
// 그 외는 바로 위 순위와 대결
|
||||
return ranked[currentRank - 2];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 전투 실행
|
||||
// ============================================================================
|
||||
|
||||
/// 아레나 전투 실행
|
||||
///
|
||||
/// [match] 대전 정보
|
||||
/// Returns: 대전 결과 (승패, 장비 교환 후 캐릭터)
|
||||
ArenaMatchResult executeCombat(ArenaMatch match) {
|
||||
final calculator = CombatCalculator(rng: _rng);
|
||||
|
||||
// 도전자 스탯 (풀 HP로 시작)
|
||||
final challengerStats = match.challenger.finalStats;
|
||||
final opponentStats = match.opponent.finalStats;
|
||||
|
||||
if (challengerStats == null || opponentStats == null) {
|
||||
// 스탯이 없으면 도전자 패배 처리
|
||||
return ArenaMatchResult(
|
||||
match: match,
|
||||
isVictory: false,
|
||||
turns: 0,
|
||||
updatedChallenger: match.challenger,
|
||||
updatedOpponent: match.opponent,
|
||||
);
|
||||
}
|
||||
|
||||
// 플레이어 스탯 (풀 HP로 초기화)
|
||||
var playerCombatStats = challengerStats.copyWith(
|
||||
hpCurrent: challengerStats.hpMax,
|
||||
mpCurrent: challengerStats.mpMax,
|
||||
);
|
||||
|
||||
// 상대를 몬스터 형태로 변환
|
||||
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||
opponentStats,
|
||||
match.opponent.characterName,
|
||||
);
|
||||
|
||||
// 전투 시뮬레이션
|
||||
int turns = 0;
|
||||
int playerAccum = 0;
|
||||
int opponentAccum = 0;
|
||||
const tickMs = 200;
|
||||
|
||||
while (playerCombatStats.isAlive && opponentMonsterStats.isAlive) {
|
||||
playerAccum += tickMs;
|
||||
opponentAccum += tickMs;
|
||||
|
||||
// 플레이어 공격
|
||||
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
||||
final result = calculator.playerAttackMonster(
|
||||
attacker: playerCombatStats,
|
||||
defender: opponentMonsterStats,
|
||||
);
|
||||
opponentMonsterStats = result.updatedDefender;
|
||||
playerAccum = 0;
|
||||
turns++;
|
||||
}
|
||||
|
||||
// 상대 공격 (살아있을 때만)
|
||||
if (opponentMonsterStats.isAlive &&
|
||||
opponentAccum >= opponentMonsterStats.attackDelayMs) {
|
||||
final result = calculator.monsterAttackPlayer(
|
||||
attacker: opponentMonsterStats,
|
||||
defender: playerCombatStats,
|
||||
);
|
||||
playerCombatStats = result.updatedDefender;
|
||||
opponentAccum = 0;
|
||||
}
|
||||
|
||||
// 무한 루프 방지
|
||||
if (turns > 1000) break;
|
||||
}
|
||||
|
||||
final isVictory = playerCombatStats.isAlive;
|
||||
|
||||
// 장비 교환
|
||||
final (updatedChallenger, updatedOpponent) = _exchangeEquipment(
|
||||
match: match,
|
||||
isVictory: isVictory,
|
||||
);
|
||||
|
||||
return ArenaMatchResult(
|
||||
match: match,
|
||||
isVictory: isVictory,
|
||||
turns: turns,
|
||||
updatedChallenger: updatedChallenger,
|
||||
updatedOpponent: updatedOpponent,
|
||||
);
|
||||
}
|
||||
|
||||
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
||||
///
|
||||
/// [match] 대전 정보
|
||||
/// Returns: 턴별 전투 상황 스트림
|
||||
Stream<ArenaCombatTurn> simulateCombat(ArenaMatch match) async* {
|
||||
final calculator = CombatCalculator(rng: _rng);
|
||||
|
||||
final challengerStats = match.challenger.finalStats;
|
||||
final opponentStats = match.opponent.finalStats;
|
||||
|
||||
if (challengerStats == null || opponentStats == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var playerCombatStats = challengerStats.copyWith(
|
||||
hpCurrent: challengerStats.hpMax,
|
||||
mpCurrent: challengerStats.mpMax,
|
||||
);
|
||||
|
||||
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||
opponentStats,
|
||||
match.opponent.characterName,
|
||||
);
|
||||
|
||||
int playerAccum = 0;
|
||||
int opponentAccum = 0;
|
||||
const tickMs = 200;
|
||||
int turns = 0;
|
||||
|
||||
// 초기 상태 전송
|
||||
yield ArenaCombatTurn(
|
||||
challengerHp: playerCombatStats.hpCurrent,
|
||||
opponentHp: opponentMonsterStats.hpCurrent,
|
||||
challengerHpMax: playerCombatStats.hpMax,
|
||||
opponentHpMax: opponentMonsterStats.hpMax,
|
||||
);
|
||||
|
||||
while (playerCombatStats.isAlive && opponentMonsterStats.isAlive) {
|
||||
playerAccum += tickMs;
|
||||
opponentAccum += tickMs;
|
||||
|
||||
int? challengerDamage;
|
||||
int? opponentDamage;
|
||||
bool isChallengerCritical = false;
|
||||
bool isOpponentCritical = false;
|
||||
bool isChallengerEvaded = false;
|
||||
bool isOpponentEvaded = false;
|
||||
bool isChallengerBlocked = false;
|
||||
bool isOpponentBlocked = false;
|
||||
|
||||
// 플레이어 공격
|
||||
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
||||
final result = calculator.playerAttackMonster(
|
||||
attacker: playerCombatStats,
|
||||
defender: opponentMonsterStats,
|
||||
);
|
||||
opponentMonsterStats = result.updatedDefender;
|
||||
playerAccum = 0;
|
||||
|
||||
if (result.result.isHit) {
|
||||
challengerDamage = result.result.damage;
|
||||
isChallengerCritical = result.result.isCritical;
|
||||
} else {
|
||||
isOpponentEvaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 상대 공격
|
||||
if (opponentMonsterStats.isAlive &&
|
||||
opponentAccum >= opponentMonsterStats.attackDelayMs) {
|
||||
final result = calculator.monsterAttackPlayer(
|
||||
attacker: opponentMonsterStats,
|
||||
defender: playerCombatStats,
|
||||
);
|
||||
playerCombatStats = result.updatedDefender;
|
||||
opponentAccum = 0;
|
||||
|
||||
if (result.result.isHit) {
|
||||
opponentDamage = result.result.damage;
|
||||
isOpponentCritical = result.result.isCritical;
|
||||
isChallengerBlocked = result.result.isBlocked;
|
||||
} else {
|
||||
isChallengerEvaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 공격이 발생했을 때만 턴 전송
|
||||
if (challengerDamage != null || opponentDamage != null) {
|
||||
turns++;
|
||||
yield ArenaCombatTurn(
|
||||
challengerDamage: challengerDamage,
|
||||
opponentDamage: opponentDamage,
|
||||
challengerHp: playerCombatStats.hpCurrent,
|
||||
opponentHp: opponentMonsterStats.hpCurrent,
|
||||
challengerHpMax: playerCombatStats.hpMax,
|
||||
opponentHpMax: opponentMonsterStats.hpMax,
|
||||
isChallengerCritical: isChallengerCritical,
|
||||
isOpponentCritical: isOpponentCritical,
|
||||
isChallengerEvaded: isChallengerEvaded,
|
||||
isOpponentEvaded: isOpponentEvaded,
|
||||
isChallengerBlocked: isChallengerBlocked,
|
||||
isOpponentBlocked: isOpponentBlocked,
|
||||
);
|
||||
|
||||
// 애니메이션을 위한 딜레이
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
// 무한 루프 방지
|
||||
if (turns > 1000) break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 장비 교환
|
||||
// ============================================================================
|
||||
|
||||
/// 장비 교환 (같은 슬롯끼리)
|
||||
///
|
||||
/// 승자가 선택한 슬롯의 장비를 서로 교환
|
||||
(HallOfFameEntry, HallOfFameEntry) _exchangeEquipment({
|
||||
required ArenaMatch match,
|
||||
required bool isVictory,
|
||||
}) {
|
||||
final slot = match.bettingSlot;
|
||||
|
||||
// 도전자 장비 목록 복사
|
||||
final challengerEquipment =
|
||||
List<EquipmentItem>.from(match.challenger.finalEquipment ?? []);
|
||||
|
||||
// 상대 장비 목록 복사
|
||||
final opponentEquipment =
|
||||
List<EquipmentItem>.from(match.opponent.finalEquipment ?? []);
|
||||
|
||||
// 해당 슬롯의 장비 찾기
|
||||
final challengerItem = _findItemBySlot(challengerEquipment, slot);
|
||||
final opponentItem = _findItemBySlot(opponentEquipment, slot);
|
||||
|
||||
// 장비 교환
|
||||
_replaceItemInList(challengerEquipment, slot, opponentItem);
|
||||
_replaceItemInList(opponentEquipment, slot, challengerItem);
|
||||
|
||||
// 업데이트된 엔트리 생성
|
||||
final updatedChallenger = match.challenger.copyWith(
|
||||
finalEquipment: challengerEquipment,
|
||||
);
|
||||
final updatedOpponent = match.opponent.copyWith(
|
||||
finalEquipment: opponentEquipment,
|
||||
);
|
||||
|
||||
return (updatedChallenger, updatedOpponent);
|
||||
}
|
||||
|
||||
/// 슬롯으로 장비 찾기
|
||||
EquipmentItem _findItemBySlot(
|
||||
List<EquipmentItem> equipment, EquipmentSlot slot) {
|
||||
for (final item in equipment) {
|
||||
if (item.slot == slot) return item;
|
||||
}
|
||||
return EquipmentItem.empty(slot);
|
||||
}
|
||||
|
||||
/// 장비 목록에서 특정 슬롯의 아이템 교체
|
||||
void _replaceItemInList(
|
||||
List<EquipmentItem> equipment,
|
||||
EquipmentSlot slot,
|
||||
EquipmentItem newItem,
|
||||
) {
|
||||
for (var i = 0; i < equipment.length; i++) {
|
||||
if (equipment[i].slot == slot) {
|
||||
equipment[i] = newItem;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 슬롯이 없으면 추가
|
||||
equipment.add(newItem);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user