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

185
lib/data/class_data.dart Normal file
View File

@@ -0,0 +1,185 @@
import 'package:askiineverdie/src/core/model/class_traits.dart';
import 'package:askiineverdie/src/core/model/race_traits.dart';
/// 클래스 데이터 정의 (class data)
///
/// 프로그래밍 테마의 6가지 클래스 정의
class ClassData {
ClassData._();
/// Bug Hunter: 전사형, 물리 공격 보너스
static const bugHunter = ClassTraits(
classId: 'bug_hunter',
name: 'Bug Hunter',
statModifiers: {
StatType.str: 2,
StatType.dex: 1,
},
startingSkills: ['power_strike'],
classSkills: ['power_strike', 'execute', 'berserker_rage'],
passives: [
ClassPassive(
type: ClassPassiveType.physicalDamageBonus,
value: 0.20,
description: '일반 공격 +20%',
),
],
restriction: EquipmentRestriction.none,
);
/// Debugger Paladin: 탱커형, 방어/회복 보너스
static const debuggerPaladin = ClassTraits(
classId: 'debugger_paladin',
name: 'Debugger Paladin',
statModifiers: {
StatType.str: 1,
StatType.con: 2,
},
startingSkills: ['shield_bash'],
classSkills: ['shield_bash', 'holy_light', 'divine_protection'],
passives: [
ClassPassive(
type: ClassPassiveType.defenseBonus,
value: 0.15,
description: '방어력 +15%',
),
ClassPassive(
type: ClassPassiveType.healingBonus,
value: 0.10,
description: '회복력 +10%',
),
],
restriction: EquipmentRestriction(
armorWeight: ArmorWeight.heavy,
),
);
/// Compiler Mage: 마법사형, 마법 데미지 보너스
static const compilerMage = ClassTraits(
classId: 'compiler_mage',
name: 'Compiler Mage',
statModifiers: {
StatType.intelligence: 2,
StatType.wis: 1,
},
startingSkills: ['fireball'],
classSkills: ['fireball', 'ice_storm', 'arcane_blast', 'mana_shield'],
passives: [
ClassPassive(
type: ClassPassiveType.magicDamageBonus,
value: 0.25,
description: '마법 데미지 +25%',
),
],
restriction: EquipmentRestriction(
armorWeight: ArmorWeight.light,
),
);
/// Refactor Monk: 민첩형, 회피/연속공격
static const refactorMonk = ClassTraits(
classId: 'refactor_monk',
name: 'Refactor Monk',
statModifiers: {
StatType.dex: 2,
StatType.wis: 1,
},
startingSkills: ['flurry'],
classSkills: ['flurry', 'palm_strike', 'meditation'],
passives: [
ClassPassive(
type: ClassPassiveType.evasionBonus,
value: 0.15,
description: '회피율 +15%',
),
ClassPassive(
type: ClassPassiveType.multiAttack,
value: 1.0,
description: '연속 공격 가능',
),
],
restriction: EquipmentRestriction(
armorWeight: ArmorWeight.light,
),
);
/// Pointer Assassin: 암살자형, 크리티컬/첫 공격 보너스
static const pointerAssassin = ClassTraits(
classId: 'pointer_assassin',
name: 'Pointer Assassin',
statModifiers: {
StatType.dex: 2,
StatType.str: 1,
},
startingSkills: ['backstab'],
classSkills: ['backstab', 'poison_blade', 'shadow_step'],
passives: [
ClassPassive(
type: ClassPassiveType.criticalBonus,
value: 0.20,
description: '크리티컬 +20%',
),
ClassPassive(
type: ClassPassiveType.firstStrikeBonus,
value: 2.0,
description: '첫 공격 2배',
),
],
restriction: EquipmentRestriction(
armorWeight: ArmorWeight.light,
),
);
/// Garbage Collector: 탱커형, HP/전투 후 회복
static const garbageCollector = ClassTraits(
classId: 'garbage_collector',
name: 'Garbage Collector',
statModifiers: {
StatType.con: 2,
StatType.str: 1,
},
startingSkills: ['absorb'],
classSkills: ['absorb', 'recycle', 'memory_leak'],
passives: [
ClassPassive(
type: ClassPassiveType.hpBonus,
value: 0.30,
description: 'HP +30%',
),
ClassPassive(
type: ClassPassiveType.postCombatHeal,
value: 0.10,
description: '전투 후 HP 10% 회복',
),
],
restriction: EquipmentRestriction(
armorWeight: ArmorWeight.heavy,
),
);
/// 모든 클래스 목록
static const List<ClassTraits> all = [
bugHunter,
debuggerPaladin,
compilerMage,
refactorMonk,
pointerAssassin,
garbageCollector,
];
/// ID로 클래스 찾기
static ClassTraits? findById(String classId) {
for (final klass in all) {
if (klass.classId == classId) return klass;
}
return null;
}
/// 이름으로 클래스 찾기
static ClassTraits? findByName(String name) {
for (final klass in all) {
if (klass.name == name) return klass;
}
return null;
}
}

