feat(combat): 레벨 페널티 및 확률 캡 상수 추가

- 레벨 차이에 따른 확률 감소 배율 함수 추가
- 확률 캡 상수 정의 (크리티컬 50%, 회피 40%, 방어 50%, 패리 35%)
- Phase 12 밸런스 조정
This commit is contained in:
JiWoong Sul
2026-01-05 19:41:52 +09:00
parent 33b7cd3b16
commit f13783a35b

View File

@@ -7,6 +7,51 @@ import 'package:asciineverdie/src/core/model/race_traits.dart';
/// 기본 Stats와 Equipment를 기반으로 계산되는 전투 관련 수치. /// 기본 Stats와 Equipment를 기반으로 계산되는 전투 관련 수치.
/// 불변(immutable) 객체로 설계되어 상태 변경 시 새 인스턴스 생성. /// 불변(immutable) 객체로 설계되어 상태 변경 시 새 인스턴스 생성.
class CombatStats { class CombatStats {
// ============================================================================
// 레벨 페널티 상수 (Phase 12)
// ============================================================================
/// 1레벨당 확률 감소율 (8%)
static const double _levelPenaltyPerLevel = 0.08;
/// 최저 페널티 배율 (20%)
static const double _minLevelMultiplier = 0.2;
// ============================================================================
// 확률 캡 상수 (Phase 12)
// ============================================================================
/// 크리티컬 확률 최대 (50%)
static const double _maxCriRate = 0.5;
/// 회피율 최대 (40%)
static const double _maxEvasion = 0.4;
/// 방패 방어율 최대 (50%)
static const double _maxBlockRate = 0.5;
/// 무기 쳐내기 최대 (35%)
static const double _maxParryRate = 0.35;
// ============================================================================
// 레벨 페널티 함수 (Phase 12)
// ============================================================================
/// 레벨 차이에 따른 확률 감소 배율 (플레이어 전용)
///
/// - levelDiff = monsterLevel - playerLevel (몬스터가 높으면 양수)
/// - 0레벨 차이: 1.0 (100% 유지)
/// - 10레벨 이상 차이: 0.2 (20% = 최저)
/// - 상승 없음 (플레이어가 높아도 보너스 없음)
static double _getLevelPenalty(int playerLevel, int monsterLevel) {
final levelDiff = monsterLevel - playerLevel;
if (levelDiff <= 0) return 1.0; // 플레이어가 높거나 같으면 페널티 없음
// 1레벨당 8%씩 감소 (100% → 92% → 84% → ... → 20%)
final penalty = 1.0 - (levelDiff * _levelPenaltyPerLevel);
return penalty.clamp(_minLevelMultiplier, 1.0);
}
const CombatStats({ const CombatStats({
// 기본 스탯 (Stats에서 복사) // 기본 스탯 (Stats에서 복사)
required this.str, required this.str,
@@ -204,13 +249,17 @@ class CombatStats {
/// [level] 캐릭터 레벨 (스케일링용) /// [level] 캐릭터 레벨 (스케일링용)
/// [race] 종족 특성 (선택사항, Phase 5) /// [race] 종족 특성 (선택사항, Phase 5)
/// [klass] 클래스 특성 (선택사항, Phase 5) /// [klass] 클래스 특성 (선택사항, Phase 5)
/// [monsterLevel] 상대 몬스터 레벨 (레벨 페널티 계산용, Phase 12)
factory CombatStats.fromStats({ factory CombatStats.fromStats({
required Stats stats, required Stats stats,
required Equipment equipment, required Equipment equipment,
required int level, required int level,
RaceTraits? race, RaceTraits? race,
ClassTraits? klass, ClassTraits? klass,
int? monsterLevel,
}) { }) {
// 레벨 페널티 계산 (Phase 12)
final levelPenalty = _getLevelPenalty(level, monsterLevel ?? level);
// 장비 총 스탯 가져오기 // 장비 총 스탯 가져오기
final equipStats = equipment.totalStats; final equipStats = equipment.totalStats;
@@ -362,9 +411,21 @@ class CombatStats {
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0; klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
criRate += classCritBonus; criRate += classCritBonus;
// 최종 클램핑 // ========================================================================
criRate = criRate.clamp(0.05, 0.8); // 레벨 페널티 및 최종 클램핑 (Phase 12)
evasion = evasion.clamp(0.0, 0.6); // ========================================================================
// 레벨 페널티 적용 (크리/회피/블록/패리)
criRate *= levelPenalty;
evasion *= levelPenalty;
var finalBlockRate = blockRate * levelPenalty;
var finalParryRate = parryRate * levelPenalty;
// 최종 클램핑 (새 캡 적용)
criRate = criRate.clamp(0.05, _maxCriRate);
evasion = evasion.clamp(0.0, _maxEvasion);
finalBlockRate = finalBlockRate.clamp(0.0, _maxBlockRate);
finalParryRate = finalParryRate.clamp(0.0, _maxParryRate);
return CombatStats( return CombatStats(
str: effectiveStr, str: effectiveStr,
@@ -381,8 +442,8 @@ class CombatStats {
criDamage: criDamage, criDamage: criDamage,
evasion: evasion, evasion: evasion,
accuracy: accuracy, accuracy: accuracy,
blockRate: blockRate, blockRate: finalBlockRate,
parryRate: parryRate, parryRate: finalParryRate,
attackDelayMs: attackDelayMs, attackDelayMs: attackDelayMs,
hpMax: totalHpMax, hpMax: totalHpMax,
hpCurrent: stats.hp.clamp(0, totalHpMax), hpCurrent: stats.hp.clamp(0, totalHpMax),