feat(race-class): Phase 5 종족/클래스 특화 시스템 구현

- RaceTraits: 7종족 (Byte Human, Null Elf, Buffer Dwarf 등)
- ClassTraits: 6클래스 (Bug Hunter, Compiler Mage 등)
- StatCalculator: 종족/클래스 보정 계산
- CombatStats.fromStats: 종족/클래스 패시브 효과 통합
- 종족별 스탯 보정 및 패시브 (경험치, HP/MP, 크리티컬 등)
- 클래스별 스탯 보정 및 패시브 (공격력, 방어력, 회피 등)
This commit is contained in:
JiWoong Sul
2025-12-17 17:22:28 +09:00
parent 21bf057cfc
commit d158c11249
6 changed files with 909 additions and 16 deletions

View File

@@ -0,0 +1,150 @@
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
import 'package:askiineverdie/src/core/model/race_traits.dart';
/// 방어구 무게 등급 (armor weight class)
enum ArmorWeight {
/// 경갑 (light armor)
light,
/// 중갑 (heavy armor)
heavy,
/// 전체 가능 (all armor)
all,
}
/// 클래스 패시브 타입 (class passive type)
enum ClassPassiveType {
/// 물리 공격력 배율 보너스
physicalDamageBonus,
/// 방어력 배율 보너스
defenseBonus,
/// 회복력 배율 보너스
healingBonus,
/// 마법 데미지 배율 보너스
magicDamageBonus,
/// 회피율 보너스
evasionBonus,
/// 연속 공격 가능
multiAttack,
/// 크리티컬 확률 보너스
criticalBonus,
/// 첫 공격 배율 보너스
firstStrikeBonus,
/// HP 배율 보너스
hpBonus,
/// 전투 후 HP 회복 비율
postCombatHeal,
}
/// 클래스 패시브 능력 (class passive ability)
class ClassPassive {
const ClassPassive({
required this.type,
required this.value,
this.description = '',
});
/// 패시브 타입
final ClassPassiveType type;
/// 효과 값 (배율의 경우 0.1 = 10%)
final double value;
/// 설명
final String description;
}
/// 장비 제한 사항 (equipment restriction)
class EquipmentRestriction {
const EquipmentRestriction({
this.armorWeight = ArmorWeight.all,
this.allowedWeaponSlots = const {},
this.disallowedSlots = const {},
});
/// 허용 방어구 무게
final ArmorWeight armorWeight;
/// 허용 무기 슬롯 (비어있으면 모두 허용)
final Set<EquipmentSlot> allowedWeaponSlots;
/// 금지된 슬롯
final Set<EquipmentSlot> disallowedSlots;
/// 전체 허용 (제한 없음)
static const none = EquipmentRestriction();
/// 특정 슬롯이 허용되는지 확인
bool isSlotAllowed(EquipmentSlot slot) {
if (disallowedSlots.contains(slot)) return false;
// 무기 슬롯 제한 확인
if (slot == EquipmentSlot.weapon && allowedWeaponSlots.isNotEmpty) {
return allowedWeaponSlots.contains(slot);
}
return true;
}
}
/// 클래스 특성 (class traits)
///
/// 각 클래스가 가진 고유한 스탯 보정, 스킬, 장비 제한
class ClassTraits {
const ClassTraits({
required this.classId,
required this.name,
required this.statModifiers,
this.startingSkills = const [],
this.classSkills = const [],
this.passives = const [],
this.restriction = EquipmentRestriction.none,
});
/// 클래스 식별자
final String classId;
/// 클래스 이름 (표시용)
final String name;
/// 스탯 보정치 맵
final Map<StatType, int> statModifiers;
/// 시작 스킬 ID 목록
final List<String> startingSkills;
/// 클래스 전용 스킬 ID 목록
final List<String> classSkills;
/// 패시브 능력 목록
final List<ClassPassive> passives;
/// 장비 제한
final EquipmentRestriction restriction;
/// 특정 스탯의 보정치 반환
int getModifier(StatType type) => statModifiers[type] ?? 0;
/// 특정 패시브 타입의 값 반환 (없으면 0)
double getPassiveValue(ClassPassiveType type) {
for (final passive in passives) {
if (passive.type == type) return passive.value;
}
return 0.0;
}
/// 특정 패시브 보유 여부
bool hasPassive(ClassPassiveType type) {
return passives.any((p) => p.type == type);
}
}

View File