167
lib/data/race_data.dart Normal file
View File

@@ -0,0 +1,167 @@
import 'package:askiineverdie/src/core/model/race_traits.dart';
/// 종족 데이터 정의 (race data)
///
/// 프로그래밍 테마의 7가지 종족 정의
class RaceData {
RaceData._();
/// Byte Human: 균형형, 경험치 보너스
static const byteHuman = RaceTraits(
raceId: 'byte_human',
name: 'Byte Human',
statModifiers: {
StatType.cha: 2,
},
passives: [
PassiveAbility(
type: PassiveType.expBonus,
value: 0.10,
description: '경험치 +10%',
),
],
expMultiplier: 1.10,
);
/// Null Elf: 민첩/지능형, 마법 데미지 보너스
static const nullElf = RaceTraits(
raceId: 'null_elf',
name: 'Null Elf',
statModifiers: {
StatType.str: -1,
StatType.con: -1,
StatType.dex: 2,
StatType.intelligence: 2,
},
passives: [
PassiveAbility(
type: PassiveType.magicDamageBonus,
value: 0.15,
description: '마법 데미지 +15%',
),
],
);
/// Buffer Dwarf: 힘/체력형, 방어력 보너스
static const bufferDwarf = RaceTraits(
raceId: 'buffer_dwarf',
name: 'Buffer Dwarf',
statModifiers: {
StatType.str: 2,
StatType.con: 2,
StatType.dex: -1,
StatType.intelligence: -1,
},
passives: [
PassiveAbility(
type: PassiveType.defenseBonus,
value: 0.10,
description: '방어력 +10%',
),
],
);
/// Stack Goblin: 민첩형, 크리티컬 보너스
static const stackGoblin = RaceTraits(
raceId: 'stack_goblin',
name: 'Stack Goblin',
statModifiers: {
StatType.str: -1,
StatType.con: -1,
StatType.dex: 3,
StatType.cha: 1,
},
passives: [
PassiveAbility(
type: PassiveType.criticalBonus,
value: 0.05,
description: '크리티컬 확률 +5%',
),
],
);
/// Heap Troll: 체력형, HP 보너스
static const heapTroll = RaceTraits(
raceId: 'heap_troll',
name: 'Heap Troll',
statModifiers: {
StatType.str: 3,
StatType.con: 3,
StatType.dex: -2,
StatType.intelligence: -2,
},
passives: [
PassiveAbility(
type: PassiveType.hpBonus,
value: 0.20,
description: 'HP +20%',
),
],
);
/// Pointer Fairy: 마법형, MP 보너스
static const pointerFairy = RaceTraits(
raceId: 'pointer_fairy',
name: 'Pointer Fairy',
statModifiers: {
StatType.str: -2,
StatType.con: -2,
StatType.dex: 2,
StatType.intelligence: 2,
StatType.wis: 2,
},
passives: [
PassiveAbility(
type: PassiveType.mpBonus,
value: 0.20,
description: 'MP +20%',
),
],
);
/// Coredump Undead: 탱커형, 사망 시 장비 보존
static const coredumpUndead = RaceTraits(
raceId: 'coredump_undead',
name: 'Coredump Undead',
statModifiers: {
StatType.str: 1,
StatType.con: 2,
StatType.dex: -1,
StatType.cha: -2,
},
passives: [
PassiveAbility(
type: PassiveType.deathEquipmentPreserve,
value: 1.0,
description: '사망 시 장비 1개 유지',
),
],
);
/// 모든 종족 목록
static const List<RaceTraits> all = [
byteHuman,
nullElf,
bufferDwarf,
stackGoblin,
heapTroll,
pointerFairy,
coredumpUndead,
];
/// ID로 종족 찾기
static RaceTraits? findById(String raceId) {
for (final race in all) {
if (race.raceId == raceId) return race;
}
return null;
}
/// 이름으로 종족 찾기
static RaceTraits? findByName(String name) {
for (final race in all) {
if (race.name == name) return race;
}
return null;
}
}

