- CombatStats에 toJson/fromJson 직렬화 메서드 추가 - HallOfFameEntry에 finalStats(CombatStats) 필드 추가 - 명예의 전당 상세 다이얼로그에서 전투 스탯, 장비, 스펠 표시 - GameState에 combatStats 접근자 추가 - game_text_l10n에 명예의 전당 관련 텍스트 추가
473 lines
14 KiB
Dart
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,
|
|
);
|
|
}
|