diff --git a/lib/data/class_data.dart b/lib/data/class_data.dart new file mode 100644 index 0000000..7dff208 --- /dev/null +++ b/lib/data/class_data.dart @@ -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 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; + } +} diff --git a/lib/data/race_data.dart b/lib/data/race_data.dart new file mode 100644 index 0000000..4fc9076 --- /dev/null +++ b/lib/data/race_data.dart @@ -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 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; + } +} diff --git a/lib/src/core/engine/stat_calculator.dart b/lib/src/core/engine/stat_calculator.dart new file mode 100644 index 0000000..e4adb02 --- /dev/null +++ b/lib/src/core/engine/stat_calculator.dart @@ -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; + } +} diff --git a/lib/src/core/model/class_traits.dart b/lib/src/core/model/class_traits.dart new file mode 100644 index 0000000..d051cd5 --- /dev/null +++ b/lib/src/core/model/class_traits.dart @@ -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 allowedWeaponSlots; + + /// 금지된 슬롯 + final Set 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 statModifiers; + + /// 시작 스킬 ID 목록 + final List startingSkills; + + /// 클래스 전용 스킬 ID 목록 + final List classSkills; + + /// 패시브 능력 목록 + final List 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); + } +} diff --git a/lib/src/core/model/combat_stats.dart b/lib/src/core/model/combat_stats.dart index bf1dfab..069852c 100644 --- a/lib/src/core/model/combat_stats.dart +++ b/lib/src/core/model/combat_stats.dart @@ -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, diff --git a/lib/src/core/model/race_traits.dart b/lib/src/core/model/race_traits.dart new file mode 100644 index 0000000..5290d12 --- /dev/null +++ b/lib/src/core/model/race_traits.dart @@ -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 statModifiers; + + /// 패시브 능력 목록 + final List 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); + } +}