feat(phase6): 밸런싱 상수 및 몬스터 스탯 스케일링 시스템 구현
- balance_constants.dart 생성 - ExpConstants: 경험치 곡선 (baseExp * 1.15^level) - MonsterType: normal, elite, miniboss, boss, finalBoss - MonsterTypeMultiplier: 타입별 HP/ATK/DEF/EXP/GOLD 배율 - MonsterBaseStats: 레벨 기반 몬스터 스탯 생성 - BossStats: 5개 보스 정의 (Syntax Error Dragon ~ Glitch God) - LevelTierSettings: 구간별 난이도 설정 (초반/중반/후반/엔드게임) - PlayerScaling: 플레이어 레벨업 시 HP/MP 스케일링 - MonsterCombatStats 통합 - MonsterBaseStats.generate() 사용하여 스탯 생성 - monsterType 매개변수 추가로 타입별 몬스터 지원
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:askiineverdie/src/core/util/balance_constants.dart';
|
||||||
|
|
||||||
/// 몬스터 공격 속도 타입
|
/// 몬스터 공격 속도 타입
|
||||||
enum MonsterSpeedType {
|
enum MonsterSpeedType {
|
||||||
/// 빠름 (600ms)
|
/// 빠름 (600ms)
|
||||||
@@ -127,22 +129,15 @@ class MonsterCombatStats {
|
|||||||
/// [name] 몬스터 표시 이름
|
/// [name] 몬스터 표시 이름
|
||||||
/// [level] 몬스터 레벨 (원본 데이터 기준)
|
/// [level] 몬스터 레벨 (원본 데이터 기준)
|
||||||
/// [speedType] 공격 속도 타입 (기본: normal)
|
/// [speedType] 공격 속도 타입 (기본: normal)
|
||||||
|
/// [monsterType] 몬스터 타입 (기본: normal)
|
||||||
factory MonsterCombatStats.fromLevel({
|
factory MonsterCombatStats.fromLevel({
|
||||||
required String name,
|
required String name,
|
||||||
required int level,
|
required int level,
|
||||||
MonsterSpeedType speedType = MonsterSpeedType.normal,
|
MonsterSpeedType speedType = MonsterSpeedType.normal,
|
||||||
|
MonsterType monsterType = MonsterType.normal,
|
||||||
}) {
|
}) {
|
||||||
// 레벨 기반 스탯 스케일링
|
// balance_constants.dart의 MonsterBaseStats 사용
|
||||||
// 레벨 1 기준으로 선형/비선형 증가
|
final baseStats = MonsterBaseStats.generate(level, monsterType);
|
||||||
|
|
||||||
// HP: 레벨 * 15 + 기본값 20
|
|
||||||
final hpMax = 20 + level * 15;
|
|
||||||
|
|
||||||
// 공격력: 레벨 * 3 + 기본값 5
|
|
||||||
final atk = 5 + level * 3;
|
|
||||||
|
|
||||||
// 방어력: 레벨 * 1.5 + 기본값 2
|
|
||||||
final def = 2 + (level * 1.5).round();
|
|
||||||
|
|
||||||
// 크리티컬 확률: 레벨에 따라 천천히 증가 (0.02 ~ 0.3)
|
// 크리티컬 확률: 레벨에 따라 천천히 증가 (0.02 ~ 0.3)
|
||||||
final criRate = (0.02 + level * 0.003).clamp(0.02, 0.3);
|
final criRate = (0.02 + level * 0.003).clamp(0.02, 0.3);
|
||||||
@@ -163,22 +158,19 @@ class MonsterCombatStats {
|
|||||||
MonsterSpeedType.slow => 1400,
|
MonsterSpeedType.slow => 1400,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 경험치 보상: 레벨 기반
|
|
||||||
final expReward = 10 + level * 5;
|
|
||||||
|
|
||||||
return MonsterCombatStats(
|
return MonsterCombatStats(
|
||||||
name: name,
|
name: name,
|
||||||
level: level,
|
level: level,
|
||||||
atk: atk,
|
atk: baseStats.atk,
|
||||||
def: def,
|
def: baseStats.def,
|
||||||
hpMax: hpMax,
|
hpMax: baseStats.hp,
|
||||||
hpCurrent: hpMax,
|
hpCurrent: baseStats.hp,
|
||||||
criRate: criRate,
|
criRate: criRate,
|
||||||
criDamage: criDamage,
|
criDamage: criDamage,
|
||||||
evasion: evasion,
|
evasion: evasion,
|
||||||
accuracy: accuracy,
|
accuracy: accuracy,
|
||||||
attackDelayMs: attackDelayMs,
|
attackDelayMs: attackDelayMs,
|
||||||
expReward: expReward,
|
expReward: baseStats.exp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
419
lib/src/core/util/balance_constants.dart
Normal file
419
lib/src/core/util/balance_constants.dart
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
/// 밸런스 상수 정의 (balance constants)
|
||||||
|
///
|
||||||
|
/// Phase 6: 레벨 1-100 구간의 난이도 곡선 및 스케일링 상수
|
||||||
|
library;
|
||||||
|
|
||||||
|
/// 경험치 관련 상수 (experience constants)
|
||||||
|
class ExpConstants {
|
||||||
|
ExpConstants._();
|
||||||
|
|
||||||
|
/// 기본 경험치 값
|
||||||
|
static const int baseExp = 100;
|
||||||
|
|
||||||
|
/// 레벨당 경험치 증가율 (1.15 = 15% 증가)
|
||||||
|
static const double expGrowthRate = 1.15;
|
||||||
|
|
||||||
|
/// 레벨업에 필요한 경험치 계산
|
||||||
|
///
|
||||||
|
/// 공식: baseExp * (expGrowthRate ^ level)
|
||||||
|
/// 레벨 10: ~405 exp
|
||||||
|
/// 레벨 50: ~108,366 exp
|
||||||
|
/// 레벨 100: ~11,739,085 exp
|
||||||
|
static int requiredExp(int level) {
|
||||||
|
if (level <= 0) return baseExp;
|
||||||
|
return (baseExp * _pow(expGrowthRate, level)).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 효율적인 거듭제곱 계산
|
||||||
|
static double _pow(double base, int exponent) {
|
||||||
|
double result = 1.0;
|
||||||
|
for (int i = 0; i < exponent; i++) {
|
||||||
|
result *= base;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 타입 (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배, 보상 2배
|
||||||
|
static const elite = MonsterTypeMultiplier(
|
||||||
|
hp: 2.0,
|
||||||
|
atk: 1.3,
|
||||||
|
def: 1.2,
|
||||||
|
exp: 2.0,
|
||||||
|
gold: 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 미니보스: HP 5배, ATK/DEF 1.5배, 보상 5배
|
||||||
|
static const miniboss = MonsterTypeMultiplier(
|
||||||
|
hp: 5.0,
|
||||||
|
atk: 1.5,
|
||||||
|
def: 1.5,
|
||||||
|
exp: 5.0,
|
||||||
|
gold: 5.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 보스: HP 10배, ATK/DEF 2배, EXP 15배, GOLD 10배
|
||||||
|
static const boss = MonsterTypeMultiplier(
|
||||||
|
hp: 10.0,
|
||||||
|
atk: 2.0,
|
||||||
|
def: 2.0,
|
||||||
|
exp: 15.0,
|
||||||
|
gold: 10.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 최종 보스: HP 20배, ATK/DEF 2.5배, EXP 50배, GOLD 30배
|
||||||
|
static const finalBoss = MonsterTypeMultiplier(
|
||||||
|
hp: 20.0,
|
||||||
|
atk: 2.5,
|
||||||
|
def: 2.5,
|
||||||
|
exp: 50.0,
|
||||||
|
gold: 30.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: 5 + level * 3
|
||||||
|
/// DEF: 2 + level * 2
|
||||||
|
/// EXP: 10 + level * 5
|
||||||
|
/// GOLD: 5 + level * 3
|
||||||
|
factory MonsterBaseStats.forLevel(int level) {
|
||||||
|
return MonsterBaseStats(
|
||||||
|
hp: 50 + level * 20 + (level * level ~/ 5),
|
||||||
|
atk: 5 + level * 3,
|
||||||
|
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)
|
||||||
|
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.6,
|
||||||
|
hasShield: true,
|
||||||
|
shieldAmount: (base.hp * 0.2).round(),
|
||||||
|
abilities: [BossAbilityType.stunAttack],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Glitch God (최종 보스, 레벨 100)
|
||||||
|
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.1,
|
||||||
|
enrageMultiplier: 2.0,
|
||||||
|
hasShield: true,
|
||||||
|
shieldAmount: (base.hp * 0.5).round(),
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 플레이어 스탯 스케일링 (player stat scaling)
|
||||||
|
class PlayerScaling {
|
||||||
|
PlayerScaling._();
|
||||||
|
|
||||||
|
/// 레벨당 HP 증가량
|
||||||
|
static const int hpPerLevel = 10;
|
||||||
|
|
||||||
|
/// 레벨당 MP 증가량
|
||||||
|
static const int mpPerLevel = 5;
|
||||||
|
|
||||||
|
/// 레벨업 시 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 * 5;
|
||||||
|
final mpMax = baseMp + (level - 1) * mpPerLevel + intBonus * 3;
|
||||||
|
return (hpMax: hpMax, mpMax: mpMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user