feat(arena): 아레나 서비스 및 모델 개선
- ArenaService 로직 확장 - ArenaMatch 모델 필드 추가
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import 'package:asciineverdie/data/skill_data.dart';
|
||||
import 'package:asciineverdie/src/core/engine/combat_calculator.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/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';
|
||||
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';
|
||||
|
||||
/// 아레나 서비스
|
||||
@@ -18,6 +23,90 @@ class ArenaService {
|
||||
|
||||
final DeterministicRandom _rng;
|
||||
|
||||
late final SkillService _skillService = SkillService(rng: _rng);
|
||||
|
||||
// ============================================================================
|
||||
// 스킬 시스템 헬퍼
|
||||
// ============================================================================
|
||||
|
||||
/// HallOfFameEntry의 finalSpells에서 Skill 목록 추출
|
||||
List<Skill> _getSkillsFromEntry(HallOfFameEntry entry) {
|
||||
final spells = entry.finalSpells;
|
||||
if (spells == null || spells.isEmpty) return [];
|
||||
|
||||
final skills = <Skill>[];
|
||||
for (final spell in spells) {
|
||||
final spellName = spell['name'];
|
||||
if (spellName != null) {
|
||||
final skill = SkillData.getSkillBySpellName(spellName);
|
||||
if (skill != null) {
|
||||
skills.add(skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
return skills;
|
||||
}
|
||||
|
||||
/// AI 스킬 선택 (우선순위: 회복 > 버프 > 공격)
|
||||
Skill? _selectBestSkill({
|
||||
required List<Skill> skills,
|
||||
required CombatStats stats,
|
||||
required SkillSystemState skillSystem,
|
||||
required MonsterCombatStats? target,
|
||||
}) {
|
||||
if (skills.isEmpty) return null;
|
||||
|
||||
final currentMp = stats.mpCurrent;
|
||||
final hpRatio = stats.hpCurrent / stats.hpMax;
|
||||
|
||||
// HP가 낮으면 회복 스킬 우선
|
||||
if (hpRatio < 0.4) {
|
||||
for (final skill in skills) {
|
||||
if (skill.type == SkillType.heal &&
|
||||
_skillService.canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null) {
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 버프가 없으면 버프 스킬
|
||||
if (skillSystem.activeBuffs.isEmpty) {
|
||||
for (final skill in skills) {
|
||||
if (skill.type == SkillType.buff &&
|
||||
_skillService.canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null) {
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MP가 충분하면 공격 스킬
|
||||
if (currentMp >= 30) {
|
||||
for (final skill in skills) {
|
||||
if (skill.type == SkillType.attack &&
|
||||
_skillService.canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null) {
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // 스킬 사용 안 함 (기본 공격)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 상대 결정
|
||||
// ============================================================================
|
||||
@@ -148,11 +237,25 @@ class ArenaService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 스킬 목록 로드
|
||||
final challengerSkills = _getSkillsFromEntry(match.challenger);
|
||||
final opponentSkills = _getSkillsFromEntry(match.opponent);
|
||||
|
||||
// 스킬 시스템 상태 초기화
|
||||
var challengerSkillSystem = SkillSystemState.empty();
|
||||
var opponentSkillSystem = SkillSystemState.empty();
|
||||
|
||||
var playerCombatStats = challengerStats.copyWith(
|
||||
hpCurrent: challengerStats.hpMax,
|
||||
mpCurrent: challengerStats.mpMax,
|
||||
);
|
||||
|
||||
// 상대도 CombatStats로 관리 (스킬 사용 위해)
|
||||
var opponentCombatStats = opponentStats.copyWith(
|
||||
hpCurrent: opponentStats.hpMax,
|
||||
mpCurrent: opponentStats.mpMax,
|
||||
);
|
||||
|
||||
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||
opponentStats,
|
||||
match.opponent.characterName,
|
||||
@@ -160,20 +263,30 @@ class ArenaService {
|
||||
|
||||
int playerAccum = 0;
|
||||
int opponentAccum = 0;
|
||||
int elapsedMs = 0;
|
||||
const tickMs = 200;
|
||||
int turns = 0;
|
||||
|
||||
// 초기 상태 전송
|
||||
yield ArenaCombatTurn(
|
||||
challengerHp: playerCombatStats.hpCurrent,
|
||||
opponentHp: opponentMonsterStats.hpCurrent,
|
||||
opponentHp: opponentCombatStats.hpCurrent,
|
||||
challengerHpMax: playerCombatStats.hpMax,
|
||||
opponentHpMax: opponentMonsterStats.hpMax,
|
||||
opponentHpMax: opponentCombatStats.hpMax,
|
||||
challengerMp: playerCombatStats.mpCurrent,
|
||||
opponentMp: opponentCombatStats.mpCurrent,
|
||||
challengerMpMax: playerCombatStats.mpMax,
|
||||
opponentMpMax: opponentCombatStats.mpMax,
|
||||
);
|
||||
|
||||
while (playerCombatStats.isAlive && opponentMonsterStats.isAlive) {
|
||||
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;
|
||||
@@ -183,59 +296,144 @@ class ArenaService {
|
||||
bool isOpponentEvaded = false;
|
||||
bool isChallengerBlocked = false;
|
||||
bool isOpponentBlocked = false;
|
||||
String? challengerSkillUsed;
|
||||
String? opponentSkillUsed;
|
||||
int? challengerHealAmount;
|
||||
int? opponentHealAmount;
|
||||
|
||||
// 플레이어 공격
|
||||
// 도전자 턴
|
||||
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;
|
||||
// 스킬 선택
|
||||
final skill = _selectBestSkill(
|
||||
skills: challengerSkills,
|
||||
stats: playerCombatStats,
|
||||
skillSystem: challengerSkillSystem,
|
||||
target: opponentMonsterStats,
|
||||
);
|
||||
|
||||
if (skill != null) {
|
||||
// 스킬 사용
|
||||
final skillResult = _useSkill(
|
||||
skill: skill,
|
||||
attacker: playerCombatStats,
|
||||
defender: opponentCombatStats,
|
||||
skillSystem: challengerSkillSystem,
|
||||
);
|
||||
playerCombatStats = skillResult.updatedAttacker;
|
||||
opponentCombatStats = skillResult.updatedDefender;
|
||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
||||
challengerSkillUsed = skill.name;
|
||||
challengerDamage = skillResult.damage;
|
||||
challengerHealAmount = skillResult.healAmount;
|
||||
isChallengerCritical = skillResult.isCritical;
|
||||
} else {
|
||||
isOpponentEvaded = true;
|
||||
// 기본 공격
|
||||
// 상대 몬스터 스탯 동기화
|
||||
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||
opponentCombatStats,
|
||||
match.opponent.characterName,
|
||||
);
|
||||
final result = calculator.playerAttackMonster(
|
||||
attacker: playerCombatStats,
|
||||
defender: opponentMonsterStats,
|
||||
);
|
||||
opponentMonsterStats = result.updatedDefender;
|
||||
opponentCombatStats = opponentCombatStats.copyWith(
|
||||
hpCurrent: opponentMonsterStats.hpCurrent,
|
||||
);
|
||||
|
||||
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;
|
||||
// 상대 턴
|
||||
if (opponentCombatStats.hpCurrent > 0 &&
|
||||
opponentAccum >= opponentCombatStats.attackDelayMs) {
|
||||
opponentAccum = 0;
|
||||
|
||||
if (result.result.isHit) {
|
||||
opponentDamage = result.result.damage;
|
||||
isOpponentCritical = result.result.isCritical;
|
||||
isChallengerBlocked = result.result.isBlocked;
|
||||
// 상대 스킬 선택
|
||||
final skill = _selectBestSkill(
|
||||
skills: opponentSkills,
|
||||
stats: opponentCombatStats,
|
||||
skillSystem: opponentSkillSystem,
|
||||
target: null,
|
||||
);
|
||||
|
||||
if (skill != null) {
|
||||
// 스킬 사용
|
||||
final skillResult = _useSkill(
|
||||
skill: skill,
|
||||
attacker: opponentCombatStats,
|
||||
defender: playerCombatStats,
|
||||
skillSystem: opponentSkillSystem,
|
||||
);
|
||||
opponentCombatStats = skillResult.updatedAttacker;
|
||||
playerCombatStats = skillResult.updatedDefender;
|
||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
||||
opponentSkillUsed = skill.name;
|
||||
opponentDamage = skillResult.damage;
|
||||
opponentHealAmount = skillResult.healAmount;
|
||||
isOpponentCritical = skillResult.isCritical;
|
||||
} else {
|
||||
isChallengerEvaded = true;
|
||||
// 기본 공격 (몬스터 형태로)
|
||||
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||
opponentCombatStats,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 공격이 발생했을 때만 턴 전송
|
||||
if (challengerDamage != null || opponentDamage != null) {
|
||||
// 액션이 발생했을 때만 턴 전송
|
||||
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: opponentMonsterStats.hpCurrent,
|
||||
opponentHp: opponentCombatStats.hpCurrent,
|
||||
challengerHpMax: playerCombatStats.hpMax,
|
||||
opponentHpMax: opponentMonsterStats.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,
|
||||
);
|
||||
|
||||
// 애니메이션을 위한 딜레이
|
||||
@@ -247,6 +445,83 @@ class ArenaService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 스킬 사용 (내부 헬퍼)
|
||||
({
|
||||
CombatStats updatedAttacker,
|
||||
CombatStats updatedDefender,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
int? damage,
|
||||
int? healAmount,
|
||||
bool isCritical,
|
||||
}) _useSkill({
|
||||
required Skill skill,
|
||||
required CombatStats attacker,
|
||||
required CombatStats defender,
|
||||
required SkillSystemState skillSystem,
|
||||
}) {
|
||||
int? damage;
|
||||
int? healAmount;
|
||||
var updatedAttacker = attacker;
|
||||
var updatedDefender = defender;
|
||||
var updatedSkillSystem = skillSystem;
|
||||
|
||||
switch (skill.type) {
|
||||
case SkillType.attack:
|
||||
final monsterStats = MonsterCombatStats.fromCombatStats(defender, '');
|
||||
final result = _skillService.useAttackSkill(
|
||||
skill: skill,
|
||||
player: attacker,
|
||||
monster: monsterStats,
|
||||
skillSystem: skillSystem,
|
||||
);
|
||||
updatedAttacker = result.updatedPlayer;
|
||||
updatedDefender = defender.copyWith(
|
||||
hpCurrent: result.updatedMonster.hpCurrent,
|
||||
);
|
||||
updatedSkillSystem = result.updatedSkillSystem;
|
||||
damage = result.result.damage;
|
||||
|
||||
case SkillType.heal:
|
||||
final result = _skillService.useHealSkill(
|
||||
skill: skill,
|
||||
player: attacker,
|
||||
skillSystem: skillSystem,
|
||||
);
|
||||
updatedAttacker = result.updatedPlayer;
|
||||
updatedSkillSystem = result.updatedSkillSystem;
|
||||
healAmount = result.result.healedAmount;
|
||||
|
||||
case SkillType.buff:
|
||||
final result = _skillService.useBuffSkill(
|
||||
skill: skill,
|
||||
player: attacker,
|
||||
skillSystem: skillSystem,
|
||||
);
|
||||
updatedAttacker = result.updatedPlayer;
|
||||
updatedSkillSystem = result.updatedSkillSystem;
|
||||
|
||||
case SkillType.debuff:
|
||||
// 디버프 스킬 사용
|
||||
final debuffResult = _skillService.useDebuffSkill(
|
||||
skill: skill,
|
||||
player: attacker,
|
||||
skillSystem: skillSystem,
|
||||
currentDebuffs: [],
|
||||
);
|
||||
updatedAttacker = debuffResult.updatedPlayer;
|
||||
updatedSkillSystem = debuffResult.updatedSkillSystem;
|
||||
}
|
||||
|
||||
return (
|
||||
updatedAttacker: updatedAttacker,
|
||||
updatedDefender: updatedDefender,
|
||||
updatedSkillSystem: updatedSkillSystem,
|
||||
damage: damage,
|
||||
healAmount: healAmount,
|
||||
isCritical: false, // 스킬 크리티컬은 별도 처리 필요
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 장비 교환
|
||||
// ============================================================================
|
||||
|
||||
@@ -62,12 +62,20 @@ class ArenaCombatTurn {
|
||||
required this.opponentHp,
|
||||
required this.challengerHpMax,
|
||||
required this.opponentHpMax,
|
||||
this.challengerMp,
|
||||
this.opponentMp,
|
||||
this.challengerMpMax,
|
||||
this.opponentMpMax,
|
||||
this.isChallengerCritical = false,
|
||||
this.isOpponentCritical = false,
|
||||
this.isChallengerEvaded = false,
|
||||
this.isOpponentEvaded = false,
|
||||
this.isChallengerBlocked = false,
|
||||
this.isOpponentBlocked = false,
|
||||
this.challengerSkillUsed,
|
||||
this.opponentSkillUsed,
|
||||
this.challengerHealAmount,
|
||||
this.opponentHealAmount,
|
||||
}) : timestamp = DateTime.now().microsecondsSinceEpoch;
|
||||
|
||||
/// 턴 식별용 타임스탬프
|
||||
@@ -91,6 +99,18 @@ class ArenaCombatTurn {
|
||||
/// 상대 최대 HP
|
||||
final int opponentHpMax;
|
||||
|
||||
/// 도전자 현재 MP
|
||||
final int? challengerMp;
|
||||
|
||||
/// 상대 현재 MP
|
||||
final int? opponentMp;
|
||||
|
||||
/// 도전자 최대 MP
|
||||
final int? challengerMpMax;
|
||||
|
||||
/// 상대 최대 MP
|
||||
final int? opponentMpMax;
|
||||
|
||||
/// 도전자 크리티컬 여부
|
||||
final bool isChallengerCritical;
|
||||
|
||||
@@ -108,4 +128,16 @@ class ArenaCombatTurn {
|
||||
|
||||
/// 상대 블록 여부
|
||||
final bool isOpponentBlocked;
|
||||
|
||||
/// 도전자 사용 스킬명 (null이면 기본 공격)
|
||||
final String? challengerSkillUsed;
|
||||
|
||||
/// 상대 사용 스킬명 (null이면 기본 공격)
|
||||
final String? opponentSkillUsed;
|
||||
|
||||
/// 도전자 회복량
|
||||
final int? challengerHealAmount;
|
||||
|
||||
/// 상대 회복량
|
||||
final int? opponentHealAmount;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user