Files
asciinevrdie/lib/src/core/util/balance_constants.dart
JiWoong Sul 12f195bed7 refactor(core): 모델 및 유틸리티 개선
- GameState 확장
- BalanceConstants 조정
- PqLogic, Roman 정리
2026-01-12 20:02:50 +09:00

492 lines
13 KiB
Dart

/// 밸런스 상수 정의 (balance constants)
///
/// Phase 6: 레벨 1-100 구간의 난이도 곡선 및 스케일링 상수
library;
/// 경험치 관련 상수 (experience constants)
class ExpConstants {
ExpConstants._();
/// 레벨업에 필요한 경험치 계산 (몬스터 기반)
///
/// 공식: (10 + level * 5) * (25 + level / 3)
/// - 몬스터 경험치와 동기화 (MonsterBaseStats.exp = 10 + level * 5)
/// - 레벨당 약 25~58마리 처치 필요
///
/// 예상:
/// 레벨 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) {
if (level <= 0) return 375;
// 해당 레벨 몬스터 경험치 = 10 + level * 5
final monsterExp = 10 + level * 5;
// 필요 킬 수 = 25 + level / 3 (레벨이 올라갈수록 약간 더 많이)
final killsRequired = 25 + level ~/ 3;
return monsterExp * killsRequired;
}
/// 총 누적 경험치 계산 (특정 레벨까지)
static int totalExpToLevel(int level) {
int total = 0;
for (int i = 1; i < level; i++) {
total += requiredExp(i);
}
return total;
}
}
/// 몬스터 타입 (monster type)
enum MonsterType {
/// 일반 몬스터
normal,
/// 정예 몬스터 (Elite)
elite,
/// 미니보스
miniboss,
/// 보스
boss,
/// 최종 보스
finalBoss,
}
/// 몬스터 타입별 배율 (monster type multipliers)
class MonsterTypeMultiplier {
const MonsterTypeMultiplier({
required this.hp,
required this.atk,
required this.def,
required this.exp,
required this.gold,
});
final double hp;
final double atk;
final double def;
final double exp;
final double gold;
/// 타입별 배율 가져오기
static MonsterTypeMultiplier forType(MonsterType type) {
return switch (type) {
MonsterType.normal => normal,
MonsterType.elite => elite,
MonsterType.miniboss => miniboss,
MonsterType.boss => boss,
MonsterType.finalBoss => finalBoss,
};
}
/// 일반: 모든 스탯 1.0배
static const normal = MonsterTypeMultiplier(
hp: 1.0,
atk: 1.0,
def: 1.0,
exp: 1.0,
gold: 1.0,
);
/// 정예: HP 2배, ATK 1.3배, DEF 1.2배, EXP 3배 (상향), GOLD 2.5배
static const elite = MonsterTypeMultiplier(
hp: 2.0,
atk: 1.3,
def: 1.2,
exp: 3.0, // 2.0 → 3.0 상향
gold: 2.5,
);
/// 미니보스: HP 5배, ATK/DEF 1.5배, EXP 8배 (상향), GOLD 6배
static const miniboss = MonsterTypeMultiplier(
hp: 5.0,
atk: 1.5,
def: 1.5,
exp: 8.0, // 5.0 → 8.0 상향
gold: 6.0,
);
/// 보스: HP 8배 (하향), ATK/DEF 1.8배 (하향), EXP 25배 (상향), GOLD 15배
static const boss = MonsterTypeMultiplier(
hp: 8.0, // 10.0 → 8.0 하향 (플레이어 접근성 개선)
atk: 1.8, // 2.0 → 1.8 하향
def: 1.8, // 2.0 → 1.8 하향
exp: 25.0, // 15.0 → 25.0 상향
gold: 15.0,
);
/// 최종 보스: HP 12배 (하향), ATK/DEF 2.2배 (하향), EXP 80배 (상향), GOLD 50배
static const finalBoss = MonsterTypeMultiplier(
hp: 12.0, // 20.0 → 12.0 대폭 하향 (클리어 가능성 확보)
atk: 2.2, // 2.5 → 2.2 하향
def: 2.2, // 2.5 → 2.2 하향
exp: 80.0, // 50.0 → 80.0 상향
gold: 50.0,
);
}
/// 몬스터 기본 스탯 (monster base stats)
class MonsterBaseStats {
const MonsterBaseStats({
required this.hp,
required this.atk,
required this.def,
required this.exp,
required this.gold,
});
final int hp;
final int atk;
final int def;
final int exp;
final int gold;
/// 레벨 기반 기본 스탯 생성
///
/// HP: 50 + level * 20 + (level^2 / 5)
/// ATK: 레벨별 차등 적용
/// - 레벨 1~5: 2 + level * 3 (초반 난이도 완화)
/// - 레벨 6+: 3 + level * 4 (기존 공식)
/// - DEF 감산율 0.5와 연동하여 밸런스 조정
/// - 플레이어가 5~10회 공격 생존 가능하도록 설계
/// DEF: 2 + level * 2
/// EXP: 10 + level * 5
/// GOLD: 5 + level * 3
factory MonsterBaseStats.forLevel(int level) {
// 레벨 1~5: 초반 난이도 완화 (2 + level * 3)
// 레벨 1=5, 2=8, 3=11, 4=14, 5=17
// 레벨 6+: 기존 공식 (3 + level * 4)
// 레벨 6=27, 10=43, 20=83, 50=203, 100=403
final atk = level <= 5 ? 2 + level * 3 : 3 + level * 4;
return MonsterBaseStats(
hp: 50 + level * 20 + (level * level ~/ 5),
atk: atk,
def: 2 + level * 2,
exp: 10 + level * 5,
gold: 5 + level * 3,
);
}
/// 타입 배율 적용
MonsterBaseStats applyType(MonsterType type) {
final mult = MonsterTypeMultiplier.forType(type);
return MonsterBaseStats(
hp: (hp * mult.hp).round(),
atk: (atk * mult.atk).round(),
def: (def * mult.def).round(),
exp: (exp * mult.exp).round(),
gold: (gold * mult.gold).round(),
);
}
/// 레벨과 타입을 기반으로 몬스터 스탯 생성
static MonsterBaseStats generate(int level, MonsterType type) {
return MonsterBaseStats.forLevel(level).applyType(type);
}
}
/// 보스 특수 능력 타입 (boss ability type)
enum BossAbilityType {
/// 연속 공격 (Syntax Error Dragon)
multiAttack,
/// HP 회복 (Memory Leak Hydra)
regeneration,
/// 보호막 (Buffer Overflow Titan)
shield,
/// 스턴 공격 (Kernel Panic Archon)
stunAttack,
/// 페이즈 전환 (Glitch God)
phaseShift,
}
/// 보스 스탯 (boss stats)
class BossStats extends MonsterBaseStats {
const BossStats({
required super.hp,
required super.atk,
required super.def,
required super.exp,
required super.gold,
required this.phases,
required this.enrageThreshold,
required this.enrageMultiplier,
required this.hasShield,
required this.shieldAmount,
required this.abilities,
});
/// 페이즈 수 (글리치 신은 5페이즈)
final int phases;
/// 분노 발동 HP % (0.3 = 30%)
final double enrageThreshold;
/// 분노 시 스탯 배율
final double enrageMultiplier;
/// 보호막 보유 여부
final bool hasShield;
/// 보호막 수치
final int shieldAmount;
/// 특수 능력 목록
final List<BossAbilityType> abilities;
/// Syntax Error Dragon (Act I 보스, 레벨 20)
static BossStats syntaxErrorDragon(int baseLevel) {
final base = MonsterBaseStats.generate(baseLevel, MonsterType.boss);
return BossStats(
hp: base.hp,
atk: base.atk,
def: base.def,
exp: base.exp,
gold: base.gold,
phases: 3,
enrageThreshold: 0.2,
enrageMultiplier: 1.5,
hasShield: false,
shieldAmount: 0,
abilities: [BossAbilityType.multiAttack],
);
}
/// Memory Leak Hydra (Act II 보스, 레벨 40)
static BossStats memoryLeakHydra(int baseLevel) {
final base = MonsterBaseStats.generate(baseLevel, MonsterType.boss);
return BossStats(
hp: base.hp,
atk: base.atk,
def: base.def,
exp: base.exp,
gold: base.gold,
phases: 2,
enrageThreshold: 0.3,
enrageMultiplier: 1.3,
hasShield: false,
shieldAmount: 0,
abilities: [BossAbilityType.regeneration],
);
}
/// Buffer Overflow Titan (Act III 보스, 레벨 60)
static BossStats bufferOverflowTitan(int baseLevel) {
final base = MonsterBaseStats.generate(baseLevel, MonsterType.boss);
return BossStats(
hp: base.hp,
atk: base.atk,
def: base.def,
exp: base.exp,
gold: base.gold,
phases: 2,
enrageThreshold: 0.25,
enrageMultiplier: 1.4,
hasShield: true,
shieldAmount: (base.hp * 0.3).round(),
abilities: [BossAbilityType.shield],
);
}
/// Kernel Panic Archon (Act IV 보스, 레벨 80)
///
/// Phase 6 밸런스 조정: enrageMultiplier 1.6 → 1.5
static BossStats kernelPanicArchon(int baseLevel) {
final base = MonsterBaseStats.generate(baseLevel, MonsterType.boss);
return BossStats(
hp: base.hp,
atk: base.atk,
def: base.def,
exp: base.exp,
gold: base.gold,
phases: 3,
enrageThreshold: 0.2,
enrageMultiplier: 1.5, // 1.6 → 1.5 (분노 시 50% 스탯 증가)
hasShield: true,
shieldAmount: (base.hp * 0.2).round(),
abilities: [BossAbilityType.stunAttack],
);
}
/// Glitch God (최종 보스, 레벨 100)
///
/// Phase 6 밸런스 조정:
/// - enrageThreshold: 0.1 → 0.15 (분노 발동 시점 완화)
/// - enrageMultiplier: 2.0 → 1.7 (분노 시 스탯 증가 완화)
/// - shieldAmount: 50% → 35% (보호막 감소)
static BossStats glitchGod(int baseLevel) {
final base = MonsterBaseStats.generate(baseLevel, MonsterType.finalBoss);
return BossStats(
hp: base.hp,
atk: base.atk,
def: base.def,
exp: base.exp,
gold: base.gold,
phases: 5,
enrageThreshold: 0.15, // 0.1 → 0.15 (15% HP에서 분노)
enrageMultiplier: 1.7, // 2.0 → 1.7 (분노 시 70% 스탯 증가)
hasShield: true,
shieldAmount: (base.hp * 0.35).round(), // 0.5 → 0.35 (보호막 30% 감소)
abilities: [
BossAbilityType.phaseShift,
BossAbilityType.multiAttack,
BossAbilityType.regeneration,
BossAbilityType.stunAttack,
],
);
}
}
/// 레벨 구간별 설정 (level tier settings)
class LevelTierSettings {
const LevelTierSettings({
required this.minLevel,
required this.maxLevel,
required this.name,
required this.targetDeathRate,
required this.estimatedPlayTimeMinutes,
});
final int minLevel;
final int maxLevel;
final String name;
/// 목표 사망 확률 (전투당, 0.01 = 1%)
final double targetDeathRate;
/// 예상 플레이 시간 (분)
final int estimatedPlayTimeMinutes;
/// 초반 (레벨 1-20): 튜토리얼
static const early = LevelTierSettings(
minLevel: 1,
maxLevel: 20,
name: '초반',
targetDeathRate: 0.02, // 1-3%
estimatedPlayTimeMinutes: 90, // 1-2시간
);
/// 중반 (레벨 21-50): 본격적인 성장
static const mid = LevelTierSettings(
minLevel: 21,
maxLevel: 50,
name: '중반',
targetDeathRate: 0.04, // 3-5%
estimatedPlayTimeMinutes: 240, // 3-5시간
);
/// 후반 (레벨 51-80): 고급 장비
static const late = LevelTierSettings(
minLevel: 51,
maxLevel: 80,
name: '후반',
targetDeathRate: 0.075, // 5-10%
estimatedPlayTimeMinutes: 390, // 5-8시간
);
/// 엔드게임 (레벨 81-100): 최종 보스
static const endgame = LevelTierSettings(
minLevel: 81,
maxLevel: 100,
name: '엔드게임',
targetDeathRate: 0.15, // 10-20%
estimatedPlayTimeMinutes: 240, // 3-5시간
);
/// 모든 레벨 구간
static const all = [early, mid, late, endgame];
/// 레벨로 구간 찾기
static LevelTierSettings forLevel(int level) {
for (final tier in all) {
if (level >= tier.minLevel && level <= tier.maxLevel) {
return tier;
}
}
return endgame;
}
}
/// Act별 최소 몬스터 레벨 (act minimum monster level)
///
/// 플레이어 레벨과 무관하게 각 Act에서 등장하는 몬스터의 최소 레벨.
/// 저레벨 플레이어가 고Act에 도달해도 적절한 난이도 유지.
class ActMonsterLevel {
ActMonsterLevel._();
/// plotStageCount 기준 최소 몬스터 레벨
/// - 1: Prologue → 1
/// - 2: Act I → 3
/// - 3: Act II → 22
/// - 4: Act III → 45
/// - 5: Act IV → 72
/// - 6: Act V → 88
static const List<int> _minimumLevels = [
1, // index 0: unused (plotStageCount starts at 1)
1, // Prologue (plotStageCount = 1)
3, // Act I (plotStageCount = 2)
22, // Act II (plotStageCount = 3)
45, // Act III (plotStageCount = 4)
72, // Act IV (plotStageCount = 5)
88, // Act V (plotStageCount = 6)
];
/// plotStageCount에 해당하는 최소 몬스터 레벨 반환
static int forPlotStage(int plotStageCount) {
if (plotStageCount < 1) return 1;
if (plotStageCount >= _minimumLevels.length) {
return _minimumLevels.last;
}
return _minimumLevels[plotStageCount];
}
}
/// 플레이어 스탯 스케일링 (player stat scaling)
class PlayerScaling {
PlayerScaling._();
/// 레벨당 HP 증가량 (12 → 18 상향, 생존율 개선)
static const int hpPerLevel = 18;
/// 레벨당 MP 증가량 (5 → 6 상향)
static const int mpPerLevel = 6;
/// CON당 HP 보너스 (6 → 10 상향, 체력 투자 효율 개선)
static const int hpPerCon = 10;
/// INT당 MP 보너스 (3 → 4 상향)
static const int mpPerInt = 4;
/// 레벨업 시 HP/MP 계산
static ({int hpMax, int mpMax}) calculateResources({
required int level,
required int baseHp,
required int baseMp,
required int conBonus,
required int intBonus,
}) {
final hpMax = baseHp + (level - 1) * hpPerLevel + conBonus * hpPerCon;
final mpMax = baseMp + (level - 1) * mpPerLevel + intBonus * mpPerInt;
return (hpMax: hpMax, mpMax: mpMax);
}
/// 레벨 구간별 ATK 보너스 (후반 DPS 보조)
/// - 레벨 60+: +1 ATK per level
/// - 레벨 80+: +2 ATK per level
static int bonusAtk(int level) {
if (level >= 80) return (level - 80) * 2 + 20;
if (level >= 60) return level - 60;
return 0;
}
}