@@ -1,4 +1,6 @@
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';
/// 전투용 파생 스탯
///
@@ -200,41 +202,61 @@ class 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 effectiveStr = stats.str + equipStats.strBonus;
final effectiveCon = stats.con + equipStats.conBonus;
final effectiveDex = stats.dex + equipStats.dexBonus;
final effectiveInt = stats.intelligence + equipStats.intBonus;
final effectiveWis = stats.wis + equipStats.wisBonus;
// 종족/클래스 스탯 보정 적용
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
final baseAtk = effectiveStr * 2 + level + equipStats.atk;
var baseAtk = effectiveStr * 2 + level + equipStats.atk;
// 기본 방어력: CON 기반 + 레벨 보정 + 장비 DEF
final baseDef = effectiveCon + (level ~/ 2) + equipStats.def;
var baseDef = effectiveCon + (level ~/ 2) + equipStats.def;
// 마법 공격력: INT 기반 + 장비 MAG_ATK
final baseMagAtk = effectiveInt * 2 + level + equipStats.magAtk;
var baseMagAtk = effectiveInt * 2 + level + equipStats.magAtk;
// 마법 방어력: WIS 기반 + 장비 MAG_DEF
final baseMagDef = effectiveWis + (level ~/ 2) + equipStats.magDef;
// 크리티컬 확률: DEX 기반 + 장비 보너스 (0.05 ~ 0.5)
final criRate = (0.05 + effectiveDex * 0.005 + equipStats.criRate).clamp(0.05, 0.5);
// 크리티컬 확률: 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.5)
final evasion = (effectiveDex * 0.005 + equipStats.evasion).clamp(0.0, 0.5);
// 회피율: 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);
@@ -253,8 +275,80 @@ class CombatStats {
final attackDelayMs = (1000 / speedModifier).round().clamp(357, 1500);
// HP/MP: 기본 + 장비 보너스
final totalHpMax = stats.hpMax + equipStats.hpBonus;
final totalMpMax = stats.mpMax + equipStats.mpBonus;
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,
@@ -262,7 +356,7 @@ class CombatStats {
dex: effectiveDex,
intelligence: effectiveInt,
wis: effectiveWis,
cha: stats.cha + equipStats.chaBonus,
cha: effectiveCha,
atk: baseAtk,
def: baseDef,
magAtk: baseMagAtk,

View File

@@ -0,0 +1,97 @@
/// 스탯 타입 열거형 (stat type)
enum StatType {
str,
con,
dex,
intelligence,
wis,
cha,
}
/// 패시브 능력 타입 (passive ability type)
enum PassiveType {
/// 경험치 배율 보너스
expBonus,
/// 마법 데미지 배율 보너스
magicDamageBonus,
/// 방어력 배율 보너스
defenseBonus,
/// 크리티컬 확률 보너스
criticalBonus,
/// HP 배율 보너스
hpBonus,
/// MP 배율 보너스
mpBonus,
/// 사망 시 장비 보존
deathEquipmentPreserve,
}
/// 패시브 능력 (passive ability)
///
/// 종족이나 클래스가 제공하는 수동적 효과
class PassiveAbility {
const PassiveAbility({
required this.type,
required this.value,
this.description = '',
});
/// 패시브 타입
final PassiveType type;
/// 효과 값 (배율의 경우 0.1 = 10%)
final double value;
/// 설명
final String description;
}
/// 종족 특성 (race traits)
///
/// 각 종족이 가진 고유한 스탯 보정과 패시브 능력
class RaceTraits {
const RaceTraits({
required this.raceId,
required this.name,
required this.statModifiers,
this.passives = const [],
this.expMultiplier = 1.0,
});
/// 종족 식별자
final String raceId;
/// 종족 이름 (표시용)
final String name;
/// 스탯 보정치 맵
final Map<StatType, int> statModifiers;
/// 패시브 능력 목록
final List<PassiveAbility> passives;
/// 경험치 배율 (1.0 = 100%)
final double expMultiplier;
/// 특정 스탯의 보정치 반환
int getModifier(StatType type) => statModifiers[type] ?? 0;
/// 특정 패시브 타입의 값 반환 (없으면 0)
double getPassiveValue(PassiveType type) {
for (final passive in passives) {
if (passive.type == type) return passive.value;
}
return 0.0;
}
/// 특정 패시브 보유 여부
bool hasPassive(PassiveType type) {
return passives.any((p) => p.type == type);
}
}