refactor(engine): 서비스 로직 정리

- ArenaService, PotionService, ProgressService 개선
- ResurrectionService, SkillService 정리
This commit is contained in:
JiWoong Sul
2026-01-12 16:17:00 +09:00
parent 32ecafd33d
commit 95528786eb
5 changed files with 176 additions and 55 deletions

View File

@@ -19,7 +19,7 @@ import 'package:asciineverdie/src/core/util/deterministic_random.dart';
/// - 장비 교환 /// - 장비 교환
class ArenaService { class ArenaService {
ArenaService({DeterministicRandom? rng}) ArenaService({DeterministicRandom? rng})
: _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch); : _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch);
final DeterministicRandom _rng; final DeterministicRandom _rng;
@@ -309,7 +309,9 @@ class ArenaService {
elapsedMs += tickMs; elapsedMs += tickMs;
// 스킬 시스템 시간 업데이트 // 스킬 시스템 시간 업데이트
challengerSkillSystem = challengerSkillSystem.copyWith(elapsedMs: elapsedMs); challengerSkillSystem = challengerSkillSystem.copyWith(
elapsedMs: elapsedMs,
);
opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs); opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs);
int? challengerDamage; int? challengerDamage;
@@ -469,10 +471,13 @@ class ArenaService {
challengerSkillSystem = skillResult.updatedSkillSystem; challengerSkillSystem = skillResult.updatedSkillSystem;
final debuffEffect = skillResult.debuffEffect; final debuffEffect = skillResult.debuffEffect;
if (debuffEffect != null) { if (debuffEffect != null) {
opponentDebuffs = opponentDebuffs opponentDebuffs =
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id) opponentDebuffs
.toList() .where(
..add(debuffEffect); (ActiveBuff d) => d.effect.id != debuffEffect.effect.id,
)
.toList()
..add(debuffEffect);
} }
challengerSkillUsed = selectedSkill.name; challengerSkillUsed = selectedSkill.name;
} else { } else {
@@ -585,10 +590,13 @@ class ArenaService {
opponentSkillSystem = skillResult.updatedSkillSystem; opponentSkillSystem = skillResult.updatedSkillSystem;
final debuffEffect = skillResult.debuffEffect; final debuffEffect = skillResult.debuffEffect;
if (debuffEffect != null) { if (debuffEffect != null) {
challengerDebuffs = challengerDebuffs challengerDebuffs =
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id) challengerDebuffs
.toList() .where(
..add(debuffEffect); (ActiveBuff d) => d.effect.id != debuffEffect.effect.id,
)
.toList()
..add(debuffEffect);
} }
opponentSkillUsed = selectedSkill.name; opponentSkillUsed = selectedSkill.name;
} else { } else {
@@ -628,7 +636,8 @@ class ArenaService {
} }
// 액션이 발생했을 때만 턴 전송 // 액션이 발생했을 때만 턴 전송
final hasAction = challengerDamage != null || final hasAction =
challengerDamage != null ||
opponentDamage != null || opponentDamage != null ||
challengerHealAmount != null || challengerHealAmount != null ||
opponentHealAmount != null || opponentHealAmount != null ||
@@ -722,12 +731,14 @@ class ArenaService {
required bool isVictory, required bool isVictory,
}) { }) {
// 도전자 장비 목록 복사 // 도전자 장비 목록 복사
final challengerEquipment = final challengerEquipment = List<EquipmentItem>.from(
List<EquipmentItem>.from(match.challenger.finalEquipment ?? []); match.challenger.finalEquipment ?? [],
);
// 상대 장비 목록 복사 // 상대 장비 목록 복사
final opponentEquipment = final opponentEquipment = List<EquipmentItem>.from(
List<EquipmentItem>.from(match.opponent.finalEquipment ?? []); match.opponent.finalEquipment ?? [],
);
if (isVictory) { if (isVictory) {
// 도전자 승리: 도전자가 선택한 슬롯의 상대 장비 획득 // 도전자 승리: 도전자가 선택한 슬롯의 상대 장비 획득
@@ -766,7 +777,9 @@ class ArenaService {
/// 슬롯으로 장비 찾기 /// 슬롯으로 장비 찾기
EquipmentItem _findItemBySlot( EquipmentItem _findItemBySlot(
List<EquipmentItem> equipment, EquipmentSlot slot) { List<EquipmentItem> equipment,
EquipmentSlot slot,
) {
for (final item in equipment) { for (final item in equipment) {
if (item.slot == slot) return item; if (item.slot == slot) return item;
} }

View File

@@ -375,8 +375,10 @@ class PotionService {
required int typeRoll, required int typeRoll,
}) { }) {
// 기본 드랍 확률 계산 // 기본 드랍 확률 계산
var dropChance = (baseDropChance + playerLevel * dropChancePerLevel) var dropChance = (baseDropChance + playerLevel * dropChancePerLevel).clamp(
.clamp(baseDropChance, maxDropChance); baseDropChance,
maxDropChance,
);
// 몬스터 등급 보너스 (Elite +5%, Boss +15%) // 몬스터 등급 보너스 (Elite +5%, Boss +15%)
dropChance += monsterGrade.potionDropBonus; dropChance += monsterGrade.potionDropBonus;

View File

@@ -104,8 +104,10 @@ class ProgressService {
); );
// ExpBar 초기화 (원본 743-746줄) // ExpBar 초기화 (원본 743-746줄)
final expBar = final expBar = ProgressBarState(
ProgressBarState(position: 0, max: ExpConstants.requiredExp(1)); position: 0,
max: ExpConstants.requiredExp(1),
);
// PlotBar 초기화 - Prologue 5분 (300초) // PlotBar 초기화 - Prologue 5분 (300초)
final plotBar = const ProgressBarState(position: 0, max: 300); final plotBar = const ProgressBarState(position: 0, max: 300);
@@ -299,23 +301,40 @@ class ProgressService {
progress = progress.copyWith(currentCombat: combatForReset); progress = progress.copyWith(currentCombat: combatForReset);
} }
// 전투 상태 초기화, 몬스터 처치 수 증가 및 물약 사용 기록 초기화 // Boss 승리 처리: 시네마틱 트리거
progress = progress.copyWith( if (progress.pendingActCompletion) {
currentCombat: null, // Act Boss를 처치했으므로 시네마틱 재생
monstersKilled: progress.monstersKilled + 1, final cinematicEntries = pq_logic.interplotCinematic(
); config,
nextState.rng,
nextState.traits.level,
progress.plotStageCount,
);
queue = QueueState(entries: [...queue.entries, ...cinematicEntries]);
progress = progress.copyWith(
currentCombat: null,
monstersKilled: progress.monstersKilled + 1,
pendingActCompletion: false, // Boss 처치 완료
);
} else {
// 일반 전투 종료
progress = progress.copyWith(
currentCombat: null,
monstersKilled: progress.monstersKilled + 1,
);
}
final resetPotionInventory = nextState.potionInventory.resetBattleUsage(); final resetPotionInventory = nextState.potionInventory.resetBattleUsage();
nextState = nextState.copyWith( nextState = nextState.copyWith(
progress: progress, progress: progress,
queue: queue,
potionInventory: resetPotionInventory, potionInventory: resetPotionInventory,
); );
// 최종 보스 처치 체크 // 최종 보스 처치 체크
if (progress.finalBossState == FinalBossState.fighting) { if (progress.finalBossState == FinalBossState.fighting) {
// 글리치 갓 처치 완료 - 게임 클리어 // 글리치 갓 처치 완료 - 게임 클리어
progress = progress.copyWith( progress = progress.copyWith(finalBossState: FinalBossState.defeated);
finalBossState: FinalBossState.defeated,
);
nextState = nextState.copyWith(progress: progress); nextState = nextState.copyWith(progress: progress);
// completeAct를 호출하여 게임 완료 처리 // completeAct를 호출하여 게임 완료 처리
@@ -406,22 +425,22 @@ class ProgressService {
} }
} }
// 플롯(plot) 바가 완료되면 InterplotCinematic 트리거 // 플롯(plot) 바가 완료되면 Act Boss 소환
// (원본 Main.pas:1301-1304) // (개선: Boss 처치 → 시네마틱 → Act 전환 순서)
if (gain && if (gain &&
progress.plot.max > 0 && progress.plot.max > 0 &&
progress.plot.position >= progress.plot.max) { progress.plot.position >= progress.plot.max &&
// InterplotCinematic을 호출하여 시네마틱 이벤트 큐에 추가 !progress.pendingActCompletion) {
final cinematicEntries = pq_logic.interplotCinematic( // Act Boss 소환 및 플래그 설정
config, final actBoss = _createActBoss(nextState);
nextState.rng, progress = progress.copyWith(
nextState.traits.level, plot: progress.plot.copyWith(position: 0), // Plot bar 리셋
nextState.progress.plotStageCount, currentCombat: actBoss,
pendingActCompletion: true, // Boss 처치 대기 플래그
); );
queue = QueueState(entries: [...queue.entries, ...cinematicEntries]);
// 플롯 바를 0으로 리셋하지 않음 - completeAct에서 처리됨
} else if (progress.currentTask.type != TaskType.load && } else if (progress.currentTask.type != TaskType.load &&
progress.plot.max > 0) { progress.plot.max > 0 &&
!progress.pendingActCompletion) {
final uncappedPlot = progress.plot.position + incrementSeconds; final uncappedPlot = progress.plot.position + incrementSeconds;
final int newPlotPos = uncappedPlot > progress.plot.max final int newPlotPos = uncappedPlot > progress.plot.max
? progress.plot.max ? progress.plot.max
@@ -531,13 +550,44 @@ class ProgressService {
return (progress: progress, queue: queue); return (progress: progress, queue: queue);
} }
// 3. 최종 보스 전투 체크 // 3. Act Boss 리트라이 체크
// pendingActCompletion이 true면 Act Boss 재소환
if (state.progress.pendingActCompletion) {
final actBoss = _createActBoss(state);
final combatCalculator = CombatCalculator(rng: state.rng);
final durationMillis = combatCalculator.estimateCombatDurationMs(
player: actBoss.playerStats,
monster: actBoss.monsterStats,
);
final taskResult = pq_logic.startTask(
progress,
l10n.taskDebugging(actBoss.monsterStats.name),
durationMillis,
);
progress = taskResult.progress.copyWith(
currentTask: TaskInfo(
caption: taskResult.caption,
type: TaskType.kill,
monsterBaseName: actBoss.monsterStats.name,
monsterPart: '*', // Boss는 WinItem 드랍
monsterLevel: actBoss.monsterStats.level,
monsterGrade: MonsterGrade.boss,
),
currentCombat: actBoss,
);
return (progress: progress, queue: queue);
}
// 4. 최종 보스 전투 체크
// finalBossState == fighting이면 Glitch God 스폰 // finalBossState == fighting이면 Glitch God 스폰
if (state.progress.finalBossState == FinalBossState.fighting) { if (state.progress.finalBossState == FinalBossState.fighting) {
return _startFinalBossFight(state, progress, queue); return _startFinalBossFight(state, progress, queue);
} }
// 4. MonsterTask 실행 (원본 678-684줄) // 5. MonsterTask 실행 (원본 678-684줄)
final level = state.traits.level; final level = state.traits.level;
// 원본 Main.pas:548-551: 25% 확률로 Quest Monster 사용 // 원본 Main.pas:548-551: 25% 확률로 Quest Monster 사용
@@ -878,7 +928,8 @@ class ProgressService {
var nextState = state; var nextState = state;
// 현재 레벨이 목표 레벨보다 낮으면 레벨업 (최대 100레벨) // 현재 레벨이 목표 레벨보다 낮으면 레벨업 (최대 100레벨)
while (nextState.traits.level < targetLevel && nextState.traits.level < 100) { while (nextState.traits.level < targetLevel &&
nextState.traits.level < 100) {
nextState = _levelUp(nextState); nextState = _levelUp(nextState);
} }
@@ -1390,10 +1441,13 @@ class ProgressService {
// 디버프 효과 추가 (기존 같은 디버프 제거 후) // 디버프 효과 추가 (기존 같은 디버프 제거 후)
if (skillResult.debuffEffect != null) { if (skillResult.debuffEffect != null) {
activeDebuffs = activeDebuffs activeDebuffs =
.where((d) => d.effect.id != skillResult.debuffEffect!.effect.id) activeDebuffs
.toList() .where(
..add(skillResult.debuffEffect!); (d) => d.effect.id != skillResult.debuffEffect!.effect.id,
)
.toList()
..add(skillResult.debuffEffect!);
} }
// 디버프 이벤트 생성 // 디버프 이벤트 생성
@@ -1533,6 +1587,55 @@ class ProgressService {
); );
} }
/// Act Boss 생성 (Act 완료 시)
///
/// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)로 설정하여
/// 플레이어가 이길 수 있는 수준 보장
CombatState _createActBoss(GameState state) {
final plotStage = state.progress.plotStageCount;
final actNumber = plotStage + 1;
// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)
// → 플레이어가 현재 레벨보다 높은 보스를 만나지 않도록 보장
final actMinLevel = ActMonsterLevel.forPlotStage(actNumber);
final bossLevel = math.min(state.traits.level, actMinLevel);
// Named monster 생성 (pq_logic.namedMonster 활용)
final bossName = pq_logic.namedMonster(config, state.rng, bossLevel);
final bossStats = MonsterBaseStats.forLevel(bossLevel);
// 플레이어 전투 스탯 생성
final playerCombatStats = CombatStats.fromStats(
stats: state.stats,
equipment: state.equipment,
level: state.traits.level,
monsterLevel: bossLevel,
);
// Boss 몬스터 스탯 생성 (일반 몬스터 대비 강화)
final monsterCombatStats = MonsterCombatStats(
name: bossName,
level: bossLevel,
atk: (bossStats.atk * 1.5).round(), // Boss 보정 (1.5배)
def: (bossStats.def * 1.5).round(),
hpMax: (bossStats.hp * 2.0).round(), // HP는 2.0배 (보스다운 전투 시간)
hpCurrent: (bossStats.hp * 2.0).round(),
criRate: 0.05,
criDamage: 1.5,
evasion: 0.0,
accuracy: 0.8,
attackDelayMs: 1000,
expReward: (bossStats.exp * 2.5).round(), // 경험치 보상 증가
);
// 전투 상태 초기화
return CombatState.start(
playerStats: playerCombatStats,
monsterStats: monsterCombatStats,
);
}
/// 플레이어 사망 처리 (Phase 4) /// 플레이어 사망 처리 (Phase 4)
/// ///
/// 모든 장비 상실 및 사망 정보 기록 /// 모든 장비 상실 및 사망 정보 기록
@@ -1578,9 +1681,11 @@ class ProgressService {
); );
// 전투 상태 초기화 및 사망 횟수 증가 // 전투 상태 초기화 및 사망 횟수 증가
// pendingActCompletion 플래그는 유지 (Boss 리트라이를 위해)
final progress = state.progress.copyWith( final progress = state.progress.copyWith(
currentCombat: null, currentCombat: null,
deathCount: state.progress.deathCount + 1, deathCount: state.progress.deathCount + 1,
// pendingActCompletion은 copyWith에서 명시하지 않으면 기존 값 유지
); );
return state.copyWith( return state.copyWith(

View File

@@ -167,10 +167,7 @@ class ResurrectionService {
caption: firstTask.caption, caption: firstTask.caption,
type: firstTask.taskType, type: firstTask.taskType,
), ),
task: ProgressBarState( task: ProgressBarState(position: 0, max: firstTask.durationMillis),
position: 0,
max: firstTask.durationMillis,
),
currentCombat: null, // 전투 상태 명시적 초기화 currentCombat: null, // 전투 상태 명시적 초기화
), ),
); );

