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