799 lines
28 KiB
Dart
799 lines
28 KiB
Dart
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';
|
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
|
import 'package:asciineverdie/src/core/model/game_state.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/model/skill.dart';
|
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
|
|
|
/// 아레나 서비스
|
|
///
|
|
/// 로컬 아레나 대전 시스템의 핵심 로직 담당:
|
|
/// - 순위 계산 및 상대 결정
|
|
/// - 전투 실행
|
|
/// - 장비 교환
|
|
class ArenaService {
|
|
ArenaService({DeterministicRandom? rng})
|
|
: _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch);
|
|
|
|
final DeterministicRandom _rng;
|
|
|
|
late final SkillService _skillService = SkillService(rng: _rng);
|
|
|
|
// ============================================================================
|
|
// 스킬 시스템 헬퍼
|
|
// ============================================================================
|
|
|
|
/// HallOfFameEntry의 finalSkills에서 Skill 목록 추출
|
|
List<Skill> _getSkillsFromEntry(HallOfFameEntry entry) {
|
|
final skillData = entry.finalSkills;
|
|
if (skillData == null || skillData.isEmpty) return [];
|
|
|
|
final skills = <Skill>[];
|
|
for (final data in skillData) {
|
|
final skillName = data['name'];
|
|
if (skillName != null) {
|
|
final skill = SkillData.getSkillBySpellName(skillName);
|
|
if (skill != null) {
|
|
skills.add(skill);
|
|
}
|
|
}
|
|
}
|
|
return skills;
|
|
}
|
|
|
|
/// 스킬 ID 목록 추출 (HallOfFameEntry에서)
|
|
List<String> _getSkillIdsFromEntry(HallOfFameEntry entry) {
|
|
return _getSkillsFromEntry(entry).map((s) => s.id).toList();
|
|
}
|
|
|
|
/// 스킬 랭크 조회 (HallOfFameEntry의 finalSkills에서)
|
|
int _getSkillRankFromEntry(HallOfFameEntry entry, String skillId) {
|
|
final skill = SkillData.getSkillById(skillId);
|
|
if (skill == null) return 1;
|
|
|
|
final skillData = entry.finalSkills;
|
|
if (skillData == null || skillData.isEmpty) return 1;
|
|
|
|
for (final data in skillData) {
|
|
if (data['name'] == skill.name) {
|
|
final rankStr = data['rank'] ?? 'I';
|
|
return _romanToInt(rankStr);
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/// 로마 숫자 → 정수 변환
|
|
int _romanToInt(String roman) {
|
|
return switch (roman) {
|
|
'I' => 1,
|
|
'II' => 2,
|
|
'III' => 3,
|
|
'IV' => 4,
|
|
'V' => 5,
|
|
_ => 1,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// 상대 결정
|
|
// ============================================================================
|
|
|
|
/// 상대 결정 (바로 위 순위, 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] 대전 정보
|
|
/// [challengerHp] 도전자 최종 HP
|
|
/// [opponentHp] 상대 최종 HP
|
|
/// [turns] 총 턴 수
|
|
/// Returns: 대전 결과 (승패, 장비 교환 후 캐릭터)
|
|
ArenaMatchResult createResultFromSimulation({
|
|
required ArenaMatch match,
|
|
required int challengerHp,
|
|
required int opponentHp,
|
|
required int turns,
|
|
}) {
|
|
// 도전자 HP가 0보다 크면 승리
|
|
final isVictory = challengerHp > 0 && opponentHp <= 0;
|
|
|
|
// 장비 교환
|
|
final (updatedChallenger, updatedOpponent) = _exchangeEquipment(
|
|
match: match,
|
|
isVictory: isVictory,
|
|
);
|
|
|
|
return ArenaMatchResult(
|
|
match: match,
|
|
isVictory: isVictory,
|
|
turns: turns,
|
|
updatedChallenger: updatedChallenger,
|
|
updatedOpponent: updatedOpponent,
|
|
);
|
|
}
|
|
|
|
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
|
///
|
|
/// progress_service._processCombatTickWithSkills()와 동일한 로직 사용
|
|
/// [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;
|
|
}
|
|
|
|
// 스킬 ID 목록 로드 (SkillBook과 동일한 방식)
|
|
var challengerSkillIds = _getSkillIdsFromEntry(match.challenger);
|
|
var opponentSkillIds = _getSkillIdsFromEntry(match.opponent);
|
|
|
|
// 스킬이 없으면 기본 스킬 사용
|
|
if (challengerSkillIds.isEmpty) {
|
|
challengerSkillIds = SkillData.defaultSkillIds;
|
|
}
|
|
if (opponentSkillIds.isEmpty) {
|
|
opponentSkillIds = SkillData.defaultSkillIds;
|
|
}
|
|
|
|
// 스킬 시스템 상태 초기화
|
|
var challengerSkillSystem = SkillSystemState.empty();
|
|
var opponentSkillSystem = SkillSystemState.empty();
|
|
|
|
// DOT 및 디버프 추적 (일반 전투와 동일)
|
|
var challengerDoTs = <DotEffect>[];
|
|
var opponentDoTs = <DotEffect>[];
|
|
var challengerDebuffs = <ActiveBuff>[];
|
|
var opponentDebuffs = <ActiveBuff>[];
|
|
|
|
var playerCombatStats = challengerStats.copyWith(
|
|
hpCurrent: challengerStats.hpMax,
|
|
mpCurrent: challengerStats.mpMax,
|
|
);
|
|
|
|
var opponentCombatStats = opponentStats.copyWith(
|
|
hpCurrent: opponentStats.hpMax,
|
|
mpCurrent: opponentStats.mpMax,
|
|
);
|
|
|
|
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
opponentCombatStats,
|
|
match.opponent.characterName,
|
|
);
|
|
|
|
var challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
playerCombatStats,
|
|
match.challenger.characterName,
|
|
);
|
|
|
|
int playerAccum = 0;
|
|
int opponentAccum = 0;
|
|
int elapsedMs = 0;
|
|
const tickMs = 200;
|
|
int turns = 0;
|
|
|
|
// 초기 상태 전송
|
|
yield ArenaCombatTurn(
|
|
challengerHp: playerCombatStats.hpCurrent,
|
|
opponentHp: opponentCombatStats.hpCurrent,
|
|
challengerHpMax: playerCombatStats.hpMax,
|
|
opponentHpMax: opponentCombatStats.hpMax,
|
|
challengerMp: playerCombatStats.mpCurrent,
|
|
opponentMp: opponentCombatStats.mpCurrent,
|
|
challengerMpMax: playerCombatStats.mpMax,
|
|
opponentMpMax: opponentCombatStats.mpMax,
|
|
);
|
|
|
|
while (playerCombatStats.isAlive && opponentCombatStats.hpCurrent > 0) {
|
|
playerAccum += tickMs;
|
|
opponentAccum += tickMs;
|
|
elapsedMs += tickMs;
|
|
|
|
// 스킬 시스템 시간 업데이트
|
|
challengerSkillSystem = challengerSkillSystem.copyWith(elapsedMs: elapsedMs);
|
|
opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs);
|
|
|
|
int? challengerDamage;
|
|
int? opponentDamage;
|
|
bool isChallengerCritical = false;
|
|
bool isOpponentCritical = false;
|
|
bool isChallengerEvaded = false;
|
|
bool isOpponentEvaded = false;
|
|
bool isChallengerBlocked = false;
|
|
bool isOpponentBlocked = false;
|
|
String? challengerSkillUsed;
|
|
String? opponentSkillUsed;
|
|
int? challengerHealAmount;
|
|
int? opponentHealAmount;
|
|
|
|
// =========================================================================
|
|
// DOT 틱 처리 (도전자 → 상대에게 적용된 DOT)
|
|
// =========================================================================
|
|
var dotDamageToOpponent = 0;
|
|
final updatedChallengerDoTs = <DotEffect>[];
|
|
for (final dot in challengerDoTs) {
|
|
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
|
if (ticksTriggered > 0) {
|
|
dotDamageToOpponent += dot.damagePerTick * ticksTriggered;
|
|
}
|
|
if (updatedDot.isActive) {
|
|
updatedChallengerDoTs.add(updatedDot);
|
|
}
|
|
}
|
|
challengerDoTs = updatedChallengerDoTs;
|
|
|
|
if (dotDamageToOpponent > 0 && opponentCombatStats.hpCurrent > 0) {
|
|
opponentCombatStats = opponentCombatStats.copyWith(
|
|
hpCurrent: (opponentCombatStats.hpCurrent - dotDamageToOpponent)
|
|
.clamp(0, opponentCombatStats.hpMax),
|
|
);
|
|
}
|
|
|
|
// DOT 틱 처리 (상대 → 도전자에게 적용된 DOT)
|
|
var dotDamageToChallenger = 0;
|
|
final updatedOpponentDoTs = <DotEffect>[];
|
|
for (final dot in opponentDoTs) {
|
|
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
|
if (ticksTriggered > 0) {
|
|
dotDamageToChallenger += dot.damagePerTick * ticksTriggered;
|
|
}
|
|
if (updatedDot.isActive) {
|
|
updatedOpponentDoTs.add(updatedDot);
|
|
}
|
|
}
|
|
opponentDoTs = updatedOpponentDoTs;
|
|
|
|
if (dotDamageToChallenger > 0 && playerCombatStats.isAlive) {
|
|
playerCombatStats = playerCombatStats.copyWith(
|
|
hpCurrent: (playerCombatStats.hpCurrent - dotDamageToChallenger)
|
|
.clamp(0, playerCombatStats.hpMax),
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 만료된 디버프 정리
|
|
// =========================================================================
|
|
challengerDebuffs = challengerDebuffs
|
|
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
|
.toList();
|
|
opponentDebuffs = opponentDebuffs
|
|
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
|
.toList();
|
|
|
|
// =========================================================================
|
|
// 도전자 턴 (selectAutoSkill 사용 - 일반 전투와 동일)
|
|
// =========================================================================
|
|
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
|
playerAccum = 0;
|
|
|
|
// 상대 몬스터 스탯 동기화
|
|
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
opponentCombatStats,
|
|
match.opponent.characterName,
|
|
);
|
|
|
|
// 스킬 자동 선택 (progress_service와 동일한 로직)
|
|
final selectedSkill = _skillService.selectAutoSkill(
|
|
player: playerCombatStats,
|
|
monster: opponentMonsterStats,
|
|
skillSystem: challengerSkillSystem,
|
|
availableSkillIds: challengerSkillIds,
|
|
activeDoTs: challengerDoTs,
|
|
activeDebuffs: opponentDebuffs,
|
|
);
|
|
|
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
|
// 스킬 랭크 조회 및 적용
|
|
final skillRank = _getSkillRankFromEntry(
|
|
match.challenger,
|
|
selectedSkill.id,
|
|
);
|
|
final skillResult = _skillService.useAttackSkillWithRank(
|
|
skill: selectedSkill,
|
|
player: playerCombatStats,
|
|
monster: opponentMonsterStats,
|
|
skillSystem: challengerSkillSystem,
|
|
rank: skillRank,
|
|
);
|
|
playerCombatStats = skillResult.updatedPlayer;
|
|
opponentCombatStats = opponentCombatStats.copyWith(
|
|
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
|
);
|
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
challengerSkillUsed = selectedSkill.name;
|
|
challengerDamage = skillResult.result.damage;
|
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
|
// DOT 스킬 사용
|
|
final skillResult = _skillService.useDotSkill(
|
|
skill: selectedSkill,
|
|
player: playerCombatStats,
|
|
skillSystem: challengerSkillSystem,
|
|
playerInt: playerCombatStats.atk ~/ 10,
|
|
playerWis: playerCombatStats.def ~/ 10,
|
|
);
|
|
playerCombatStats = skillResult.updatedPlayer;
|
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
if (skillResult.dotEffect != null) {
|
|
challengerDoTs.add(skillResult.dotEffect!);
|
|
}
|
|
challengerSkillUsed = selectedSkill.name;
|
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
|
// 회복 스킬 사용
|
|
final skillResult = _skillService.useHealSkill(
|
|
skill: selectedSkill,
|
|
player: playerCombatStats,
|
|
skillSystem: challengerSkillSystem,
|
|
);
|
|
playerCombatStats = skillResult.updatedPlayer;
|
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
challengerSkillUsed = selectedSkill.name;
|
|
challengerHealAmount = skillResult.result.healedAmount;
|
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
|
// 버프 스킬 사용
|
|
final skillResult = _skillService.useBuffSkill(
|
|
skill: selectedSkill,
|
|
player: playerCombatStats,
|
|
skillSystem: challengerSkillSystem,
|
|
);
|
|
playerCombatStats = skillResult.updatedPlayer;
|
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
challengerSkillUsed = selectedSkill.name;
|
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
|
// 디버프 스킬 사용
|
|
final skillResult = _skillService.useDebuffSkill(
|
|
skill: selectedSkill,
|
|
player: playerCombatStats,
|
|
skillSystem: challengerSkillSystem,
|
|
currentDebuffs: opponentDebuffs,
|
|
);
|
|
playerCombatStats = skillResult.updatedPlayer;
|
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
final debuffEffect = skillResult.debuffEffect;
|
|
if (debuffEffect != null) {
|
|
opponentDebuffs = opponentDebuffs
|
|
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id)
|
|
.toList()
|
|
..add(debuffEffect);
|
|
}
|
|
challengerSkillUsed = selectedSkill.name;
|
|
} else {
|
|
// 일반 공격
|
|
final result = calculator.playerAttackMonster(
|
|
attacker: playerCombatStats,
|
|
defender: opponentMonsterStats,
|
|
);
|
|
opponentCombatStats = opponentCombatStats.copyWith(
|
|
hpCurrent: result.updatedDefender.hpCurrent,
|
|
);
|
|
|
|
if (result.result.isHit) {
|
|
challengerDamage = result.result.damage;
|
|
isChallengerCritical = result.result.isCritical;
|
|
} else {
|
|
isOpponentEvaded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 상대 턴 (selectAutoSkill 사용 - 일반 전투와 동일)
|
|
// =========================================================================
|
|
if (opponentCombatStats.hpCurrent > 0 &&
|
|
opponentAccum >= opponentCombatStats.attackDelayMs) {
|
|
opponentAccum = 0;
|
|
|
|
// 도전자 몬스터 스탯 동기화
|
|
challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
playerCombatStats,
|
|
match.challenger.characterName,
|
|
);
|
|
|
|
// 스킬 자동 선택 (progress_service와 동일한 로직)
|
|
final selectedSkill = _skillService.selectAutoSkill(
|
|
player: opponentCombatStats,
|
|
monster: challengerMonsterStats,
|
|
skillSystem: opponentSkillSystem,
|
|
availableSkillIds: opponentSkillIds,
|
|
activeDoTs: opponentDoTs,
|
|
activeDebuffs: challengerDebuffs,
|
|
);
|
|
|
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
|
// 스킬 랭크 조회 및 적용
|
|
final skillRank = _getSkillRankFromEntry(
|
|
match.opponent,
|
|
selectedSkill.id,
|
|
);
|
|
final skillResult = _skillService.useAttackSkillWithRank(
|
|
skill: selectedSkill,
|
|
player: opponentCombatStats,
|
|
monster: challengerMonsterStats,
|
|
skillSystem: opponentSkillSystem,
|
|
rank: skillRank,
|
|
);
|
|
opponentCombatStats = skillResult.updatedPlayer;
|
|
playerCombatStats = playerCombatStats.copyWith(
|
|
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
|
);
|
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
opponentSkillUsed = selectedSkill.name;
|
|
opponentDamage = skillResult.result.damage;
|
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
|
// DOT 스킬 사용
|
|
final skillResult = _skillService.useDotSkill(
|
|
skill: selectedSkill,
|
|
player: opponentCombatStats,
|
|
skillSystem: opponentSkillSystem,
|
|
playerInt: opponentCombatStats.atk ~/ 10,
|
|
playerWis: opponentCombatStats.def ~/ 10,
|
|
);
|
|
opponentCombatStats = skillResult.updatedPlayer;
|
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
if (skillResult.dotEffect != null) {
|
|
opponentDoTs.add(skillResult.dotEffect!);
|
|
}
|
|
opponentSkillUsed = selectedSkill.name;
|
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
|
// 회복 스킬 사용
|
|
final skillResult = _skillService.useHealSkill(
|
|
skill: selectedSkill,
|
|
player: opponentCombatStats,
|
|
skillSystem: opponentSkillSystem,
|
|
);
|
|
opponentCombatStats = skillResult.updatedPlayer;
|
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
opponentSkillUsed = selectedSkill.name;
|
|
opponentHealAmount = skillResult.result.healedAmount;
|
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
|
// 버프 스킬 사용
|
|
final skillResult = _skillService.useBuffSkill(
|
|
skill: selectedSkill,
|
|
player: opponentCombatStats,
|
|
skillSystem: opponentSkillSystem,
|
|
);
|
|
opponentCombatStats = skillResult.updatedPlayer;
|
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
opponentSkillUsed = selectedSkill.name;
|
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
|
// 디버프 스킬 사용
|
|
final skillResult = _skillService.useDebuffSkill(
|
|
skill: selectedSkill,
|
|
player: opponentCombatStats,
|
|
skillSystem: opponentSkillSystem,
|
|
currentDebuffs: challengerDebuffs,
|
|
);
|
|
opponentCombatStats = skillResult.updatedPlayer;
|
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
final debuffEffect = skillResult.debuffEffect;
|
|
if (debuffEffect != null) {
|
|
challengerDebuffs = challengerDebuffs
|
|
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id)
|
|
.toList()
|
|
..add(debuffEffect);
|
|
}
|
|
opponentSkillUsed = selectedSkill.name;
|
|
} else {
|
|
// 일반 공격 (디버프 효과 적용)
|
|
var debuffedOpponent = opponentCombatStats;
|
|
if (challengerDebuffs.isNotEmpty) {
|
|
double atkMod = 0;
|
|
for (final debuff in challengerDebuffs) {
|
|
if (!debuff.isExpired(elapsedMs)) {
|
|
atkMod += debuff.effect.atkModifier;
|
|
}
|
|
}
|
|
final newAtk = (opponentCombatStats.atk * (1 + atkMod))
|
|
.round()
|
|
.clamp(opponentCombatStats.atk ~/ 10, opponentCombatStats.atk);
|
|
debuffedOpponent = opponentCombatStats.copyWith(atk: newAtk);
|
|
}
|
|
|
|
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
debuffedOpponent,
|
|
match.opponent.characterName,
|
|
);
|
|
final result = calculator.monsterAttackPlayer(
|
|
attacker: opponentMonsterStats,
|
|
defender: playerCombatStats,
|
|
);
|
|
playerCombatStats = result.updatedDefender;
|
|
|
|
if (result.result.isHit) {
|
|
opponentDamage = result.result.damage;
|
|
isOpponentCritical = result.result.isCritical;
|
|
isChallengerBlocked = result.result.isBlocked;
|
|
} else {
|
|
isChallengerEvaded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 액션이 발생했을 때만 턴 전송
|
|
final hasAction = challengerDamage != null ||
|
|
opponentDamage != null ||
|
|
challengerHealAmount != null ||
|
|
opponentHealAmount != null ||
|
|
challengerSkillUsed != null ||
|
|
opponentSkillUsed != null;
|
|
|
|
if (hasAction) {
|
|
turns++;
|
|
yield ArenaCombatTurn(
|
|
challengerDamage: challengerDamage,
|
|
opponentDamage: opponentDamage,
|
|
challengerHp: playerCombatStats.hpCurrent,
|
|
opponentHp: opponentCombatStats.hpCurrent,
|
|
challengerHpMax: playerCombatStats.hpMax,
|
|
opponentHpMax: opponentCombatStats.hpMax,
|
|
challengerMp: playerCombatStats.mpCurrent,
|
|
opponentMp: opponentCombatStats.mpCurrent,
|
|
challengerMpMax: playerCombatStats.mpMax,
|
|
opponentMpMax: opponentCombatStats.mpMax,
|
|
isChallengerCritical: isChallengerCritical,
|
|
isOpponentCritical: isOpponentCritical,
|
|
isChallengerEvaded: isChallengerEvaded,
|
|
isOpponentEvaded: isOpponentEvaded,
|
|
isChallengerBlocked: isChallengerBlocked,
|
|
isOpponentBlocked: isOpponentBlocked,
|
|
challengerSkillUsed: challengerSkillUsed,
|
|
opponentSkillUsed: opponentSkillUsed,
|
|
challengerHealAmount: challengerHealAmount,
|
|
opponentHealAmount: opponentHealAmount,
|
|
);
|
|
|
|
// 애니메이션을 위한 딜레이
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
// 무한 루프 방지
|
|
if (turns > 1000) break;
|
|
}
|
|
}
|
|
// ============================================================================
|
|
// 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 challengerEquipment =
|
|
List<EquipmentItem>.from(match.challenger.finalEquipment ?? []);
|
|
|
|
// 상대 장비 목록 복사
|
|
final opponentEquipment =
|
|
List<EquipmentItem>.from(match.opponent.finalEquipment ?? []);
|
|
|
|
if (isVictory) {
|
|
// 도전자 승리: 도전자가 선택한 슬롯의 상대 장비 획득
|
|
final winnerSlot = match.challengerBettingSlot;
|
|
final lootedItem = _findItemBySlot(opponentEquipment, winnerSlot);
|
|
|
|
// 도전자: 약탈한 장비로 교체
|
|
_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(
|
|
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);
|
|
}
|
|
|
|
/// 기본 장비 생성 (Common 등급)
|
|
///
|
|
/// 패자가 장비를 잃었을 때 빈 슬롯 방지용
|
|
EquipmentItem _createDefaultEquipment(EquipmentSlot slot) {
|
|
return ItemService.createDefaultEquipmentForSlot(slot);
|
|
}
|
|
}
|