refactor(engine): tick() 메서드 분할 (350→80 LOC)

- 8개 헬퍼 메서드로 책임 분리
- _generateNextTask() 35 LOC로 감소
This commit is contained in:
JiWoong Sul
2026-01-21 17:34:31 +09:00
parent 742b0d1773
commit 7f44e95163

View File

@@ -161,36 +161,107 @@ class ProgressService {
/// Tick the timer loop (equivalent to Timer1Timer in the original code). /// Tick the timer loop (equivalent to Timer1Timer in the original code).
ProgressTickResult tick(GameState state, int elapsedMillis) { ProgressTickResult tick(GameState state, int elapsedMillis) {
// 10000ms 제한: 100x 배속 (50ms * 100 = 5000ms) + 여유 공간
// 디버그 터보 모드(100x) 지원을 위해 확장
final int clamped = elapsedMillis.clamp(0, 10000).toInt(); final int clamped = elapsedMillis.clamp(0, 10000).toInt();
var progress = state.progress;
var queue = state.queue; // 1. 스킬 시스템 업데이트 (시간, 버프, MP 회복)
var nextState = state; var nextState = _updateSkillSystem(state, clamped);
var progress = nextState.progress;
var queue = nextState.queue;
// 2. 태스크 바 진행 중이면 전투 틱 처리
if (progress.task.position < progress.task.max) {
return _processTaskInProgress(nextState, clamped);
}
// 3. 태스크 완료 처리
final gain = progress.currentTask.type == TaskType.kill;
final incrementSeconds = progress.task.max ~/ 1000;
final int monsterExpReward =
progress.currentCombat?.monsterStats.expReward ?? 0;
var leveledUp = false; var leveledUp = false;
var questDone = false; var questDone = false;
var actDone = false; var actDone = false;
var gameComplete = false; var gameComplete = false;
// 스킬 시스템 시간 업데이트 (Phase 3) // 4. 킬 태스크 완료 처리
if (gain) {
final killResult = _handleKillTaskCompletion(nextState, progress, queue);
if (killResult.earlyReturn != null) return killResult.earlyReturn!;
nextState = killResult.state;
progress = killResult.progress;
queue = killResult.queue;
}
// 5. 시장/판매/구매 태스크 완료 처리
final marketResult = _handleMarketTaskCompletion(nextState, progress, queue);
if (marketResult.earlyReturn != null) return marketResult.earlyReturn!;
nextState = marketResult.state;
progress = marketResult.progress;
queue = marketResult.queue;
// 6. 경험치/레벨업 처리
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) {
final expResult = _handleExpGain(nextState, progress, monsterExpReward);
nextState = expResult.state;
progress = expResult.progress;
leveledUp = expResult.leveledUp;
}
// 7. 퀘스트 진행 처리
final questResult = _handleQuestProgress(
nextState, progress, queue, gain, incrementSeconds,
);
nextState = questResult.state;
progress = questResult.progress;
queue = questResult.queue;
questDone = questResult.completed;
// 8. 플롯 진행 및 Act Boss 소환 처리
progress = _handlePlotProgress(
nextState, progress, gain, incrementSeconds,
);
// 9. 다음 태스크 디큐/생성
final dequeueResult = _handleTaskDequeue(nextState, progress, queue);
nextState = dequeueResult.state;
progress = dequeueResult.progress;
queue = dequeueResult.queue;
actDone = dequeueResult.actDone;
gameComplete = dequeueResult.gameComplete;
nextState = _recalculateEncumbrance(
nextState.copyWith(progress: progress, queue: queue),
);
return ProgressTickResult(
state: nextState,
leveledUp: leveledUp,
completedQuest: questDone,
completedAct: actDone,
gameComplete: gameComplete,
);
}
/// 스킬 시스템 업데이트 (시간, 버프 정리, MP 회복)
GameState _updateSkillSystem(GameState state, int elapsedMs) {
final skillService = SkillService(rng: state.rng); final skillService = SkillService(rng: state.rng);
var skillSystem = skillService.updateElapsedTime( var skillSystem = skillService.updateElapsedTime(
state.skillSystem, state.skillSystem,
clamped, elapsedMs,
); );
// 만료된 버프 정리
skillSystem = skillService.cleanupExpiredBuffs(skillSystem); skillSystem = skillService.cleanupExpiredBuffs(skillSystem);
var nextState = state.copyWith(skillSystem: skillSystem);
// 비전투 시 MP 회복 // 비전투 시 MP 회복
final isInCombat = final isInCombat =
progress.currentTask.type == TaskType.kill && state.progress.currentTask.type == TaskType.kill &&
progress.currentCombat != null && state.progress.currentCombat != null &&
progress.currentCombat!.isActive; state.progress.currentCombat!.isActive;
if (!isInCombat && nextState.stats.mp < nextState.stats.mpMax) { if (!isInCombat && nextState.stats.mp < nextState.stats.mpMax) {
final mpRegen = skillService.calculateMpRegen( final mpRegen = skillService.calculateMpRegen(
elapsedMs: clamped, elapsedMs: elapsedMs,
isInCombat: false, isInCombat: false,
wis: nextState.stats.wis, wis: nextState.stats.wis,
); );
@@ -205,232 +276,271 @@ class ProgressService {
} }
} }
nextState = nextState.copyWith(skillSystem: skillSystem); return nextState;
}
// Advance task bar if still running. /// 태스크 진행 중 처리 (전투 틱 포함)
if (progress.task.position < progress.task.max) { ProgressTickResult _processTaskInProgress(GameState state, int elapsedMs) {
final uncapped = progress.task.position + clamped; var progress = state.progress;
final int newTaskPos = uncapped > progress.task.max final uncapped = progress.task.position + elapsedMs;
? progress.task.max final int newTaskPos = uncapped > progress.task.max
: uncapped; ? progress.task.max
: uncapped;
// 킬 태스크 중 전투 진행 (CombatTickService 사용) var updatedCombat = progress.currentCombat;
var updatedCombat = progress.currentCombat; var updatedSkillSystem = state.skillSystem;
var updatedSkillSystem = nextState.skillSystem; var updatedPotionInventory = state.potionInventory;
var updatedPotionInventory = nextState.potionInventory; var nextState = state;
if (progress.currentTask.type == TaskType.kill &&
updatedCombat != null &&
updatedCombat.isActive) {
final combatTickService = CombatTickService(rng: nextState.rng);
final combatResult = combatTickService.processTick(
state: nextState,
combat: updatedCombat,
skillSystem: updatedSkillSystem,
elapsedMs: clamped,
);
updatedCombat = combatResult.combat;
updatedSkillSystem = combatResult.skillSystem;
if (combatResult.potionInventory != null) {
updatedPotionInventory = combatResult.potionInventory!;
}
// Phase 4: 플레이어 사망 체크 // 킬 태스크 중 전투 진행
if (!updatedCombat.playerStats.isAlive) { if (progress.currentTask.type == TaskType.kill &&
final monsterName = updatedCombat.monsterStats.name; updatedCombat != null &&
nextState = _processPlayerDeath( updatedCombat.isActive) {
nextState, final combatTickService = CombatTickService(rng: state.rng);
killerName: monsterName, final combatResult = combatTickService.processTick(
cause: DeathCause.monster, state: state,
); combat: updatedCombat,
return ProgressTickResult(state: nextState, playerDied: true); skillSystem: updatedSkillSystem,
} elapsedMs: elapsedMs,
);
updatedCombat = combatResult.combat;
updatedSkillSystem = combatResult.skillSystem;
if (combatResult.potionInventory != null) {
updatedPotionInventory = combatResult.potionInventory!;
} }
progress = progress.copyWith( // 플레이어 사망 체크
task: progress.task.copyWith(position: newTaskPos), if (!updatedCombat.playerStats.isAlive) {
currentCombat: updatedCombat, final monsterName = updatedCombat.monsterStats.name;
); nextState = _processPlayerDeath(
nextState = _recalculateEncumbrance( state,
nextState.copyWith( killerName: monsterName,
progress: progress, cause: DeathCause.monster,
skillSystem: updatedSkillSystem, );
potionInventory: updatedPotionInventory, return ProgressTickResult(state: nextState, playerDied: true);
), }
);
return ProgressTickResult(state: nextState);
} }
final gain = progress.currentTask.type == TaskType.kill; progress = progress.copyWith(
final incrementSeconds = progress.task.max ~/ 1000; task: progress.task.copyWith(position: newTaskPos),
currentCombat: updatedCombat,
// 몬스터 경험치 미리 저장 (currentCombat이 null되기 전) );
final int monsterExpReward = nextState = _recalculateEncumbrance(
progress.currentCombat?.monsterStats.expReward ?? 0; state.copyWith(
// 킬 태스크 완료 시 전투 결과 반영 및 전리품 획득
if (gain) {
// 전투 결과에 따라 플레이어 HP 업데이트 + 전투 후 회복
final combat = progress.currentCombat;
if (combat != null && combat.isActive) {
// 전투 중 남은 HP
final remainingHp = combat.playerStats.hpCurrent;
final maxHp = combat.playerStats.hpMax;
// 전투 승리 시 HP 회복 (50% + CON/2 + 클래스 패시브)
// 아이들 게임 특성상 전투 사이 HP가 회복되어야 지속 플레이 가능
final conBonus = nextState.stats.con ~/ 2;
var healAmount = (maxHp * 0.5).round() + conBonus;
// 클래스 패시브: 전투 후 HP 회복 (예: Garbage Collector +5%)
final klass = ClassData.findById(nextState.traits.classId);
if (klass != null) {
final postCombatHealRate =
klass.getPassiveValue(ClassPassiveType.postCombatHeal);
if (postCombatHealRate > 0) {
healAmount += (maxHp * postCombatHealRate).round();
}
}
final newHp = (remainingHp + healAmount).clamp(0, maxHp);
nextState = nextState.copyWith(
stats: nextState.stats.copyWith(hpCurrent: newHp),
);
}
// 전리품 획득 (원본 Main.pas:625-630)
final lootResult = _winLoot(nextState);
nextState = lootResult.state;
// 물약 드랍 시 전투 로그에 이벤트 추가
var combatForReset = progress.currentCombat;
if (lootResult.droppedPotion != null && combatForReset != null) {
final potionDropEvent = CombatEvent.potionDrop(
timestamp: nextState.skillSystem.elapsedMs,
potionName: lootResult.droppedPotion!.name,
isHp: lootResult.droppedPotion!.isHpPotion,
);
final updatedEvents = [...combatForReset.recentEvents, potionDropEvent];
combatForReset = combatForReset.copyWith(
recentEvents: updatedEvents.length > 10
? updatedEvents.sublist(updatedEvents.length - 10)
: updatedEvents,
);
progress = progress.copyWith(currentCombat: combatForReset);
}
// Boss 승리 처리: 시네마틱 트리거
if (progress.pendingActCompletion) {
// Act Boss를 처치했으므로 시네마틱 재생
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,
);
}
nextState = nextState.copyWith(
progress: progress, progress: progress,
queue: queue, skillSystem: updatedSkillSystem,
potionInventory: updatedPotionInventory,
),
);
return ProgressTickResult(state: nextState);
}
/// 킬 태스크 완료 처리 (HP 회복, 전리품, 보스 처리)
({
GameState state,
ProgressState progress,
QueueState queue,
ProgressTickResult? earlyReturn,
}) _handleKillTaskCompletion(
GameState state,
ProgressState progress,
QueueState queue,
) {
var nextState = state;
// 전투 후 HP 회복
final combat = progress.currentCombat;
if (combat != null && combat.isActive) {
final remainingHp = combat.playerStats.hpCurrent;
final maxHp = combat.playerStats.hpMax;
final conBonus = nextState.stats.con ~/ 2;
var healAmount = (maxHp * 0.5).round() + conBonus;
final klass = ClassData.findById(nextState.traits.classId);
if (klass != null) {
final postCombatHealRate =
klass.getPassiveValue(ClassPassiveType.postCombatHeal);
if (postCombatHealRate > 0) {
healAmount += (maxHp * postCombatHealRate).round();
}
}
final newHp = (remainingHp + healAmount).clamp(0, maxHp);
nextState = nextState.copyWith(
stats: nextState.stats.copyWith(hpCurrent: newHp),
); );
}
// 최종 보스 처치 체크 // 전리품 획득
if (progress.finalBossState == FinalBossState.fighting) { final lootResult = _winLoot(nextState);
// 글리치 갓 처치 완료 - 게임 클리어 nextState = lootResult.state;
progress = progress.copyWith(finalBossState: FinalBossState.defeated);
nextState = nextState.copyWith(progress: progress);
// completeAct를 호출하여 게임 완료 처리 // 물약 드랍 로그 추가
final actResult = completeAct(nextState); var combatForReset = progress.currentCombat;
nextState = actResult.state; if (lootResult.droppedPotion != null && combatForReset != null) {
final potionDropEvent = CombatEvent.potionDrop(
timestamp: nextState.skillSystem.elapsedMs,
potionName: lootResult.droppedPotion!.name,
isHp: lootResult.droppedPotion!.isHpPotion,
);
final updatedEvents = [...combatForReset.recentEvents, potionDropEvent];
combatForReset = combatForReset.copyWith(
recentEvents: updatedEvents.length > 10
? updatedEvents.sublist(updatedEvents.length - 10)
: updatedEvents,
);
progress = progress.copyWith(currentCombat: combatForReset);
}
return ProgressTickResult( // Boss 승리 처리
state: nextState, if (progress.pendingActCompletion) {
leveledUp: false, final cinematicEntries = pq_logic.interplotCinematic(
completedQuest: false, 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,
);
} else {
progress = progress.copyWith(
currentCombat: null,
monstersKilled: progress.monstersKilled + 1,
);
}
nextState = nextState.copyWith(progress: progress, queue: queue);
// 최종 보스 처치 체크
if (progress.finalBossState == FinalBossState.fighting) {
progress = progress.copyWith(finalBossState: FinalBossState.defeated);
nextState = nextState.copyWith(progress: progress);
final actResult = completeAct(nextState);
return (
state: actResult.state,
progress: actResult.state.progress,
queue: actResult.state.queue,
earlyReturn: ProgressTickResult(
state: actResult.state,
completedAct: true, completedAct: true,
gameComplete: true, gameComplete: true,
); ),
} );
} }
// 시장/판매/구매 태스크 완료 시 처리 (MarketService 사용) return (
final marketService = MarketService(rng: nextState.rng); state: nextState,
progress: progress,
queue: queue,
earlyReturn: null,
);
}
/// 시장/판매/구매 태스크 완료 처리
({
GameState state,
ProgressState progress,
QueueState queue,
ProgressTickResult? earlyReturn,
}) _handleMarketTaskCompletion(
GameState state,
ProgressState progress,
QueueState queue,
) {
var nextState = state;
final marketService = MarketService(rng: state.rng);
final taskType = progress.currentTask.type; final taskType = progress.currentTask.type;
if (taskType == TaskType.buying) { if (taskType == TaskType.buying) {
// 장비 구매 완료 (원본 631-634)
nextState = marketService.completeBuying(nextState); nextState = marketService.completeBuying(nextState);
progress = nextState.progress; progress = nextState.progress;
} else if (taskType == TaskType.market || taskType == TaskType.sell) { } else if (taskType == TaskType.market || taskType == TaskType.sell) {
// 시장 도착 또는 판매 완료 (원본 635-649)
final sellResult = marketService.processSell(nextState); final sellResult = marketService.processSell(nextState);
nextState = sellResult.state; nextState = sellResult.state;
progress = nextState.progress; progress = nextState.progress;
queue = nextState.queue; queue = nextState.queue;
// 판매 중이면 다른 로직 건너뛰기
if (sellResult.continuesSelling) { if (sellResult.continuesSelling) {
nextState = _recalculateEncumbrance( nextState = _recalculateEncumbrance(
nextState.copyWith(progress: progress, queue: queue), nextState.copyWith(progress: progress, queue: queue),
); );
return ProgressTickResult( return (
state: nextState, state: nextState,
leveledUp: false, progress: progress,
completedQuest: false, queue: queue,
completedAct: false, earlyReturn: ProgressTickResult(state: nextState),
); );
} }
} }
// Gain XP / level up (몬스터 경험치 기반) return (
// 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음 state: nextState,
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) { progress: progress,
// 종족 경험치 배율 적용 (예: Byte Human +5%, Callback Seraph +3%) queue: queue,
final race = RaceData.findById(nextState.traits.raceId); earlyReturn: null,
final expMultiplier = race?.expMultiplier ?? 1.0; );
final adjustedExp = (monsterExpReward * expMultiplier).round(); }
final newExpPos = progress.exp.position + adjustedExp;
// 레벨업 체크 (경험치가 필요량 이상일 때) /// 경험치 획득 및 레벨업 처리
if (newExpPos >= progress.exp.max) { ({GameState state, ProgressState progress, bool leveledUp}) _handleExpGain(
// 초과 경험치 계산 GameState state,
final overflowExp = newExpPos - progress.exp.max; ProgressState progress,
nextState = _levelUp(nextState); int monsterExpReward,
leveledUp = true; ) {
progress = nextState.progress; var nextState = state;
var leveledUp = false;
// 초과 경험치를 다음 레벨에 적용 final race = RaceData.findById(nextState.traits.raceId);
if (overflowExp > 0 && nextState.traits.level < 100) { final expMultiplier = race?.expMultiplier ?? 1.0;
progress = progress.copyWith( final adjustedExp = (monsterExpReward * expMultiplier).round();
exp: progress.exp.copyWith(position: overflowExp), final newExpPos = progress.exp.position + adjustedExp;
);
} if (newExpPos >= progress.exp.max) {
} else { final overflowExp = newExpPos - progress.exp.max;
nextState = _levelUp(nextState);
leveledUp = true;
progress = nextState.progress;
if (overflowExp > 0 && nextState.traits.level < 100) {
progress = progress.copyWith( progress = progress.copyWith(
exp: progress.exp.copyWith(position: newExpPos), exp: progress.exp.copyWith(position: overflowExp),
); );
} }
} else {
progress = progress.copyWith(
exp: progress.exp.copyWith(position: newExpPos),
);
} }
// Advance quest bar after Act I. return (state: nextState, progress: progress, leveledUp: leveledUp);
}
/// 퀘스트 진행 처리
({
GameState state,
ProgressState progress,
QueueState queue,
bool completed,
}) _handleQuestProgress(
GameState state,
ProgressState progress,
QueueState queue,
bool gain,
int incrementSeconds,
) {
var nextState = state;
var questDone = false;
final canQuestProgress = final canQuestProgress =
gain && gain &&
progress.plotStageCount > 1 && progress.plotStageCount > 1 &&
progress.questCount > 0 && progress.questCount > 0 &&
progress.quest.max > 0; progress.quest.max > 0;
if (canQuestProgress) { if (canQuestProgress) {
if (progress.quest.position + incrementSeconds >= progress.quest.max) { if (progress.quest.position + incrementSeconds >= progress.quest.max) {
nextState = completeQuest(nextState); nextState = completeQuest(nextState);
@@ -446,19 +556,31 @@ class ProgressService {
} }
} }
// 플롯(plot) 바가 완료되면 Act Boss 소환 return (
// (개선: Boss 처치 → 시네마틱 → Act 전환 순서) state: nextState,
progress: progress,
queue: queue,
completed: questDone,
);
}
/// 플롯 진행 및 Act Boss 소환 처리
ProgressState _handlePlotProgress(
GameState state,
ProgressState progress,
bool gain,
int incrementSeconds,
) {
if (gain && if (gain &&
progress.plot.max > 0 && progress.plot.max > 0 &&
progress.plot.position >= progress.plot.max && progress.plot.position >= progress.plot.max &&
!progress.pendingActCompletion) { !progress.pendingActCompletion) {
// Act Boss 소환 및 플래그 설정
final actProgressionService = ActProgressionService(config: config); final actProgressionService = ActProgressionService(config: config);
final actBoss = actProgressionService.createActBoss(nextState); final actBoss = actProgressionService.createActBoss(state);
progress = progress.copyWith( return progress.copyWith(
plot: progress.plot.copyWith(position: 0), // Plot bar 리셋 plot: progress.plot.copyWith(position: 0),
currentCombat: actBoss, currentCombat: actBoss,
pendingActCompletion: true, // Boss 처치 대기 플래그 pendingActCompletion: true,
); );
} else if (progress.currentTask.type != TaskType.load && } else if (progress.currentTask.type != TaskType.load &&
progress.plot.max > 0 && progress.plot.max > 0 &&
@@ -467,12 +589,29 @@ class ProgressService {
final int newPlotPos = uncappedPlot > progress.plot.max final int newPlotPos = uncappedPlot > progress.plot.max
? progress.plot.max ? progress.plot.max
: uncappedPlot; : uncappedPlot;
progress = progress.copyWith( return progress.copyWith(
plot: progress.plot.copyWith(position: newPlotPos), plot: progress.plot.copyWith(position: newPlotPos),
); );
} }
return progress;
}
/// 태스크 디큐 및 생성 처리
({
GameState state,
ProgressState progress,
QueueState queue,
bool actDone,
bool gameComplete,
}) _handleTaskDequeue(
GameState state,
ProgressState progress,
QueueState queue,
) {
var nextState = state;
var actDone = false;
var gameComplete = false;
// Dequeue next scripted task if available.
final dq = pq_logic.dequeue(progress, queue); final dq = pq_logic.dequeue(progress, queue);
if (dq != null) { if (dq != null) {
progress = dq.progress.copyWith( progress = dq.progress.copyWith(
@@ -480,7 +619,6 @@ class ProgressService {
); );
queue = dq.queue; queue = dq.queue;
// plot 타입이 dequeue 되면 completeAct 실행 (원본 Main.pas 로직)
if (dq.kind == QueueKind.plot) { if (dq.kind == QueueKind.plot) {
nextState = nextState.copyWith(progress: progress, queue: queue); nextState = nextState.copyWith(progress: progress, queue: queue);
final actResult = completeAct(nextState); final actResult = completeAct(nextState);
@@ -491,22 +629,17 @@ class ProgressService {
queue = nextState.queue; queue = nextState.queue;
} }
} else { } else {
// 큐가 비어있으면 새 태스크 생성 (원본 Dequeue 667-684줄)
nextState = nextState.copyWith(progress: progress, queue: queue); nextState = nextState.copyWith(progress: progress, queue: queue);
final newTaskResult = _generateNextTask(nextState); final newTaskResult = _generateNextTask(nextState);
progress = newTaskResult.progress; progress = newTaskResult.progress;
queue = newTaskResult.queue; queue = newTaskResult.queue;
} }
nextState = _recalculateEncumbrance( return (
nextState.copyWith(progress: progress, queue: queue),
);
return ProgressTickResult(
state: nextState, state: nextState,
leveledUp: leveledUp, progress: progress,
completedQuest: questDone, queue: queue,
completedAct: actDone, actDone: actDone,
gameComplete: gameComplete, gameComplete: gameComplete,
); );
} }
@@ -519,120 +652,155 @@ class ProgressService {
final queue = state.queue; final queue = state.queue;
final oldTaskType = progress.currentTask.type; final oldTaskType = progress.currentTask.type;
// 1. Encumbrance가 가득 찼으면 시장으로 이동 (원본 667-669줄) // 1. Encumbrance 초과 시 시장 이동
if (progress.encumbrance.position >= progress.encumbrance.max && if (_shouldGoToMarket(progress)) {
progress.encumbrance.max > 0) { return _createMarketTask(progress, queue);
final taskResult = pq_logic.startTask(
progress,
l10n.taskHeadingToMarket(),
4 * 1000,
);
progress = taskResult.progress.copyWith(
currentTask: TaskInfo(
caption: taskResult.caption,
type: TaskType.market,
),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (progress: progress, queue: queue);
} }
// 2. kill/heading/buying 태스크가 아니었으면 heading 또는 buying 태스크 실행 // 2. 전환 태스크 (buying/heading)
// (원본 670-677줄) - buying 완료 후 무한 루프 방지 if (_needsTransitionTask(oldTaskType)) {
if (oldTaskType != TaskType.kill && return _createTransitionTask(state, progress, queue);
oldTaskType != TaskType.neutral &&
oldTaskType != TaskType.buying) {
// Gold가 충분하면 장비 구매 (Common 장비 가격 기준)
// 실제 구매 가격과 동일한 공식 사용: level * 50
final gold = state.inventory.gold;
final equipPrice = state.traits.level * 50; // Common 장비 1개 가격
if (gold > equipPrice) {
final taskResult = pq_logic.startTask(
progress,
l10n.taskUpgradingHardware(),
5 * 1000,
);
progress = taskResult.progress.copyWith(
currentTask: TaskInfo(
caption: taskResult.caption,
type: TaskType.buying,
),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (progress: progress, queue: queue);
}
// Gold가 부족하면 전장으로 이동 (원본 674-676줄)
final taskResult = pq_logic.startTask(
progress,
l10n.taskEnteringDebugZone(),
4 * 1000,
);
progress = taskResult.progress.copyWith(
currentTask: TaskInfo(
caption: taskResult.caption,
type: TaskType.neutral,
),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (progress: progress, queue: queue);
} }
// 3. Act Boss 리트라이 체크 // 3. Act Boss 리트라이
// pendingActCompletion이 true면 Act Boss 재소환
if (state.progress.pendingActCompletion) { if (state.progress.pendingActCompletion) {
final actProgressionService = ActProgressionService(config: config); return _createActBossRetryTask(state, progress, queue);
final actBoss = actProgressionService.createActBoss(state); }
final combatCalculator = CombatCalculator(rng: state.rng);
final durationMillis = combatCalculator.estimateCombatDurationMs(
player: actBoss.playerStats,
monster: actBoss.monsterStats,
);
// 4. 최종 보스 전투
if (state.progress.finalBossState == FinalBossState.fighting &&
!state.progress.isInBossLevelingMode) {
if (state.progress.bossLevelingEndTime != null) {
progress = progress.copyWith(clearBossLevelingEndTime: true);
}
final actProgressionService = ActProgressionService(config: config);
return actProgressionService.startFinalBossFight(state, progress, queue);
}
// 5. 일반 몬스터 전투
return _createMonsterTask(state, progress, queue);
}
/// 시장 이동 조건 확인
bool _shouldGoToMarket(ProgressState progress) {
return progress.encumbrance.position >= progress.encumbrance.max &&
progress.encumbrance.max > 0;
}
/// 전환 태스크 필요 여부 확인
bool _needsTransitionTask(TaskType oldTaskType) {
return oldTaskType != TaskType.kill &&
oldTaskType != TaskType.neutral &&
oldTaskType != TaskType.buying;
}
/// 시장 이동 태스크 생성
({ProgressState progress, QueueState queue}) _createMarketTask(
ProgressState progress,
QueueState queue,
) {
final taskResult = pq_logic.startTask(
progress,
l10n.taskHeadingToMarket(),
4 * 1000,
);
final updatedProgress = taskResult.progress.copyWith(
currentTask: TaskInfo(
caption: taskResult.caption,
type: TaskType.market,
),
currentCombat: null,
);
return (progress: updatedProgress, queue: queue);
}
/// 전환 태스크 생성 (buying 또는 heading)
({ProgressState progress, QueueState queue}) _createTransitionTask(
GameState state,
ProgressState progress,
QueueState queue,
) {
final gold = state.inventory.gold;
final equipPrice = state.traits.level * 50;
// Gold 충분 시 장비 구매
if (gold > equipPrice) {
final taskResult = pq_logic.startTask( final taskResult = pq_logic.startTask(
progress, progress,
l10n.taskDebugging(actBoss.monsterStats.name), l10n.taskUpgradingHardware(),
durationMillis, 5 * 1000,
); );
final updatedProgress = taskResult.progress.copyWith(
progress = taskResult.progress.copyWith(
currentTask: TaskInfo( currentTask: TaskInfo(
caption: taskResult.caption, caption: taskResult.caption,
type: TaskType.kill, type: TaskType.buying,
monsterBaseName: actBoss.monsterStats.name,
monsterPart: '*', // Boss는 WinItem 드랍
monsterLevel: actBoss.monsterStats.level,
monsterGrade: MonsterGrade.boss,
monsterSize: getBossSizeForAct(state.progress.plotStageCount),
), ),
currentCombat: actBoss, currentCombat: null,
); );
return (progress: updatedProgress, queue: queue);
return (progress: progress, queue: queue);
} }
// 4. 최종 보스 전투 체크 // Gold 부족 시 전장 이동
// finalBossState == fighting이면 Glitch God 스폰 final taskResult = pq_logic.startTask(
// 단, 레벨링 모드 중이면 일반 몬스터로 레벨업 후 재도전 progress,
if (state.progress.finalBossState == FinalBossState.fighting) { l10n.taskEnteringDebugZone(),
if (state.progress.isInBossLevelingMode) { 4 * 1000,
// 레벨링 모드: 일반 몬스터 전투로 대체 (아래 MonsterTask로 진행) );
} else { final updatedProgress = taskResult.progress.copyWith(
// 레벨링 모드 종료 또는 첫 도전: 보스전 시작 currentTask: TaskInfo(
// 레벨링 모드가 끝났으면 타이머 초기화 caption: taskResult.caption,
if (state.progress.bossLevelingEndTime != null) { type: TaskType.neutral,
progress = progress.copyWith(clearBossLevelingEndTime: true); ),
} currentCombat: null,
final actProgressionService = ActProgressionService(config: config); );
return actProgressionService.startFinalBossFight(state, progress, queue); return (progress: updatedProgress, queue: queue);
} }
}
// 5. MonsterTask 실행 (원본 678-684줄) /// Act Boss 재도전 태스크 생성
({ProgressState progress, QueueState queue}) _createActBossRetryTask(
GameState state,
ProgressState progress,
QueueState queue,
) {
final actProgressionService = ActProgressionService(config: config);
final actBoss = actProgressionService.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,
);
final updatedProgress = taskResult.progress.copyWith(
currentTask: TaskInfo(
caption: taskResult.caption,
type: TaskType.kill,
monsterBaseName: actBoss.monsterStats.name,
monsterPart: '*',
monsterLevel: actBoss.monsterStats.level,
monsterGrade: MonsterGrade.boss,
monsterSize: getBossSizeForAct(state.progress.plotStageCount),
),
currentCombat: actBoss,
);
return (progress: updatedProgress, queue: queue);
}
/// 일반 몬스터 전투 태스크 생성
({ProgressState progress, QueueState queue}) _createMonsterTask(
GameState state,
ProgressState progress,
QueueState queue,
) {
final level = state.traits.level; final level = state.traits.level;
// 원본 Main.pas:548-551: 25% 확률로 Quest Monster 사용 // 퀘스트 몬스터 데이터 확인
// fQuest.Caption이 비어있지 않으면 해당 몬스터 데이터 전달
final questMonster = state.progress.currentQuestMonster; final questMonster = state.progress.currentQuestMonster;
final questMonsterData = questMonster?.monsterData; final questMonsterData = questMonster?.monsterData;
final questLevel = questMonsterData != null final questLevel = questMonsterData != null
@@ -640,6 +808,7 @@ class ProgressService {
0 0
: null; : null;
// 몬스터 생성
final monsterResult = pq_logic.monsterTask( final monsterResult = pq_logic.monsterTask(
config, config,
state.rng, state.rng,
@@ -648,8 +817,7 @@ class ProgressService {
questLevel, questLevel,
); );
// 전투용 몬스터 레벨 조정 (밸런스) // 몬스터 레벨 조정 (밸런스)
// Act별 최소 레벨과 플레이어 레벨 중 큰 값을 기준으로 ±3 범위 제한
final actMinLevel = ActMonsterLevel.forPlotStage( final actMinLevel = ActMonsterLevel.forPlotStage(
state.progress.plotStageCount, state.progress.plotStageCount,
); );
@@ -658,7 +826,7 @@ class ProgressService {
.clamp(math.max(1, baseLevel - 3), baseLevel + 3) .clamp(math.max(1, baseLevel - 3), baseLevel + 3)
.toInt(); .toInt();
// 전투 스탯 생성 (Phase 12: 몬스터 레벨 기반 페널티 적용) // 전투 스탯 생성
final playerCombatStats = CombatStats.fromStats( final playerCombatStats = CombatStats.fromStats(
stats: state.stats, stats: state.stats,
equipment: state.equipment, equipment: state.equipment,
@@ -673,13 +841,12 @@ class ProgressService {
plotStageCount: state.progress.plotStageCount, plotStageCount: state.progress.plotStageCount,
); );
// 전투 상태 초기화 // 전투 상태 및 지속시간
final combatState = CombatState.start( final combatState = CombatState.start(
playerStats: playerCombatStats, playerStats: playerCombatStats,
monsterStats: monsterCombatStats, monsterStats: monsterCombatStats,
); );
// 태스크 지속시간 계산 (CombatCalculator 기반)
final combatCalculator = CombatCalculator(rng: state.rng); final combatCalculator = CombatCalculator(rng: state.rng);
final durationMillis = combatCalculator.estimateCombatDurationMs( final durationMillis = combatCalculator.estimateCombatDurationMs(
player: playerCombatStats, player: playerCombatStats,
@@ -692,14 +859,14 @@ class ProgressService {
durationMillis, durationMillis,
); );
// 몬스터 사이즈 결정 (Act 기반, Phase 13) // 몬스터 사이즈 결정
final monsterSize = getMonsterSizeForAct( final monsterSize = getMonsterSizeForAct(
plotStageCount: state.progress.plotStageCount, plotStageCount: state.progress.plotStageCount,
grade: monsterResult.grade, grade: monsterResult.grade,
rng: state.rng, rng: state.rng,
); );
progress = taskResult.progress.copyWith( final updatedProgress = taskResult.progress.copyWith(
currentTask: TaskInfo( currentTask: TaskInfo(
caption: taskResult.caption, caption: taskResult.caption,
type: TaskType.kill, type: TaskType.kill,
@@ -712,7 +879,7 @@ class ProgressService {
currentCombat: combatState, currentCombat: combatState,
); );
return (progress: progress, queue: queue); return (progress: updatedProgress, queue: queue);
} }
/// Advances quest completion, applies reward, and enqueues next quest task. /// Advances quest completion, applies reward, and enqueues next quest task.