Compare commits
2 Commits
61edd87252
...
21d8febeb0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21d8febeb0 | ||
|
|
5487c79474 |
@@ -104,7 +104,8 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ExpBar 초기화 (원본 743-746줄)
|
// ExpBar 초기화 (원본 743-746줄)
|
||||||
final expBar = ProgressBarState(position: 0, max: pq_logic.levelUpTime(1));
|
final expBar =
|
||||||
|
ProgressBarState(position: 0, max: ExpConstants.requiredExp(1));
|
||||||
|
|
||||||
// PlotBar 초기화 - Prologue 5분 (300초)
|
// PlotBar 초기화 - Prologue 5분 (300초)
|
||||||
final plotBar = const ProgressBarState(position: 0, max: 300);
|
final plotBar = const ProgressBarState(position: 0, max: 300);
|
||||||
@@ -253,6 +254,10 @@ class ProgressService {
|
|||||||
final gain = progress.currentTask.type == TaskType.kill;
|
final gain = progress.currentTask.type == TaskType.kill;
|
||||||
final incrementSeconds = progress.task.max ~/ 1000;
|
final incrementSeconds = progress.task.max ~/ 1000;
|
||||||
|
|
||||||
|
// 몬스터 경험치 미리 저장 (currentCombat이 null되기 전)
|
||||||
|
final int monsterExpReward =
|
||||||
|
progress.currentCombat?.monsterStats.expReward ?? 0;
|
||||||
|
|
||||||
// 킬 태스크 완료 시 전투 결과 반영 및 전리품 획득
|
// 킬 태스크 완료 시 전투 결과 반영 및 전리품 획득
|
||||||
if (gain) {
|
if (gain) {
|
||||||
// 전투 결과에 따라 플레이어 HP 업데이트 + 전투 후 회복
|
// 전투 결과에 따라 플레이어 HP 업데이트 + 전투 후 회복
|
||||||
@@ -354,19 +359,26 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gain XP / level up.
|
// Gain XP / level up (몬스터 경험치 기반)
|
||||||
// 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음
|
// 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음
|
||||||
if (gain) {
|
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) {
|
||||||
if (progress.exp.position >= progress.exp.max &&
|
final newExpPos = progress.exp.position + monsterExpReward;
|
||||||
nextState.traits.level < 100) {
|
|
||||||
|
// 레벨업 체크 (경험치가 필요량 이상일 때)
|
||||||
|
if (newExpPos >= progress.exp.max) {
|
||||||
|
// 초과 경험치 계산
|
||||||
|
final overflowExp = newExpPos - progress.exp.max;
|
||||||
nextState = _levelUp(nextState);
|
nextState = _levelUp(nextState);
|
||||||
leveledUp = true;
|
leveledUp = true;
|
||||||
progress = nextState.progress;
|
progress = nextState.progress;
|
||||||
} else if (nextState.traits.level < 100) {
|
|
||||||
final uncappedExp = progress.exp.position + incrementSeconds;
|
// 초과 경험치를 다음 레벨에 적용
|
||||||
final int newExpPos = uncappedExp > progress.exp.max
|
if (overflowExp > 0 && nextState.traits.level < 100) {
|
||||||
? progress.exp.max
|
progress = progress.copyWith(
|
||||||
: uncappedExp;
|
exp: progress.exp.copyWith(position: overflowExp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
progress = progress.copyWith(
|
progress = progress.copyWith(
|
||||||
exp: progress.exp.copyWith(position: newExpPos),
|
exp: progress.exp.copyWith(position: newExpPos),
|
||||||
);
|
);
|
||||||
@@ -870,15 +882,18 @@ class ProgressService {
|
|||||||
nextState = _levelUp(nextState);
|
nextState = _levelUp(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
final progress = nextState.progress.copyWith(
|
// 태스크 바 완료 처리
|
||||||
|
var progress = nextState.progress.copyWith(
|
||||||
task: nextState.progress.task.copyWith(
|
task: nextState.progress.task.copyWith(
|
||||||
position: nextState.progress.task.max,
|
position: nextState.progress.task.max,
|
||||||
),
|
),
|
||||||
plot: nextState.progress.plot.copyWith(
|
|
||||||
position: nextState.progress.plot.max,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return nextState.copyWith(progress: progress);
|
nextState = nextState.copyWith(progress: progress);
|
||||||
|
|
||||||
|
// 디버그 모드에서는 completeAct 직접 호출하여 plotStageCount 즉시 업데이트
|
||||||
|
// 시네마틱은 생략하고 바로 다음 Act로 진입
|
||||||
|
final actResult = completeAct(nextState);
|
||||||
|
return actResult.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameState _applyReward(GameState state, pq_logic.RewardKind reward) {
|
GameState _applyReward(GameState state, pq_logic.RewardKind reward) {
|
||||||
@@ -912,7 +927,7 @@ class ProgressService {
|
|||||||
|
|
||||||
final expBar = ProgressBarState(
|
final expBar = ProgressBarState(
|
||||||
position: 0,
|
position: 0,
|
||||||
max: pq_logic.levelUpTime(nextLevel),
|
max: ExpConstants.requiredExp(nextLevel),
|
||||||
);
|
);
|
||||||
final progress = nextState.progress.copyWith(exp: expBar);
|
final progress = nextState.progress.copyWith(exp: expBar);
|
||||||
nextState = nextState.copyWith(progress: progress);
|
nextState = nextState.copyWith(progress: progress);
|
||||||
|
|||||||
@@ -7,36 +7,27 @@ library;
|
|||||||
class ExpConstants {
|
class ExpConstants {
|
||||||
ExpConstants._();
|
ExpConstants._();
|
||||||
|
|
||||||
/// 기본 경험치 값
|
/// 레벨업에 필요한 경험치 계산 (몬스터 기반)
|
||||||
static const int baseExp = 100;
|
|
||||||
|
|
||||||
/// 레벨 구간별 경험치 증가율 (tiered growth rate)
|
|
||||||
/// - 1-30: 1.10 (초반 빠른 진행)
|
|
||||||
/// - 31-60: 1.12 (중반 적정 속도)
|
|
||||||
/// - 61-100: 1.14 (후반 도전)
|
|
||||||
static double _getGrowthRate(int level) {
|
|
||||||
if (level <= 30) return 1.10;
|
|
||||||
if (level <= 60) return 1.12;
|
|
||||||
return 1.14;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 레벨업에 필요한 경험치 계산 (구간별 차등 적용)
|
|
||||||
///
|
///
|
||||||
/// 조정 후 예상:
|
/// 공식: (10 + level * 5) * (25 + level / 3)
|
||||||
/// 레벨 10: ~259 exp
|
/// - 몬스터 경험치와 동기화 (MonsterBaseStats.exp = 10 + level * 5)
|
||||||
/// 레벨 30: ~1,744 exp
|
/// - 레벨당 약 25~58마리 처치 필요
|
||||||
/// 레벨 50: ~9,705 exp
|
///
|
||||||
/// 레벨 80: ~133,860 exp
|
/// 예상:
|
||||||
/// 레벨 100: ~636,840 exp
|
/// 레벨 1: 15 * 25 = 375 exp (~25마리)
|
||||||
|
/// 레벨 10: 60 * 28 = 1,680 exp (~28마리)
|
||||||
|
/// 레벨 30: 160 * 35 = 5,600 exp (~35마리)
|
||||||
|
/// 레벨 50: 260 * 42 = 10,920 exp (~42마리)
|
||||||
|
/// 레벨 80: 410 * 52 = 21,320 exp (~52마리)
|
||||||
|
/// 레벨 100: 510 * 58 = 29,580 exp (~58마리)
|
||||||
static int requiredExp(int level) {
|
static int requiredExp(int level) {
|
||||||
if (level <= 0) return baseExp;
|
if (level <= 0) return 375;
|
||||||
|
|
||||||
// 구간별 복합 성장 계산
|
// 해당 레벨 몬스터 경험치 = 10 + level * 5
|
||||||
double result = baseExp.toDouble();
|
final monsterExp = 10 + level * 5;
|
||||||
for (int i = 1; i <= level; i++) {
|
// 필요 킬 수 = 25 + level / 3 (레벨이 올라갈수록 약간 더 많이)
|
||||||
result *= _getGrowthRate(i);
|
final killsRequired = 25 + level ~/ 3;
|
||||||
}
|
return monsterExp * killsRequired;
|
||||||
return result.round();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 총 누적 경험치 계산 (특정 레벨까지)
|
/// 총 누적 경험치 계산 (특정 레벨까지)
|
||||||
@@ -159,14 +150,16 @@ class MonsterBaseStats {
|
|||||||
/// 레벨 기반 기본 스탯 생성
|
/// 레벨 기반 기본 스탯 생성
|
||||||
///
|
///
|
||||||
/// HP: 50 + level * 20 + (level^2 / 5)
|
/// HP: 50 + level * 20 + (level^2 / 5)
|
||||||
/// ATK: 5 + level * 4 (플레이어 DEF 스케일링에 맞춰 상향)
|
/// ATK: 10 + level * 12 (장비 DEF 스케일링 대응)
|
||||||
|
/// - 장비 DEF ≈ level * 16 (9개 방어구 합산)
|
||||||
|
/// - 데미지 공식: ATK - DEF * 0.5 → 의미있는 피해를 위해 상향
|
||||||
/// DEF: 2 + level * 2
|
/// DEF: 2 + level * 2
|
||||||
/// EXP: 10 + level * 5
|
/// EXP: 10 + level * 5
|
||||||
/// GOLD: 5 + level * 3
|
/// GOLD: 5 + level * 3
|
||||||
factory MonsterBaseStats.forLevel(int level) {
|
factory MonsterBaseStats.forLevel(int level) {
|
||||||
return MonsterBaseStats(
|
return MonsterBaseStats(
|
||||||
hp: 50 + level * 20 + (level * level ~/ 5),
|
hp: 50 + level * 20 + (level * level ~/ 5),
|
||||||
atk: 5 + level * 4,
|
atk: 10 + level * 12,
|
||||||
def: 2 + level * 2,
|
def: 2 + level * 2,
|
||||||
exp: 10 + level * 5,
|
exp: 10 + level * 5,
|
||||||
gold: 5 + level * 3,
|
gold: 5 + level * 3,
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
|||||||
import 'package:asciineverdie/src/core/engine/progress_loop.dart';
|
import 'package:asciineverdie/src/core/engine/progress_loop.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_stats.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/monster_combat_stats.dart';
|
||||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_repository.dart';
|
import 'package:asciineverdie/src/core/storage/save_repository.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_service.dart';
|
import 'package:asciineverdie/src/core/storage/save_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
class _FakeSaveManager implements SaveManager {
|
class _FakeSaveManager implements SaveManager {
|
||||||
@@ -54,6 +58,37 @@ void main() {
|
|||||||
|
|
||||||
test('autosaves on level-up and stop when configured', () async {
|
test('autosaves on level-up and stop when configured', () async {
|
||||||
final saveManager = _FakeSaveManager();
|
final saveManager = _FakeSaveManager();
|
||||||
|
|
||||||
|
// 레벨 1에서 레벨업에 필요한 경험치
|
||||||
|
final requiredExp = ExpConstants.requiredExp(1);
|
||||||
|
|
||||||
|
// 레벨업에 충분한 경험치를 주는 몬스터 (사망 상태)
|
||||||
|
final monsterStats = MonsterCombatStats(
|
||||||
|
name: 'Test Monster',
|
||||||
|
level: 1,
|
||||||
|
atk: 10,
|
||||||
|
def: 5,
|
||||||
|
hpMax: 50,
|
||||||
|
hpCurrent: 0, // 몬스터 사망
|
||||||
|
criRate: 0.05,
|
||||||
|
criDamage: 1.5,
|
||||||
|
evasion: 0.0,
|
||||||
|
accuracy: 0.8,
|
||||||
|
attackDelayMs: 1000,
|
||||||
|
expReward: requiredExp + 100, // 레벨업에 충분한 경험치
|
||||||
|
);
|
||||||
|
|
||||||
|
final combatState = CombatState(
|
||||||
|
playerStats: CombatStats.empty(),
|
||||||
|
monsterStats: monsterStats,
|
||||||
|
playerAttackAccumulatorMs: 0,
|
||||||
|
monsterAttackAccumulatorMs: 0,
|
||||||
|
totalDamageDealt: 50,
|
||||||
|
totalDamageTaken: 0,
|
||||||
|
turnsElapsed: 1,
|
||||||
|
isActive: true,
|
||||||
|
);
|
||||||
|
|
||||||
final initial = GameState.withSeed(
|
final initial = GameState.withSeed(
|
||||||
seed: 123,
|
seed: 123,
|
||||||
traits: const Traits(
|
traits: const Traits(
|
||||||
@@ -74,15 +109,16 @@ void main() {
|
|||||||
hpMax: 9,
|
hpMax: 9,
|
||||||
mpMax: 8,
|
mpMax: 8,
|
||||||
),
|
),
|
||||||
progress: const ProgressState(
|
progress: ProgressState(
|
||||||
task: ProgressBarState(position: 1200, max: 1200),
|
task: const ProgressBarState(position: 1200, max: 1200),
|
||||||
quest: ProgressBarState(position: 0, max: 10),
|
quest: const ProgressBarState(position: 0, max: 10),
|
||||||
plot: ProgressBarState(position: 0, max: 10),
|
plot: const ProgressBarState(position: 0, max: 10),
|
||||||
exp: ProgressBarState(position: 3, max: 3),
|
exp: ProgressBarState(position: requiredExp - 50, max: requiredExp),
|
||||||
encumbrance: ProgressBarState(position: 0, max: 0),
|
encumbrance: const ProgressBarState(position: 0, max: 0),
|
||||||
currentTask: TaskInfo(caption: 'Battle', type: TaskType.kill),
|
currentTask: const TaskInfo(caption: 'Battle', type: TaskType.kill),
|
||||||
plotStageCount: 1,
|
plotStageCount: 1,
|
||||||
questCount: 0,
|
questCount: 0,
|
||||||
|
currentCombat: combatState,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_stats.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/monster_combat_stats.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/pq_logic.dart' as pq_logic;
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -62,6 +65,36 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('tick levels up when EXP is full during kill task', () {
|
test('tick levels up when EXP is full during kill task', () {
|
||||||
|
// 레벨업에 필요한 경험치 (레벨 1 → 2)
|
||||||
|
final requiredExp = ExpConstants.requiredExp(1);
|
||||||
|
|
||||||
|
// 몬스터가 requiredExp 이상의 경험치를 주도록 설정
|
||||||
|
final monsterStats = MonsterCombatStats(
|
||||||
|
name: 'Test Monster',
|
||||||
|
level: 10,
|
||||||
|
atk: 10,
|
||||||
|
def: 5,
|
||||||
|
hpMax: 100,
|
||||||
|
hpCurrent: 0, // 몬스터 사망 상태
|
||||||
|
criRate: 0.05,
|
||||||
|
criDamage: 1.5,
|
||||||
|
evasion: 0.0,
|
||||||
|
accuracy: 0.8,
|
||||||
|
attackDelayMs: 1000,
|
||||||
|
expReward: requiredExp + 100, // 레벨업에 충분한 경험치
|
||||||
|
);
|
||||||
|
|
||||||
|
final combatState = CombatState(
|
||||||
|
playerStats: CombatStats.empty(),
|
||||||
|
monsterStats: monsterStats,
|
||||||
|
playerAttackAccumulatorMs: 0,
|
||||||
|
monsterAttackAccumulatorMs: 0,
|
||||||
|
totalDamageDealt: 100,
|
||||||
|
totalDamageTaken: 0,
|
||||||
|
turnsElapsed: 1,
|
||||||
|
isActive: true,
|
||||||
|
);
|
||||||
|
|
||||||
final initial = GameState.withSeed(
|
final initial = GameState.withSeed(
|
||||||
seed: 7,
|
seed: 7,
|
||||||
traits: const Traits(
|
traits: const Traits(
|
||||||
@@ -82,15 +115,16 @@ void main() {
|
|||||||
hpMax: 10,
|
hpMax: 10,
|
||||||
mpMax: 11,
|
mpMax: 11,
|
||||||
),
|
),
|
||||||
progress: const ProgressState(
|
progress: ProgressState(
|
||||||
task: ProgressBarState(position: 1000, max: 1000),
|
task: const ProgressBarState(position: 1000, max: 1000),
|
||||||
quest: ProgressBarState(position: 0, max: 10),
|
quest: const ProgressBarState(position: 0, max: 10),
|
||||||
plot: ProgressBarState(position: 0, max: 10),
|
plot: const ProgressBarState(position: 0, max: 10),
|
||||||
exp: ProgressBarState(position: 5, max: 5),
|
exp: ProgressBarState(position: requiredExp - 50, max: requiredExp),
|
||||||
encumbrance: ProgressBarState(position: 0, max: 0),
|
encumbrance: const ProgressBarState(position: 0, max: 0),
|
||||||
currentTask: TaskInfo(caption: 'Battle', type: TaskType.kill),
|
currentTask: const TaskInfo(caption: 'Battle', type: TaskType.kill),
|
||||||
plotStageCount: 1,
|
plotStageCount: 1,
|
||||||
questCount: 0,
|
questCount: 0,
|
||||||
|
currentCombat: combatState,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,8 +135,8 @@ void main() {
|
|||||||
expect(result.state.traits.level, 2);
|
expect(result.state.traits.level, 2);
|
||||||
expect(result.state.stats.hpMax, greaterThan(initial.stats.hpMax));
|
expect(result.state.stats.hpMax, greaterThan(initial.stats.hpMax));
|
||||||
expect(result.state.stats.mpMax, greaterThan(initial.stats.mpMax));
|
expect(result.state.stats.mpMax, greaterThan(initial.stats.mpMax));
|
||||||
expect(result.state.progress.exp.position, 0);
|
// 새 레벨의 필요 경험치로 초기화됨
|
||||||
expect(result.state.progress.exp.max, pq_logic.levelUpTime(2));
|
expect(result.state.progress.exp.max, ExpConstants.requiredExp(2));
|
||||||
// 태스크 완료 후 새 태스크가 자동으로 시작됨
|
// 태스크 완료 후 새 태스크가 자동으로 시작됨
|
||||||
expect(result.state.progress.task.position, 0);
|
expect(result.state.progress.task.position, 0);
|
||||||
expect(result.state.progress.task.max, greaterThan(0));
|
expect(result.state.progress.task.max, greaterThan(0));
|
||||||
|
|||||||
Reference in New Issue
Block a user