Files
asciinevrdie/lib/src/core/model/combat_stats.dart
JiWoong Sul df5fdbaac2 feat(hall-of-fame): 명예의 전당 상세 UI 및 전투 스탯 저장 추가
- CombatStats에 toJson/fromJson 직렬화 메서드 추가
- HallOfFameEntry에 finalStats(CombatStats) 필드 추가
- 명예의 전당 상세 다이얼로그에서 전투 스탯, 장비, 스펠 표시
- GameState에 combatStats 접근자 추가
- game_text_l10n에 명예의 전당 관련 텍스트 추가
2025-12-24 17:20:52 +09:00

473 lines
14 KiB
Dart

import 'package:askiineverdie/src/core/model/class_traits.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/model/race_traits.dart';
/// 전투용 파생 스탯
///
/// 기본 Stats와 Equipment를 기반으로 계산되는 전투 관련 수치.
/// 불변(immutable) 객체로 설계되어 상태 변경 시 새 인스턴스 생성.
class CombatStats {
const CombatStats({
// 기본 스탯 (Stats에서 복사)
required this.str,
required this.con,
required this.dex,
required this.intelligence,
required this.wis,
required this.cha,
// 파생 스탯
required this.atk,
required this.def,
required this.magAtk,
required this.magDef,
required this.criRate,
required this.criDamage,
required this.evasion,
required this.accuracy,
required this.blockRate,
required this.parryRate,
required this.attackDelayMs,
// 자원
required this.hpMax,
required this.hpCurrent,
required this.mpMax,
required this.mpCurrent,
});
// ============================================================================
// 기본 스탯
// ============================================================================
/// 힘: 물리 공격력 보정
final int str;
/// 체력: HP, 방어력 보정
final int con;
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
final int dex;
/// 지능: 마법 공격력, MP
final int intelligence;
/// 지혜: 마법 방어력, MP 회복
final int wis;
/// 매력: 상점 가격, 드롭률 보정
final int cha;
// ============================================================================
// 파생 스탯 (전투용)
// ============================================================================
/// 물리 공격력
final int atk;
/// 물리 방어력
final int def;
/// 마법 공격력
final int magAtk;
/// 마법 방어력
final int magDef;
/// 크리티컬 확률 (0.0 ~ 1.0)
final double criRate;
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
final double criDamage;
/// 회피율 (0.0 ~ 0.5)
final double evasion;
/// 명중률 (0.8 ~ 1.0)
final double accuracy;
/// 방패 방어율 (0.0 ~ 0.4)
final double blockRate;
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
final double parryRate;
/// 공격 딜레이 (밀리초)
final int attackDelayMs;
// ============================================================================
// 자원
// ============================================================================
/// 최대 HP
final int hpMax;
/// 현재 HP
final int hpCurrent;
/// 최대 MP
final int mpMax;
/// 현재 MP
final int mpCurrent;
// ============================================================================
// 유틸리티
// ============================================================================
/// HP 비율 (0.0 ~ 1.0)
double get hpRatio => hpMax > 0 ? hpCurrent / hpMax : 0.0;
/// MP 비율 (0.0 ~ 1.0)
double get mpRatio => mpMax > 0 ? mpCurrent / mpMax : 0.0;
/// 생존 여부
bool get isAlive => hpCurrent > 0;
/// 사망 여부
bool get isDead => hpCurrent <= 0;
/// HP 변경된 새 인스턴스 반환
CombatStats withHp(int newHp) {
return copyWith(hpCurrent: newHp.clamp(0, hpMax));
}
/// MP 변경된 새 인스턴스 반환
CombatStats withMp(int newMp) {
return copyWith(mpCurrent: newMp.clamp(0, mpMax));
}
/// 데미지 적용된 새 인스턴스 반환
CombatStats applyDamage(int damage) {
return withHp(hpCurrent - damage);
}
/// 힐 적용된 새 인스턴스 반환
CombatStats applyHeal(int amount) {
return withHp(hpCurrent + amount);
}
CombatStats copyWith({
int? str,
int? con,
int? dex,
int? intelligence,
int? wis,
int? cha,
int? atk,
int? def,
int? magAtk,
int? magDef,
double? criRate,
double? criDamage,
double? evasion,
double? accuracy,
double? blockRate,
double? parryRate,
int? attackDelayMs,
int? hpMax,
int? hpCurrent,
int? mpMax,
int? mpCurrent,
}) {
return CombatStats(
str: str ?? this.str,
con: con ?? this.con,
dex: dex ?? this.dex,
intelligence: intelligence ?? this.intelligence,
wis: wis ?? this.wis,
cha: cha ?? this.cha,
atk: atk ?? this.atk,
def: def ?? this.def,
magAtk: magAtk ?? this.magAtk,
magDef: magDef ?? this.magDef,
criRate: criRate ?? this.criRate,
criDamage: criDamage ?? this.criDamage,
evasion: evasion ?? this.evasion,
accuracy: accuracy ?? this.accuracy,
blockRate: blockRate ?? this.blockRate,
parryRate: parryRate ?? this.parryRate,
attackDelayMs: attackDelayMs ?? this.attackDelayMs,
hpMax: hpMax ?? this.hpMax,
hpCurrent: hpCurrent ?? this.hpCurrent,
mpMax: mpMax ?? this.mpMax,
mpCurrent: mpCurrent ?? this.mpCurrent,
);
}
// ============================================================================
// 팩토리 메서드
// ============================================================================
/// Stats와 Equipment에서 CombatStats 생성
///
/// [stats] 캐릭터 기본 스탯
/// [equipment] 장착 장비 (장비 스탯 적용)
/// [level] 캐릭터 레벨 (스케일링용)
/// [race] 종족 특성 (선택사항, Phase 5)
/// [klass] 클래스 특성 (선택사항, Phase 5)
factory CombatStats.fromStats({
required Stats stats,
required Equipment equipment,
required int level,
RaceTraits? race,
ClassTraits? klass,
}) {
// 장비 총 스탯 가져오기
final equipStats = equipment.totalStats;
// 종족/클래스 스탯 보정 적용
final raceStr = race?.getModifier(StatType.str) ?? 0;
final raceCon = race?.getModifier(StatType.con) ?? 0;
final raceDex = race?.getModifier(StatType.dex) ?? 0;
final raceInt = race?.getModifier(StatType.intelligence) ?? 0;
final raceWis = race?.getModifier(StatType.wis) ?? 0;
final raceCha = race?.getModifier(StatType.cha) ?? 0;
final classStr = klass?.getModifier(StatType.str) ?? 0;
final classCon = klass?.getModifier(StatType.con) ?? 0;
final classDex = klass?.getModifier(StatType.dex) ?? 0;
final classInt = klass?.getModifier(StatType.intelligence) ?? 0;
final classWis = klass?.getModifier(StatType.wis) ?? 0;
final classCha = klass?.getModifier(StatType.cha) ?? 0;
// 장비 보너스 + 종족/클래스 보정이 적용된 기본 스탯
final effectiveStr = stats.str + equipStats.strBonus + raceStr + classStr;
final effectiveCon = stats.con + equipStats.conBonus + raceCon + classCon;
final effectiveDex = stats.dex + equipStats.dexBonus + raceDex + classDex;
final effectiveInt =
stats.intelligence + equipStats.intBonus + raceInt + classInt;
final effectiveWis = stats.wis + equipStats.wisBonus + raceWis + classWis;
final effectiveCha = stats.cha + equipStats.chaBonus + raceCha + classCha;
// 기본 공격력: STR 기반 + 레벨 보정 + 장비 ATK
var baseAtk = effectiveStr * 2 + level + equipStats.atk;
// 기본 방어력: CON 기반 + 레벨 보정 + 장비 DEF
var baseDef = effectiveCon + (level ~/ 2) + equipStats.def;
// 마법 공격력: INT 기반 + 장비 MAG_ATK
var baseMagAtk = effectiveInt * 2 + level + equipStats.magAtk;
// 마법 방어력: WIS 기반 + 장비 MAG_DEF
final baseMagDef = effectiveWis + (level ~/ 2) + equipStats.magDef;
// 크리티컬 확률: DEX 기반 + 장비 보너스 (0.05 ~ 0.8)
var criRate = 0.05 + effectiveDex * 0.005 + equipStats.criRate;
// 크리티컬 데미지: 기본 1.5배, DEX에 따라 증가 (최대 3.0)
final criDamage = (1.5 + effectiveDex * 0.01).clamp(1.5, 3.0);
// 회피율: DEX 기반 + 장비 보너스 (0.0 ~ 0.6)
var evasion = effectiveDex * 0.005 + equipStats.evasion;
// 명중률: DEX 기반 (0.8 ~ 1.0)
final accuracy = (0.8 + effectiveDex * 0.002).clamp(0.8, 1.0);
// 방패 방어율: 방패 장착 시 기본 + CON 보정 + 장비 보너스 (0.0 ~ 0.5)
final hasShield = equipment.shield.isNotEmpty;
final baseBlockRate = hasShield ? (0.1 + effectiveCon * 0.003) : 0.0;
final blockRate = (baseBlockRate + equipStats.blockRate).clamp(0.0, 0.5);
// 무기 쳐내기: DEX + STR 기반 + 장비 보너스 (0.0 ~ 0.4)
final baseParryRate = (effectiveDex + effectiveStr) * 0.002;
final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4);
// 공격 속도: 무기 기본 공속 + DEX 보정
// 무기 attackSpeed가 0이면 기본값 1000ms 사용
final weaponItem = equipment.items[0]; // 무기 슬롯
final weaponSpeed = weaponItem.stats.attackSpeed;
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
final speedModifier = 1.0 + (effectiveDex - 10) * 0.02;
final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(
300,
2000,
);
// HP/MP: 기본 + 장비 보너스
var totalHpMax = stats.hpMax + equipStats.hpBonus;
var totalMpMax = stats.mpMax + equipStats.mpBonus;
// ========================================================================
// 종족 패시브 적용 (Phase 5)
// ========================================================================
// HP 보너스 (Heap Troll: +20%)
final raceHpBonus = race?.getPassiveValue(PassiveType.hpBonus) ?? 0.0;
if (raceHpBonus > 0) {
totalHpMax = (totalHpMax * (1 + raceHpBonus)).round();
}
// MP 보너스 (Pointer Fairy: +20%)
final raceMpBonus = race?.getPassiveValue(PassiveType.mpBonus) ?? 0.0;
if (raceMpBonus > 0) {
totalMpMax = (totalMpMax * (1 + raceMpBonus)).round();
}
// 마법 데미지 보너스 (Null Elf: +15%)
final raceMagicBonus =
race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
if (raceMagicBonus > 0) {
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
}
// 방어력 보너스 (Buffer Dwarf: +10%)
final raceDefBonus = race?.getPassiveValue(PassiveType.defenseBonus) ?? 0.0;
if (raceDefBonus > 0) {
baseDef = (baseDef * (1 + raceDefBonus)).round();
}
// 크리티컬 보너스 (Stack Goblin: +5%)
final raceCritBonus =
race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
criRate += raceCritBonus;
// ========================================================================
// 클래스 패시브 적용 (Phase 5)
// ========================================================================
// HP 보너스 (Garbage Collector: +30%)
final classHpBonus =
klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
if (classHpBonus > 0) {
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
}
// 물리 공격력 보너스 (Bug Hunter: +20%)
final classPhysBonus =
klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
if (classPhysBonus > 0) {
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
}
// 방어력 보너스 (Debugger Paladin: +15%)
final classDefBonus =
klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
if (classDefBonus > 0) {
baseDef = (baseDef * (1 + classDefBonus)).round();
}
// 마법 데미지 보너스 (Compiler Mage: +25%)
final classMagBonus =
klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
if (classMagBonus > 0) {
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
}
// 회피율 보너스 (Refactor Monk: +15%)
final classEvasionBonus =
klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
evasion += classEvasionBonus;
// 크리티컬 보너스 (Pointer Assassin: +20%)
final classCritBonus =
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
criRate += classCritBonus;
// 최종 클램핑
criRate = criRate.clamp(0.05, 0.8);
evasion = evasion.clamp(0.0, 0.6);
return CombatStats(
str: effectiveStr,
con: effectiveCon,
dex: effectiveDex,
intelligence: effectiveInt,
wis: effectiveWis,
cha: effectiveCha,
atk: baseAtk,
def: baseDef,
magAtk: baseMagAtk,
magDef: baseMagDef,
criRate: criRate,
criDamage: criDamage,
evasion: evasion,
accuracy: accuracy,
blockRate: blockRate,
parryRate: parryRate,
attackDelayMs: attackDelayMs,
hpMax: totalHpMax,
hpCurrent: stats.hp.clamp(0, totalHpMax),
mpMax: totalMpMax,
mpCurrent: stats.mp.clamp(0, totalMpMax),
);
}
/// JSON으로 직렬화
Map<String, dynamic> toJson() {
return {
'str': str,
'con': con,
'dex': dex,
'intelligence': intelligence,
'wis': wis,
'cha': cha,
'atk': atk,
'def': def,
'magAtk': magAtk,
'magDef': magDef,
'criRate': criRate,
'criDamage': criDamage,
'evasion': evasion,
'accuracy': accuracy,
'blockRate': blockRate,
'parryRate': parryRate,
'attackDelayMs': attackDelayMs,
'hpMax': hpMax,
'hpCurrent': hpCurrent,
'mpMax': mpMax,
'mpCurrent': mpCurrent,
};
}
/// JSON에서 역직렬화
factory CombatStats.fromJson(Map<String, dynamic> json) {
return CombatStats(
str: json['str'] as int,
con: json['con'] as int,
dex: json['dex'] as int,
intelligence: json['intelligence'] as int,
wis: json['wis'] as int,
cha: json['cha'] as int,
atk: json['atk'] as int,
def: json['def'] as int,
magAtk: json['magAtk'] as int,
magDef: json['magDef'] as int,
criRate: (json['criRate'] as num).toDouble(),
criDamage: (json['criDamage'] as num).toDouble(),
evasion: (json['evasion'] as num).toDouble(),
accuracy: (json['accuracy'] as num).toDouble(),
blockRate: (json['blockRate'] as num).toDouble(),
parryRate: (json['parryRate'] as num).toDouble(),
attackDelayMs: json['attackDelayMs'] as int,
hpMax: json['hpMax'] as int,
hpCurrent: json['hpCurrent'] as int,
mpMax: json['mpMax'] as int,
mpCurrent: json['mpCurrent'] as int,
);
}
/// 테스트/디버그용 기본값
factory CombatStats.empty() => const CombatStats(
str: 10,
con: 10,
dex: 10,
intelligence: 10,
wis: 10,
cha: 10,
atk: 20,
def: 10,
magAtk: 20,
magDef: 10,
criRate: 0.05,
criDamage: 1.5,
evasion: 0.05,
accuracy: 0.85,
blockRate: 0.0,
parryRate: 0.0,
attackDelayMs: 1000,
hpMax: 100,
hpCurrent: 100,
mpMax: 50,
mpCurrent: 50,
);
}