View File

@@ -446,10 +446,12 @@ class SkillService {
// ATK 증가량 기준 정렬 // ATK 증가량 기준 정렬
buffSkills.sort((a, b) { buffSkills.sort((a, b) {
final aValue = (a.buff?.atkModifier ?? 0) + final aValue =
(a.buff?.atkModifier ?? 0) +
(a.buff?.defModifier ?? 0) * 0.5 + (a.buff?.defModifier ?? 0) * 0.5 +
(a.buff?.criRateModifier ?? 0) * 0.3; (a.buff?.criRateModifier ?? 0) * 0.3;
final bValue = (b.buff?.atkModifier ?? 0) + final bValue =
(b.buff?.atkModifier ?? 0) +
(b.buff?.defModifier ?? 0) * 0.5 + (b.buff?.defModifier ?? 0) * 0.5 +
(b.buff?.criRateModifier ?? 0) * 0.3; (b.buff?.criRateModifier ?? 0) * 0.3;
return bValue.compareTo(aValue); return bValue.compareTo(aValue);
@@ -470,9 +472,11 @@ class SkillService {
// 디버프 효과 크기 기준 정렬 (음수 값이므로 절대값으로 비교) // 디버프 효과 크기 기준 정렬 (음수 값이므로 절대값으로 비교)
debuffSkills.sort((a, b) { debuffSkills.sort((a, b) {
final aValue = (a.buff?.atkModifier ?? 0).abs() + final aValue =
(a.buff?.atkModifier ?? 0).abs() +
(a.buff?.defModifier ?? 0).abs() * 0.5; (a.buff?.defModifier ?? 0).abs() * 0.5;
final bValue = (b.buff?.atkModifier ?? 0).abs() + final bValue =
(b.buff?.atkModifier ?? 0).abs() +
(b.buff?.defModifier ?? 0).abs() * 0.5; (b.buff?.defModifier ?? 0).abs() * 0.5;
return bValue.compareTo(aValue); return bValue.compareTo(aValue);
}); });