refactor(engine): progress_service 832줄 → 543줄 분할
- kill_task_handler.dart (133줄): 킬 태스크 완료, 전리품, 보스 - quest_completion_handler.dart (177줄): 퀘스트/플롯 진행 - exp_handler.dart (92줄): 경험치 획득, 레벨업 - 콜백 기반 의존성 주입으로 순환 참조 방지
This commit is contained in:
92
lib/src/core/engine/exp_handler.dart
Normal file
92
lib/src/core/engine/exp_handler.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:asciineverdie/data/race_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
|
|
||||||
|
/// 경험치(EXP) 획득 및 레벨업(level-up) 처리를 담당하는 핸들러.
|
||||||
|
class ExpHandler {
|
||||||
|
const ExpHandler({
|
||||||
|
required this.mutations,
|
||||||
|
required this.rewards,
|
||||||
|
required this.recalculateEncumbrance,
|
||||||
|
});
|
||||||
|
|
||||||
|
final GameMutations mutations;
|
||||||
|
final RewardService rewards;
|
||||||
|
|
||||||
|
/// 무게(encumbrance) 재계산 콜백
|
||||||
|
final GameState Function(GameState) recalculateEncumbrance;
|
||||||
|
|
||||||
|
/// 경험치 획득 및 레벨업 처리
|
||||||
|
({GameState state, ProgressState progress, bool leveledUp}) handleExpGain(
|
||||||
|
GameState state,
|
||||||
|
ProgressState progress,
|
||||||
|
int monsterExpReward,
|
||||||
|
) {
|
||||||
|
var nextState = state;
|
||||||
|
var leveledUp = false;
|
||||||
|
|
||||||
|
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: overflowExp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progress = progress.copyWith(
|
||||||
|
exp: progress.exp.copyWith(position: newExpPos),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (state: nextState, progress: progress, leveledUp: leveledUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레벨업 처리 (스탯 증가, 스킬/주문 획득)
|
||||||
|
GameState levelUp(GameState state) {
|
||||||
|
// 최대 레벨(100) 안전장치: 이미 100레벨이면 레벨업하지 않음
|
||||||
|
if (state.traits.level >= 100) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextLevel = state.traits.level + 1;
|
||||||
|
final rng = state.rng;
|
||||||
|
|
||||||
|
// HP/MP 증가량 (PlayerScaling 기반 + 랜덤 변동)
|
||||||
|
// 기존: CON/3 + 1 + random(0-3) → ~6-9 HP/레벨 (너무 낮음)
|
||||||
|
// 신규: 18 + CON/5 + random(0-4) → ~20-25 HP/레벨 (생존율 개선)
|
||||||
|
final hpGain = 18 + state.stats.con ~/ 5 + rng.nextInt(5);
|
||||||
|
final mpGain = 6 + state.stats.intelligence ~/ 5 + rng.nextInt(3);
|
||||||
|
|
||||||
|
var nextState = state.copyWith(
|
||||||
|
traits: state.traits.copyWith(level: nextLevel),
|
||||||
|
stats: state.stats.copyWith(
|
||||||
|
hpMax: state.stats.hpMax + hpGain,
|
||||||
|
mpMax: state.stats.mpMax + mpGain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 스탯 2회, 주문(spell) 1회 획득 (원본 레벨업 규칙)
|
||||||
|
nextState = mutations.winStat(nextState);
|
||||||
|
nextState = mutations.winStat(nextState);
|
||||||
|
nextState = mutations.winSpell(nextState, nextState.stats.wis, nextLevel);
|
||||||
|
|
||||||
|
final expBar = ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: ExpConstants.requiredExp(nextLevel),
|
||||||
|
);
|
||||||
|
final progress = nextState.progress.copyWith(exp: expBar);
|
||||||
|
nextState = nextState.copyWith(progress: progress);
|
||||||
|
return recalculateEncumbrance(nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
lib/src/core/engine/kill_task_handler.dart
Normal file
129
lib/src/core/engine/kill_task_handler.dart
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import 'package:asciineverdie/data/class_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/loot_handler.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
|
/// 킬 태스크(kill task) 완료 시 처리를 담당하는 핸들러.
|
||||||
|
///
|
||||||
|
/// HP 회복, 전리품(loot) 획득, 보스 처리 등을 수행한다.
|
||||||
|
class KillTaskHandler {
|
||||||
|
const KillTaskHandler({
|
||||||
|
required this.config,
|
||||||
|
required this.lootHandler,
|
||||||
|
required this.completeActFn,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PqConfig config;
|
||||||
|
final LootHandler lootHandler;
|
||||||
|
|
||||||
|
/// Act 완료 처리 콜백 (ProgressService.completeAct 위임)
|
||||||
|
final ({GameState state, bool gameComplete}) Function(GameState)
|
||||||
|
completeActFn;
|
||||||
|
|
||||||
|
/// 킬 태스크 완료 처리 (HP 회복, 전리품, 보스 처리)
|
||||||
|
({
|
||||||
|
GameState state,
|
||||||
|
ProgressState progress,
|
||||||
|
QueueState queue,
|
||||||
|
ProgressTickResult? earlyReturn,
|
||||||
|
})
|
||||||
|
handle(GameState state, ProgressState progress, QueueState queue) {
|
||||||
|
var nextState = state;
|
||||||
|
|
||||||
|
// 전투 후 HP 회복(heal)
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전리품(loot) 획득
|
||||||
|
final lootResult = lootHandler.winLoot(nextState);
|
||||||
|
nextState = lootResult.state;
|
||||||
|
|
||||||
|
// 물약(potion) 드랍 로그 추가
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 최종 보스(final boss) 처치 체크
|
||||||
|
if (progress.finalBossState == FinalBossState.fighting) {
|
||||||
|
progress = progress.copyWith(finalBossState: FinalBossState.defeated);
|
||||||
|
nextState = nextState.copyWith(progress: progress);
|
||||||
|
final actResult = completeActFn(nextState);
|
||||||
|
return (
|
||||||
|
state: actResult.state,
|
||||||
|
progress: actResult.state.progress,
|
||||||
|
queue: actResult.state.queue,
|
||||||
|
earlyReturn: ProgressTickResult(
|
||||||
|
state: actResult.state,
|
||||||
|
completedAct: true,
|
||||||
|
gameComplete: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
state: nextState,
|
||||||
|
progress: progress,
|
||||||
|
queue: queue,
|
||||||
|
earlyReturn: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import 'package:asciineverdie/data/class_data.dart';
|
|
||||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:asciineverdie/data/race_data.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
|
||||||
import 'package:asciineverdie/src/core/engine/act_progression_service.dart';
|
|
||||||
import 'package:asciineverdie/src/core/engine/combat_tick_service.dart';
|
import 'package:asciineverdie/src/core/engine/combat_tick_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/death_handler.dart';
|
import 'package:asciineverdie/src/core/engine/death_handler.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/exp_handler.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/kill_task_handler.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/loot_handler.dart';
|
import 'package:asciineverdie/src/core/engine/loot_handler.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/market_service.dart';
|
import 'package:asciineverdie/src/core/engine/market_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/quest_completion_handler.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/task_generator.dart';
|
import 'package:asciineverdie/src/core/engine/task_generator.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
@@ -42,27 +40,68 @@ class ProgressTickResult {
|
|||||||
leveledUp || completedQuest || completedAct || playerDied || gameComplete;
|
leveledUp || completedQuest || completedAct || playerDied || gameComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drives quest/plot/task progression by applying queued actions and rewards.
|
/// 게임 진행(tick) 루프를 구동하는 메인 서비스.
|
||||||
|
///
|
||||||
|
/// 각 세부 처리는 핸들러에 위임한다:
|
||||||
|
/// - [KillTaskHandler]: 킬 태스크 완료 (HP 회복, 전리품, 보스)
|
||||||
|
/// - [QuestCompletionHandler]: 퀘스트/플롯 진행 및 Act 완료
|
||||||
|
/// - [ExpHandler]: 경험치 획득 및 레벨업
|
||||||
class ProgressService {
|
class ProgressService {
|
||||||
ProgressService({
|
ProgressService({
|
||||||
required this.config,
|
required this.config,
|
||||||
required this.mutations,
|
required this.mutations,
|
||||||
required this.rewards,
|
required this.rewards,
|
||||||
}) : _taskGenerator = TaskGenerator(config: config),
|
}) : _taskGenerator = TaskGenerator(config: config),
|
||||||
_lootHandler = LootHandler(mutations: mutations);
|
_killTaskHandler = KillTaskHandler(
|
||||||
|
config: config,
|
||||||
|
lootHandler: LootHandler(mutations: mutations),
|
||||||
|
completeActFn: _placeholder,
|
||||||
|
),
|
||||||
|
_questHandler = QuestCompletionHandler(
|
||||||
|
config: config,
|
||||||
|
rewards: rewards,
|
||||||
|
recalculateEncumbrance: _placeholderState,
|
||||||
|
),
|
||||||
|
_expHandler = ExpHandler(
|
||||||
|
mutations: mutations,
|
||||||
|
rewards: rewards,
|
||||||
|
recalculateEncumbrance: _placeholderState,
|
||||||
|
) {
|
||||||
|
// 초기화 후 콜백(callback) 바인딩
|
||||||
|
_killTaskHandler = KillTaskHandler(
|
||||||
|
config: config,
|
||||||
|
lootHandler: LootHandler(mutations: mutations),
|
||||||
|
completeActFn: completeAct,
|
||||||
|
);
|
||||||
|
_questHandler = QuestCompletionHandler(
|
||||||
|
config: config,
|
||||||
|
rewards: rewards,
|
||||||
|
recalculateEncumbrance: _recalculateEncumbrance,
|
||||||
|
);
|
||||||
|
_expHandler = ExpHandler(
|
||||||
|
mutations: mutations,
|
||||||
|
rewards: rewards,
|
||||||
|
recalculateEncumbrance: _recalculateEncumbrance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기화 시점 placeholder (바로 덮어쓰여짐)
|
||||||
|
static ({GameState state, bool gameComplete}) _placeholder(GameState s) =>
|
||||||
|
(state: s, gameComplete: false);
|
||||||
|
static GameState _placeholderState(GameState s) => s;
|
||||||
|
|
||||||
final PqConfig config;
|
final PqConfig config;
|
||||||
final GameMutations mutations;
|
final GameMutations mutations;
|
||||||
final RewardService rewards;
|
final RewardService rewards;
|
||||||
|
|
||||||
final TaskGenerator _taskGenerator;
|
final TaskGenerator _taskGenerator;
|
||||||
final LootHandler _lootHandler;
|
late KillTaskHandler _killTaskHandler;
|
||||||
|
late QuestCompletionHandler _questHandler;
|
||||||
|
late ExpHandler _expHandler;
|
||||||
static const _deathHandler = DeathHandler();
|
static const _deathHandler = DeathHandler();
|
||||||
|
|
||||||
/// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767)
|
/// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767)
|
||||||
/// Prologue 태스크들을 큐에 추가하고 첫 태스크 시작
|
|
||||||
GameState initializeNewGame(GameState state) {
|
GameState initializeNewGame(GameState state) {
|
||||||
// 초기 큐 설정 - ASCII NEVER DIE 세계관 프롤로그 (l10n 지원)
|
|
||||||
final prologueTexts = l10n.prologueTexts;
|
final prologueTexts = l10n.prologueTexts;
|
||||||
final initialQueue = <QueueEntry>[
|
final initialQueue = <QueueEntry>[
|
||||||
QueueEntry(
|
QueueEntry(
|
||||||
@@ -97,20 +136,16 @@ class ProgressService {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 첫 번째 태스크 시작 (원본 752줄)
|
|
||||||
final taskResult = pq_logic.startTask(
|
final taskResult = pq_logic.startTask(
|
||||||
state.progress,
|
state.progress,
|
||||||
l10n.taskCompiling,
|
l10n.taskCompiling,
|
||||||
2 * 1000,
|
2 * 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ExpBar 초기화 (원본 743-746줄)
|
|
||||||
final expBar = ProgressBarState(
|
final expBar = ProgressBarState(
|
||||||
position: 0,
|
position: 0,
|
||||||
max: ExpConstants.requiredExp(1),
|
max: ExpConstants.requiredExp(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
// PlotBar 초기화 - Prologue 5분 (300초)
|
|
||||||
final plotBar = const ProgressBarState(position: 0, max: 300);
|
final plotBar = const ProgressBarState(position: 0, max: 300);
|
||||||
|
|
||||||
final progress = taskResult.progress.copyWith(
|
final progress = taskResult.progress.copyWith(
|
||||||
@@ -120,7 +155,7 @@ class ProgressService {
|
|||||||
caption: '${l10n.taskCompiling}...',
|
caption: '${l10n.taskCompiling}...',
|
||||||
type: TaskType.load,
|
type: TaskType.load,
|
||||||
),
|
),
|
||||||
plotStageCount: 1, // Prologue
|
plotStageCount: 1,
|
||||||
questCount: 0,
|
questCount: 0,
|
||||||
plotHistory: [
|
plotHistory: [
|
||||||
HistoryEntry(caption: l10n.taskPrologue, isComplete: false),
|
HistoryEntry(caption: l10n.taskPrologue, isComplete: false),
|
||||||
@@ -136,7 +171,7 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a task and tags its type (kill, plot, load, neutral).
|
/// 태스크 시작 (타입 태깅 포함)
|
||||||
GameState startTask(
|
GameState startTask(
|
||||||
GameState state, {
|
GameState state, {
|
||||||
required String caption,
|
required String caption,
|
||||||
@@ -154,11 +189,11 @@ class ProgressService {
|
|||||||
return state.copyWith(progress: progress);
|
return state.copyWith(progress: progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tick the timer loop (equivalent to Timer1Timer in the original code).
|
/// 메인 틱(tick) 처리 (원본 Timer1Timer 대응)
|
||||||
ProgressTickResult tick(GameState state, int elapsedMillis) {
|
ProgressTickResult tick(GameState state, int elapsedMillis) {
|
||||||
final int clamped = elapsedMillis.clamp(0, 10000).toInt();
|
final int clamped = elapsedMillis.clamp(0, 10000).toInt();
|
||||||
|
|
||||||
// 1. 스킬 시스템 업데이트 (시간, 버프, MP 회복)
|
// 1. 스킬 시스템 업데이트
|
||||||
var nextState = _updateSkillSystem(state, clamped);
|
var nextState = _updateSkillSystem(state, clamped);
|
||||||
var progress = nextState.progress;
|
var progress = nextState.progress;
|
||||||
var queue = nextState.queue;
|
var queue = nextState.queue;
|
||||||
@@ -180,7 +215,7 @@ class ProgressService {
|
|||||||
|
|
||||||
// 4. 킬 태스크 완료 처리
|
// 4. 킬 태스크 완료 처리
|
||||||
if (gain) {
|
if (gain) {
|
||||||
final killResult = _handleKillTaskCompletion(nextState, progress, queue);
|
final killResult = _killTaskHandler.handle(nextState, progress, queue);
|
||||||
if (killResult.earlyReturn != null) return killResult.earlyReturn!;
|
if (killResult.earlyReturn != null) return killResult.earlyReturn!;
|
||||||
nextState = killResult.state;
|
nextState = killResult.state;
|
||||||
progress = killResult.progress;
|
progress = killResult.progress;
|
||||||
@@ -200,14 +235,18 @@ class ProgressService {
|
|||||||
|
|
||||||
// 6. 경험치/레벨업 처리
|
// 6. 경험치/레벨업 처리
|
||||||
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) {
|
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) {
|
||||||
final expResult = _handleExpGain(nextState, progress, monsterExpReward);
|
final expResult = _expHandler.handleExpGain(
|
||||||
|
nextState,
|
||||||
|
progress,
|
||||||
|
monsterExpReward,
|
||||||
|
);
|
||||||
nextState = expResult.state;
|
nextState = expResult.state;
|
||||||
progress = expResult.progress;
|
progress = expResult.progress;
|
||||||
leveledUp = expResult.leveledUp;
|
leveledUp = expResult.leveledUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 퀘스트 진행 처리
|
// 7. 퀘스트 진행 처리
|
||||||
final questResult = _handleQuestProgress(
|
final questResult = _questHandler.handleQuestProgress(
|
||||||
nextState,
|
nextState,
|
||||||
progress,
|
progress,
|
||||||
queue,
|
queue,
|
||||||
@@ -220,7 +259,12 @@ class ProgressService {
|
|||||||
questDone = questResult.completed;
|
questDone = questResult.completed;
|
||||||
|
|
||||||
// 8. 플롯 진행 및 Act Boss 소환 처리
|
// 8. 플롯 진행 및 Act Boss 소환 처리
|
||||||
progress = _handlePlotProgress(nextState, progress, gain, incrementSeconds);
|
progress = _questHandler.handlePlotProgress(
|
||||||
|
nextState,
|
||||||
|
progress,
|
||||||
|
gain,
|
||||||
|
incrementSeconds,
|
||||||
|
);
|
||||||
|
|
||||||
// 9. 다음 태스크 디큐/생성
|
// 9. 다음 태스크 디큐/생성
|
||||||
final dequeueResult = _handleTaskDequeue(nextState, progress, queue);
|
final dequeueResult = _handleTaskDequeue(nextState, progress, queue);
|
||||||
@@ -243,6 +287,60 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 퀘스트 완료 처리 (public API 유지)
|
||||||
|
GameState completeQuest(GameState state) =>
|
||||||
|
_questHandler.completeQuest(state);
|
||||||
|
|
||||||
|
/// Act 완료 처리 (public API 유지)
|
||||||
|
({GameState state, bool gameComplete}) completeAct(GameState state) =>
|
||||||
|
_questHandler.completeAct(state);
|
||||||
|
|
||||||
|
/// 개발자 치트(cheat): 태스크 바 즉시 완료
|
||||||
|
GameState forceTaskComplete(GameState state) {
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
task: state.progress.task.copyWith(position: state.progress.task.max),
|
||||||
|
);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 개발자 치트: 퀘스트 바 즉시 완료
|
||||||
|
GameState forceQuestComplete(GameState state) {
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
task: state.progress.task.copyWith(position: state.progress.task.max),
|
||||||
|
quest: state.progress.quest.copyWith(position: state.progress.quest.max),
|
||||||
|
);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 개발자 치트: 플롯 바 즉시 완료
|
||||||
|
GameState forcePlotComplete(GameState state) {
|
||||||
|
final nextPlotStage = state.progress.plotStageCount + 1;
|
||||||
|
final targetLevel = ActMonsterLevel.forPlotStage(nextPlotStage);
|
||||||
|
var nextState = state;
|
||||||
|
|
||||||
|
while (nextState.traits.level < targetLevel &&
|
||||||
|
nextState.traits.level < 100) {
|
||||||
|
nextState = _expHandler.levelUp(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
final equipLevel = nextState.traits.level;
|
||||||
|
for (var slotIndex = 0; slotIndex < Equipment.slotCount; slotIndex++) {
|
||||||
|
nextState = mutations.winEquipByIndex(nextState, equipLevel, slotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = nextState.progress.copyWith(
|
||||||
|
task: nextState.progress.task.copyWith(
|
||||||
|
position: nextState.progress.task.max,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
nextState = nextState.copyWith(progress: progress);
|
||||||
|
|
||||||
|
final actResult = completeAct(nextState);
|
||||||
|
return actResult.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 내부 헬퍼 메서드 ──────────────────────────────────
|
||||||
|
|
||||||
/// 스킬 시스템 업데이트 (시간, 버프 정리, MP 회복)
|
/// 스킬 시스템 업데이트 (시간, 버프 정리, MP 회복)
|
||||||
GameState _updateSkillSystem(GameState state, int elapsedMs) {
|
GameState _updateSkillSystem(GameState state, int elapsedMs) {
|
||||||
final skillService = SkillService(rng: state.rng);
|
final skillService = SkillService(rng: state.rng);
|
||||||
@@ -254,7 +352,6 @@ class ProgressService {
|
|||||||
|
|
||||||
var nextState = state.copyWith(skillSystem: skillSystem);
|
var nextState = state.copyWith(skillSystem: skillSystem);
|
||||||
|
|
||||||
// 비전투 시 MP 회복
|
|
||||||
final isInCombat =
|
final isInCombat =
|
||||||
state.progress.currentTask.type == TaskType.kill &&
|
state.progress.currentTask.type == TaskType.kill &&
|
||||||
state.progress.currentCombat != null &&
|
state.progress.currentCombat != null &&
|
||||||
@@ -293,7 +390,6 @@ class ProgressService {
|
|||||||
var updatedPotionInventory = state.potionInventory;
|
var updatedPotionInventory = state.potionInventory;
|
||||||
var nextState = state;
|
var nextState = state;
|
||||||
|
|
||||||
// 킬 태스크 중 전투 진행
|
|
||||||
if (progress.currentTask.type == TaskType.kill &&
|
if (progress.currentTask.type == TaskType.kill &&
|
||||||
updatedCombat != null &&
|
updatedCombat != null &&
|
||||||
updatedCombat.isActive) {
|
updatedCombat.isActive) {
|
||||||
@@ -310,7 +406,6 @@ class ProgressService {
|
|||||||
updatedPotionInventory = combatResult.potionInventory!;
|
updatedPotionInventory = combatResult.potionInventory!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 플레이어 사망 체크
|
|
||||||
if (!updatedCombat.playerStats.isAlive) {
|
if (!updatedCombat.playerStats.isAlive) {
|
||||||
final monsterName = updatedCombat.monsterStats.name;
|
final monsterName = updatedCombat.monsterStats.name;
|
||||||
nextState = _deathHandler.processPlayerDeath(
|
nextState = _deathHandler.processPlayerDeath(
|
||||||
@@ -336,113 +431,6 @@ class ProgressService {
|
|||||||
return ProgressTickResult(state: nextState);
|
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전리품 획득
|
|
||||||
final lootResult = _lootHandler.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) {
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
state: nextState,
|
|
||||||
progress: progress,
|
|
||||||
queue: queue,
|
|
||||||
earlyReturn: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 시장/판매/구매 태스크 완료 처리
|
/// 시장/판매/구매 태스크 완료 처리
|
||||||
({
|
({
|
||||||
GameState state,
|
GameState state,
|
||||||
@@ -489,113 +477,6 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 경험치 획득 및 레벨업 처리
|
|
||||||
({GameState state, ProgressState progress, bool leveledUp}) _handleExpGain(
|
|
||||||
GameState state,
|
|
||||||
ProgressState progress,
|
|
||||||
int monsterExpReward,
|
|
||||||
) {
|
|
||||||
var nextState = state;
|
|
||||||
var leveledUp = false;
|
|
||||||
|
|
||||||
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: overflowExp),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
progress = progress.copyWith(
|
|
||||||
exp: progress.exp.copyWith(position: newExpPos),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
questDone = true;
|
|
||||||
progress = nextState.progress;
|
|
||||||
queue = nextState.queue;
|
|
||||||
} else {
|
|
||||||
progress = progress.copyWith(
|
|
||||||
quest: progress.quest.copyWith(
|
|
||||||
position: progress.quest.position + incrementSeconds,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
final actProgressionService = ActProgressionService(config: config);
|
|
||||||
final actBoss = actProgressionService.createActBoss(state);
|
|
||||||
return progress.copyWith(
|
|
||||||
plot: progress.plot.copyWith(position: 0),
|
|
||||||
currentCombat: actBoss,
|
|
||||||
pendingActCompletion: true,
|
|
||||||
);
|
|
||||||
} else if (progress.currentTask.type != TaskType.load &&
|
|
||||||
progress.plot.max > 0 &&
|
|
||||||
!progress.pendingActCompletion) {
|
|
||||||
final uncappedPlot = progress.plot.position + incrementSeconds;
|
|
||||||
final int newPlotPos = uncappedPlot > progress.plot.max
|
|
||||||
? progress.plot.max
|
|
||||||
: uncappedPlot;
|
|
||||||
return progress.copyWith(
|
|
||||||
plot: progress.plot.copyWith(position: newPlotPos),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 태스크 디큐 및 생성 처리
|
/// 태스크 디큐 및 생성 처리
|
||||||
({
|
({
|
||||||
GameState state,
|
GameState state,
|
||||||
@@ -645,178 +526,8 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advances quest completion, applies reward, and enqueues next quest task.
|
/// 무게(encumbrance) 재계산
|
||||||
GameState completeQuest(GameState state) {
|
|
||||||
final result = pq_logic.completeQuest(
|
|
||||||
config,
|
|
||||||
state.rng,
|
|
||||||
state.traits.level,
|
|
||||||
);
|
|
||||||
|
|
||||||
var nextState = _applyReward(state, result.reward);
|
|
||||||
final questCount = nextState.progress.questCount + 1;
|
|
||||||
|
|
||||||
// 퀘스트 히스토리 업데이트: 이전 퀘스트 완료 표시, 새 퀘스트 추가
|
|
||||||
final updatedQuestHistory = [
|
|
||||||
...nextState.progress.questHistory.map(
|
|
||||||
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
|
||||||
),
|
|
||||||
HistoryEntry(caption: result.caption, isComplete: false),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 퀘스트 몬스터 정보 저장 (Exterminate 타입용)
|
|
||||||
// 원본 fQuest.Caption = monsterData, fQuest.Tag = monsterIndex
|
|
||||||
final questMonster = result.monsterIndex != null
|
|
||||||
? QuestMonsterInfo(
|
|
||||||
monsterData: result.monsterName!,
|
|
||||||
monsterIndex: result.monsterIndex!,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Append quest entry to queue (task kind).
|
|
||||||
final updatedQueue = QueueState(
|
|
||||||
entries: [
|
|
||||||
...nextState.queue.entries,
|
|
||||||
QueueEntry(
|
|
||||||
kind: QueueKind.task,
|
|
||||||
durationMillis: 50 + nextState.rng.nextInt(100),
|
|
||||||
caption: result.caption,
|
|
||||||
taskType: TaskType.neutral,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update quest progress bar with reset position.
|
|
||||||
final progress = nextState.progress.copyWith(
|
|
||||||
quest: ProgressBarState(
|
|
||||||
position: 0,
|
|
||||||
max: 50 + nextState.rng.nextInt(100),
|
|
||||||
),
|
|
||||||
questCount: questCount,
|
|
||||||
questHistory: updatedQuestHistory,
|
|
||||||
currentQuestMonster: questMonster,
|
|
||||||
);
|
|
||||||
|
|
||||||
return _recalculateEncumbrance(
|
|
||||||
nextState.copyWith(progress: progress, queue: updatedQueue),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advances plot to next act and applies any act-level rewards.
|
|
||||||
/// Returns gameComplete=true if Final Boss was defeated (game ends).
|
|
||||||
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
|
||||||
final actProgressionService = ActProgressionService(config: config);
|
|
||||||
|
|
||||||
// Act 보상 먼저 적용
|
|
||||||
final actRewards = actProgressionService.getActRewards(
|
|
||||||
state.progress.plotStageCount,
|
|
||||||
);
|
|
||||||
var nextState = state;
|
|
||||||
for (final reward in actRewards) {
|
|
||||||
nextState = _applyReward(nextState, reward);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act 완료 처리 (ActProgressionService 위임)
|
|
||||||
final result = actProgressionService.completeAct(nextState);
|
|
||||||
|
|
||||||
return (
|
|
||||||
state: _recalculateEncumbrance(result.state),
|
|
||||||
gameComplete: result.gameComplete,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Developer-only cheat hooks for quickly finishing bars.
|
|
||||||
GameState forceTaskComplete(GameState state) {
|
|
||||||
final progress = state.progress.copyWith(
|
|
||||||
task: state.progress.task.copyWith(position: state.progress.task.max),
|
|
||||||
);
|
|
||||||
return state.copyWith(progress: progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
GameState forceQuestComplete(GameState state) {
|
|
||||||
final progress = state.progress.copyWith(
|
|
||||||
task: state.progress.task.copyWith(position: state.progress.task.max),
|
|
||||||
quest: state.progress.quest.copyWith(position: state.progress.quest.max),
|
|
||||||
);
|
|
||||||
return state.copyWith(progress: progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
GameState forcePlotComplete(GameState state) {
|
|
||||||
// 다음 Act의 최소 몬스터 레벨까지 레벨업
|
|
||||||
final nextPlotStage = state.progress.plotStageCount + 1;
|
|
||||||
final targetLevel = ActMonsterLevel.forPlotStage(nextPlotStage);
|
|
||||||
var nextState = state;
|
|
||||||
|
|
||||||
// 현재 레벨이 목표 레벨보다 낮으면 레벨업 (최대 100레벨)
|
|
||||||
while (nextState.traits.level < targetLevel &&
|
|
||||||
nextState.traits.level < 100) {
|
|
||||||
nextState = _levelUp(nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모든 장비 슬롯을 목표 레벨에 맞는 장비로 교체 (전투 보상 드랍 공식 사용)
|
|
||||||
final equipLevel = nextState.traits.level;
|
|
||||||
for (var slotIndex = 0; slotIndex < Equipment.slotCount; slotIndex++) {
|
|
||||||
nextState = mutations.winEquipByIndex(nextState, equipLevel, slotIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 태스크 바 완료 처리
|
|
||||||
var progress = nextState.progress.copyWith(
|
|
||||||
task: nextState.progress.task.copyWith(
|
|
||||||
position: nextState.progress.task.max,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
nextState = nextState.copyWith(progress: progress);
|
|
||||||
|
|
||||||
// 디버그 모드에서는 completeAct 직접 호출하여 plotStageCount 즉시 업데이트
|
|
||||||
// 시네마틱은 생략하고 바로 다음 Act로 진입
|
|
||||||
final actResult = completeAct(nextState);
|
|
||||||
return actResult.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameState _applyReward(GameState state, pq_logic.RewardKind reward) {
|
|
||||||
final updated = rewards.applyReward(state, reward);
|
|
||||||
return _recalculateEncumbrance(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
GameState _levelUp(GameState state) {
|
|
||||||
// 최대 레벨(100) 안전장치: 이미 100레벨이면 레벨업하지 않음
|
|
||||||
if (state.traits.level >= 100) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
final nextLevel = state.traits.level + 1;
|
|
||||||
final rng = state.rng;
|
|
||||||
|
|
||||||
// HP/MP 증가량 (PlayerScaling 기반 + 랜덤 변동)
|
|
||||||
// 기존: CON/3 + 1 + random(0-3) → ~6-9 HP/레벨 (너무 낮음)
|
|
||||||
// 신규: 18 + CON/5 + random(0-4) → ~20-25 HP/레벨 (생존율 개선)
|
|
||||||
final hpGain = 18 + state.stats.con ~/ 5 + rng.nextInt(5);
|
|
||||||
final mpGain = 6 + state.stats.intelligence ~/ 5 + rng.nextInt(3);
|
|
||||||
|
|
||||||
var nextState = state.copyWith(
|
|
||||||
traits: state.traits.copyWith(level: nextLevel),
|
|
||||||
stats: state.stats.copyWith(
|
|
||||||
hpMax: state.stats.hpMax + hpGain,
|
|
||||||
mpMax: state.stats.mpMax + mpGain,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Win two stats and a spell, matching the original leveling rules.
|
|
||||||
nextState = mutations.winStat(nextState);
|
|
||||||
nextState = mutations.winStat(nextState);
|
|
||||||
nextState = mutations.winSpell(nextState, nextState.stats.wis, nextLevel);
|
|
||||||
|
|
||||||
final expBar = ProgressBarState(
|
|
||||||
position: 0,
|
|
||||||
max: ExpConstants.requiredExp(nextLevel),
|
|
||||||
);
|
|
||||||
final progress = nextState.progress.copyWith(exp: expBar);
|
|
||||||
nextState = nextState.copyWith(progress: progress);
|
|
||||||
return _recalculateEncumbrance(nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
GameState _recalculateEncumbrance(GameState state) {
|
GameState _recalculateEncumbrance(GameState state) {
|
||||||
// items에는 Gold가 포함되지 않음 (inventory.gold 필드로 관리)
|
|
||||||
final encumValue = state.inventory.items.fold<int>(
|
final encumValue = state.inventory.items.fold<int>(
|
||||||
0,
|
0,
|
||||||
(sum, item) => sum + item.count,
|
(sum, item) => sum + item.count,
|
||||||
|
|||||||
177
lib/src/core/engine/quest_completion_handler.dart
Normal file
177
lib/src/core/engine/quest_completion_handler.dart
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import 'package:asciineverdie/src/core/engine/act_progression_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
|
/// 퀘스트(quest) 완료 및 플롯(plot) 진행을 담당하는 핸들러.
|
||||||
|
class QuestCompletionHandler {
|
||||||
|
const QuestCompletionHandler({
|
||||||
|
required this.config,
|
||||||
|
required this.rewards,
|
||||||
|
required this.recalculateEncumbrance,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PqConfig config;
|
||||||
|
final RewardService rewards;
|
||||||
|
|
||||||
|
/// 무게(encumbrance) 재계산 콜백
|
||||||
|
final GameState Function(GameState) recalculateEncumbrance;
|
||||||
|
|
||||||
|
/// 퀘스트 진행 처리
|
||||||
|
({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);
|
||||||
|
questDone = true;
|
||||||
|
progress = nextState.progress;
|
||||||
|
queue = nextState.queue;
|
||||||
|
} else {
|
||||||
|
progress = progress.copyWith(
|
||||||
|
quest: progress.quest.copyWith(
|
||||||
|
position: progress.quest.position + incrementSeconds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
state: nextState,
|
||||||
|
progress: progress,
|
||||||
|
queue: queue,
|
||||||
|
completed: questDone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 퀘스트 완료 처리 (보상 적용, 다음 퀘스트 생성)
|
||||||
|
GameState completeQuest(GameState state) {
|
||||||
|
final result = pq_logic.completeQuest(
|
||||||
|
config,
|
||||||
|
state.rng,
|
||||||
|
state.traits.level,
|
||||||
|
);
|
||||||
|
|
||||||
|
var nextState = _applyReward(state, result.reward);
|
||||||
|
final questCount = nextState.progress.questCount + 1;
|
||||||
|
|
||||||
|
// 퀘스트 히스토리(history) 업데이트: 이전 퀘스트 완료 표시, 새 퀘스트 추가
|
||||||
|
final updatedQuestHistory = [
|
||||||
|
...nextState.progress.questHistory.map(
|
||||||
|
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
||||||
|
),
|
||||||
|
HistoryEntry(caption: result.caption, isComplete: false),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 퀘스트 몬스터 정보 저장 (Exterminate 타입용)
|
||||||
|
final questMonster = result.monsterIndex != null
|
||||||
|
? QuestMonsterInfo(
|
||||||
|
monsterData: result.monsterName!,
|
||||||
|
monsterIndex: result.monsterIndex!,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 큐에 퀘스트 태스크 추가
|
||||||
|
final updatedQueue = QueueState(
|
||||||
|
entries: [
|
||||||
|
...nextState.queue.entries,
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 50 + nextState.rng.nextInt(100),
|
||||||
|
caption: result.caption,
|
||||||
|
taskType: TaskType.neutral,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 퀘스트 진행 바(bar) 리셋
|
||||||
|
final progress = nextState.progress.copyWith(
|
||||||
|
quest: ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: 50 + nextState.rng.nextInt(100),
|
||||||
|
),
|
||||||
|
questCount: questCount,
|
||||||
|
questHistory: updatedQuestHistory,
|
||||||
|
currentQuestMonster: questMonster,
|
||||||
|
);
|
||||||
|
|
||||||
|
return recalculateEncumbrance(
|
||||||
|
nextState.copyWith(progress: progress, queue: updatedQueue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 플롯 진행 및 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) {
|
||||||
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
final actBoss = actProgressionService.createActBoss(state);
|
||||||
|
return progress.copyWith(
|
||||||
|
plot: progress.plot.copyWith(position: 0),
|
||||||
|
currentCombat: actBoss,
|
||||||
|
pendingActCompletion: true,
|
||||||
|
);
|
||||||
|
} else if (progress.currentTask.type != TaskType.load &&
|
||||||
|
progress.plot.max > 0 &&
|
||||||
|
!progress.pendingActCompletion) {
|
||||||
|
final uncappedPlot = progress.plot.position + incrementSeconds;
|
||||||
|
final int newPlotPos = uncappedPlot > progress.plot.max
|
||||||
|
? progress.plot.max
|
||||||
|
: uncappedPlot;
|
||||||
|
return progress.copyWith(
|
||||||
|
plot: progress.plot.copyWith(position: newPlotPos),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Act 완료 처리 (보상 적용 후 다음 Act로 진행)
|
||||||
|
/// gameComplete=true이면 최종 보스 격파로 게임 종료.
|
||||||
|
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
||||||
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
|
||||||
|
// Act 보상 먼저 적용
|
||||||
|
final actRewards = actProgressionService.getActRewards(
|
||||||
|
state.progress.plotStageCount,
|
||||||
|
);
|
||||||
|
var nextState = state;
|
||||||
|
for (final reward in actRewards) {
|
||||||
|
nextState = _applyReward(nextState, reward);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 완료 처리 (ActProgressionService 위임)
|
||||||
|
final result = actProgressionService.completeAct(nextState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
state: recalculateEncumbrance(result.state),
|
||||||
|
gameComplete: result.gameComplete,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState _applyReward(GameState state, pq_logic.RewardKind reward) {
|
||||||
|
final updated = rewards.applyReward(state, reward);
|
||||||
|
return recalculateEncumbrance(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user