View File

@@ -0,0 +1,200 @@
import 'package:askiineverdie/src/core/model/class_traits.dart';
import 'package:askiineverdie/src/core/model/combat_stats.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/model/race_traits.dart';
/// 스탯 계산기 (stat calculator)
///
/// 기본 스탯에 종족/클래스 보정을 적용하여 최종 스탯 계산
class StatCalculator {
const StatCalculator();
/// 기본 스탯에 종족/클래스 보정 적용
///
/// [baseStats] 기본 캐릭터 스탯
/// [race] 종족 특성
/// [klass] 클래스 특성
Stats applyModifiers({
required Stats baseStats,
required RaceTraits race,
required ClassTraits klass,
}) {
// 종족 보정 적용
var str = baseStats.str + race.getModifier(StatType.str);
var con = baseStats.con + race.getModifier(StatType.con);
var dex = baseStats.dex + race.getModifier(StatType.dex);
var intel = baseStats.intelligence + race.getModifier(StatType.intelligence);
var wis = baseStats.wis + race.getModifier(StatType.wis);
var cha = baseStats.cha + race.getModifier(StatType.cha);
// 클래스 보정 적용
str += klass.getModifier(StatType.str);
con += klass.getModifier(StatType.con);
dex += klass.getModifier(StatType.dex);
intel += klass.getModifier(StatType.intelligence);
wis += klass.getModifier(StatType.wis);
cha += klass.getModifier(StatType.cha);
// HP/MP에 종족 패시브 적용
var hpMax = baseStats.hpMax;
var mpMax = baseStats.mpMax;
// 종족 HP 보너스 (Heap Troll: +20%)
final raceHpBonus = race.getPassiveValue(PassiveType.hpBonus);
if (raceHpBonus > 0) {
hpMax = (hpMax * (1 + raceHpBonus)).round();
}
// 종족 MP 보너스 (Pointer Fairy: +20%)
final raceMpBonus = race.getPassiveValue(PassiveType.mpBonus);
if (raceMpBonus > 0) {
mpMax = (mpMax * (1 + raceMpBonus)).round();
}
// 클래스 HP 보너스 (Garbage Collector: +30%)
final classHpBonus = klass.getPassiveValue(ClassPassiveType.hpBonus);
if (classHpBonus > 0) {
hpMax = (hpMax * (1 + classHpBonus)).round();
}
return baseStats.copyWith(
str: str,
con: con,
dex: dex,
intelligence: intel,
wis: wis,
cha: cha,
hpMax: hpMax,
mpMax: mpMax,
);
}
/// CombatStats에 종족/클래스 패시브 효과 적용
///
/// [combatStats] 기본 전투 스탯
/// [race] 종족 특성
/// [klass] 클래스 특성
CombatStats applyPassives({
required CombatStats combatStats,
required RaceTraits race,
required ClassTraits klass,
}) {
var atk = combatStats.atk;
var def = combatStats.def;
var magAtk = combatStats.magAtk;
var criRate = combatStats.criRate;
var evasion = combatStats.evasion;
// 종족 패시브 적용
// 마법 데미지 보너스 (Null Elf: +15%)
final raceMagicBonus = race.getPassiveValue(PassiveType.magicDamageBonus);
if (raceMagicBonus > 0) {
magAtk = (magAtk * (1 + raceMagicBonus)).round();
}
// 방어력 보너스 (Buffer Dwarf: +10%)
final raceDefenseBonus = race.getPassiveValue(PassiveType.defenseBonus);
if (raceDefenseBonus > 0) {
def = (def * (1 + raceDefenseBonus)).round();
}
// 크리티컬 보너스 (Stack Goblin: +5%)
final raceCritBonus = race.getPassiveValue(PassiveType.criticalBonus);
if (raceCritBonus > 0) {
criRate = (criRate + raceCritBonus).clamp(0.0, 0.8);
}
// 클래스 패시브 적용
// 물리 공격력 보너스 (Bug Hunter: +20%)
final classPhysicalBonus = klass.getPassiveValue(ClassPassiveType.physicalDamageBonus);
if (classPhysicalBonus > 0) {
atk = (atk * (1 + classPhysicalBonus)).round();
}
// 방어력 보너스 (Debugger Paladin: +15%)
final classDefenseBonus = klass.getPassiveValue(ClassPassiveType.defenseBonus);
if (classDefenseBonus > 0) {
def = (def * (1 + classDefenseBonus)).round();
}
// 마법 데미지 보너스 (Compiler Mage: +25%)
final classMagicBonus = klass.getPassiveValue(ClassPassiveType.magicDamageBonus);
if (classMagicBonus > 0) {
magAtk = (magAtk * (1 + classMagicBonus)).round();
}
// 회피율 보너스 (Refactor Monk: +15%)
final classEvasionBonus = klass.getPassiveValue(ClassPassiveType.evasionBonus);
if (classEvasionBonus > 0) {
evasion = (evasion + classEvasionBonus).clamp(0.0, 0.6);
}
// 크리티컬 보너스 (Pointer Assassin: +20%)
final classCritBonus = klass.getPassiveValue(ClassPassiveType.criticalBonus);
if (classCritBonus > 0) {
criRate = (criRate + classCritBonus).clamp(0.0, 0.8);
}
return combatStats.copyWith(
atk: atk,
def: def,
magAtk: magAtk,
criRate: criRate,
evasion: evasion,
);
}
/// 경험치 배율 계산
///
/// 종족 경험치 배율 반환 (Byte Human: 1.10)
double calculateExpMultiplier(RaceTraits race) {
return race.expMultiplier;
}
/// 전투 후 HP 회복량 계산 (Garbage Collector 패시브)
///
/// [klass] 클래스 특성
/// [maxHp] 최대 HP
/// Returns: 회복할 HP 양
int calculatePostCombatHeal({
required ClassTraits klass,
required int maxHp,
}) {
final healRate = klass.getPassiveValue(ClassPassiveType.postCombatHeal);
if (healRate <= 0) return 0;
return (maxHp * healRate).round();
}
/// 첫 공격 배율 계산 (Pointer Assassin 패시브)
///
/// [klass] 클래스 특성
/// Returns: 첫 공격 배율 (기본 1.0)
double calculateFirstStrikeMultiplier(ClassTraits klass) {
final bonus = klass.getPassiveValue(ClassPassiveType.firstStrikeBonus);
return bonus > 0 ? bonus : 1.0;
}
/// 사망 시 보존할 장비 개수 (Coredump Undead 패시브)
///
/// [race] 종족 특성
/// Returns: 보존 장비 개수
int calculateDeathEquipmentPreserve(RaceTraits race) {
if (race.hasPassive(PassiveType.deathEquipmentPreserve)) {
return race.getPassiveValue(PassiveType.deathEquipmentPreserve).round();
}
return 0;
}
/// 연속 공격 가능 여부 (Refactor Monk 패시브)
bool hasMultiAttack(ClassTraits klass) {
return klass.hasPassive(ClassPassiveType.multiAttack);
}
/// 회복력 배율 계산 (Debugger Paladin 패시브)
double calculateHealingMultiplier(ClassTraits klass) {
final bonus = klass.getPassiveValue(ClassPassiveType.healingBonus);
return 1.0 + bonus;
}
}

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);
}
}