From 2621942ced62b957bca1e3f7a0e0c8bfd5b24720 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 14 Jan 2026 23:03:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(model):=20=EC=8A=A4=ED=82=AC=20=ED=8B=B0?= =?UTF-8?q?=EC=96=B4=20=EB=B0=8F=20=ED=8C=8C=EC=9B=8C=20=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EC=96=B4=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Skill 클래스에 tier 필드 추가 (1~5, 높을수록 강함) - 타입별 powerScore 동적 계산 로직 구현 - isStrongerThan() 메서드로 스킬 강도 비교 지원 - SkillFailReason에 onGlobalCooldown 추가 - SkillSlots 클래스 신규 추가 (타입별 슬롯 제한) --- lib/src/core/model/skill.dart | 80 ++++++++ lib/src/core/model/skill_slots.dart | 277 ++++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 lib/src/core/model/skill_slots.dart diff --git a/lib/src/core/model/skill.dart b/lib/src/core/model/skill.dart index ef0c2a8..454e8bb 100644 --- a/lib/src/core/model/skill.dart +++ b/lib/src/core/model/skill.dart @@ -117,6 +117,7 @@ class Skill { required this.mpCost, required this.cooldownMs, required this.power, + this.tier = 1, this.damageMultiplier = 1.0, this.healAmount = 0, this.healPercent = 0.0, @@ -133,6 +134,9 @@ class Skill { this.mpHealAmount = 0, }); + /// 스킬 티어 (1~5, 높을수록 강함) + final int tier; + /// 스킬 ID final String id; @@ -213,6 +217,79 @@ class Skill { if (type != SkillType.attack || damageMultiplier <= 0) return 0; return damageMultiplier / mpCost; } + + /// 스킬 파워 점수 (동적 계산, 같은 티어 내 비교용) + /// + /// 타입별 다른 공식: + /// - 공격: DPS 기반 + /// - 회복: HPS 기반 + /// - 버프/디버프: 효과 강도 × 지속시간 / 쿨타임 + double get powerScore { + final cdSec = cooldownMs / 1000; + if (cdSec <= 0) return 0; + + return switch (type) { + SkillType.attack => _attackPowerScore(cdSec), + SkillType.heal => _healPowerScore(cdSec), + SkillType.buff => _buffPowerScore(cdSec), + SkillType.debuff => _debuffPowerScore(cdSec), + }; + } + + double _attackPowerScore(double cdSec) { + // DOT 스킬 + if (isDot && + baseDotDamage != null && + baseDotDurationMs != null && + baseDotTickMs != null && + baseDotTickMs! > 0) { + final ticks = baseDotDurationMs! / baseDotTickMs!; + return (baseDotDamage! * ticks) / cdSec; + } + + // 즉발 공격: power × 배율 × 타수 / 쿨타임 + var score = power * damageMultiplier * hitCount / cdSec; + score *= (1 + lifestealPercent); // 흡혈 보너스 + score *= (1 + targetDefReduction); // 방감 보너스 + return score; + } + + double _healPowerScore(double cdSec) { + // 고정 회복 + %회복(1000HP 기준) + MP회복 + final totalHeal = healAmount + (healPercent * 1000) + mpHealAmount; + var score = totalHeal / cdSec; + if (buff != null) score *= 1.2; // 추가 버프 보너스 + return score; + } + + double _buffPowerScore(double cdSec) { + if (buff == null) return 0; + final b = buff!; + final strength = + b.atkModifier.abs() + + b.defModifier.abs() + + b.criRateModifier.abs() + + b.evasionModifier.abs(); + return strength * (b.durationMs / 1000) / cdSec * 100; + } + + double _debuffPowerScore(double cdSec) { + if (buff == null) return 0; + final b = buff!; + final strength = b.atkModifier.abs() + b.defModifier.abs(); + return strength * (b.durationMs / 1000) / cdSec * 100; + } + + /// 다른 스킬과 비교하여 이 스킬이 더 강한지 판단 + /// + /// 1. 티어가 다르면 티어로 판단 + /// 2. 티어가 같으면 파워 점수로 판단 + bool isStrongerThan(Skill other) { + if (tier != other.tier) { + return tier > other.tier; + } + return powerScore > other.powerScore; + } } /// 스킬 사용 상태 (쿨타임 추적) @@ -347,6 +424,9 @@ enum SkillFailReason { /// 쿨타임 중 onCooldown, + /// 글로벌 쿨타임(GCD) 중 + onGlobalCooldown, + /// 스킬 없음 skillNotFound, diff --git a/lib/src/core/model/skill_slots.dart b/lib/src/core/model/skill_slots.dart new file mode 100644 index 0000000..004bfc0 --- /dev/null +++ b/lib/src/core/model/skill_slots.dart @@ -0,0 +1,277 @@ +import 'package:asciineverdie/data/skill_data.dart'; +import 'package:asciineverdie/src/core/model/skill.dart'; + +/// 스킬 슬롯 제한 상수 +/// +/// 타입별로 장착 가능한 최대 스킬 수 +class SkillSlotLimits { + SkillSlotLimits._(); + + static const int attack = 3; + static const int heal = 2; + static const int buff = 2; + static const int debuff = 1; + + /// 타입별 슬롯 제한 반환 + static int getLimit(SkillType type) { + return switch (type) { + SkillType.attack => attack, + SkillType.heal => heal, + SkillType.buff => buff, + SkillType.debuff => debuff, + }; + } +} + +/// 캐릭터가 장착한 스킬 슬롯 +/// +/// 타입별로 슬롯 제한이 있으며, 새 스킬이 기존 스킬보다 +/// 강할 경우 자동으로 교체됨 +class SkillSlots { + const SkillSlots({ + this.attackSkills = const [], + this.healSkills = const [], + this.buffSkills = const [], + this.debuffSkills = const [], + }); + + /// 공격 스킬 (최대 3개) + final List attackSkills; + + /// 회복 스킬 (최대 2개) + final List healSkills; + + /// 버프 스킬 (최대 2개) + final List buffSkills; + + /// 디버프 스킬 (최대 1개) + final List debuffSkills; + + /// 장착된 모든 스킬 목록 + List get allSkills => [ + ...attackSkills, + ...healSkills, + ...buffSkills, + ...debuffSkills, + ]; + + /// 타입별 스킬 목록 반환 + List getSkillsByType(SkillType type) { + return switch (type) { + SkillType.attack => attackSkills, + SkillType.heal => healSkills, + SkillType.buff => buffSkills, + SkillType.debuff => debuffSkills, + }; + } + + /// 해당 타입 슬롯이 가득 찼는지 확인 + bool isSlotFull(SkillType type) { + final current = getSkillsByType(type).length; + final limit = SkillSlotLimits.getLimit(type); + return current >= limit; + } + + /// 스킬이 이미 장착되어 있는지 확인 + bool hasSkill(String skillId) { + return allSkills.any((s) => s.id == skillId); + } + + /// 새 스킬 추가 시도 + /// + /// 반환값: (성공 여부, 새 SkillSlots, 교체된 스킬) + /// - 슬롯에 여유가 있으면 추가 + /// - 슬롯이 가득 차면 가장 약한 스킬과 비교 후 교체 + /// - 새 스킬이 더 약하면 추가 실패 + SkillAddResult tryAddSkill(Skill newSkill) { + // 이미 장착된 스킬인지 확인 + if (hasSkill(newSkill.id)) { + return SkillAddResult( + success: false, + slots: this, + replacedSkill: null, + reason: SkillAddFailReason.alreadyEquipped, + ); + } + + final type = newSkill.type; + final currentSkills = List.from(getSkillsByType(type)); + final limit = SkillSlotLimits.getLimit(type); + + // 슬롯에 여유가 있으면 그냥 추가 + if (currentSkills.length < limit) { + currentSkills.add(newSkill); + return SkillAddResult( + success: true, + slots: _copyWith(type, currentSkills), + replacedSkill: null, + reason: null, + ); + } + + // 슬롯이 가득 찼으면 가장 약한 스킬 찾기 + final weakest = _findWeakestSkill(currentSkills); + + // 새 스킬이 가장 약한 스킬보다 강한지 확인 + if (newSkill.isStrongerThan(weakest)) { + currentSkills.remove(weakest); + currentSkills.add(newSkill); + return SkillAddResult( + success: true, + slots: _copyWith(type, currentSkills), + replacedSkill: weakest, + reason: null, + ); + } + + // 새 스킬이 더 약함 + return SkillAddResult( + success: false, + slots: this, + replacedSkill: null, + reason: SkillAddFailReason.weakerThanExisting, + ); + } + + /// 특정 스킬 제거 + SkillSlots removeSkill(String skillId) { + for (final type in SkillType.values) { + final skills = getSkillsByType(type); + final index = skills.indexWhere((s) => s.id == skillId); + if (index != -1) { + final newSkills = List.from(skills)..removeAt(index); + return _copyWith(type, newSkills); + } + } + return this; + } + + /// 가장 약한 스킬 찾기 (powerScore 기준) + Skill _findWeakestSkill(List skills) { + if (skills.isEmpty) { + throw ArgumentError('스킬 목록이 비어있습니다'); + } + + var weakest = skills.first; + for (final skill in skills.skip(1)) { + // tier가 낮거나, tier가 같으면 powerScore가 낮은 것이 약함 + if (weakest.isStrongerThan(skill)) { + weakest = skill; + } + } + return weakest; + } + + /// 타입별 스킬 목록을 교체한 새 SkillSlots 반환 + SkillSlots _copyWith(SkillType type, List skills) { + return switch (type) { + SkillType.attack => SkillSlots( + attackSkills: skills, + healSkills: healSkills, + buffSkills: buffSkills, + debuffSkills: debuffSkills, + ), + SkillType.heal => SkillSlots( + attackSkills: attackSkills, + healSkills: skills, + buffSkills: buffSkills, + debuffSkills: debuffSkills, + ), + SkillType.buff => SkillSlots( + attackSkills: attackSkills, + healSkills: healSkills, + buffSkills: skills, + debuffSkills: debuffSkills, + ), + SkillType.debuff => SkillSlots( + attackSkills: attackSkills, + healSkills: healSkills, + buffSkills: buffSkills, + debuffSkills: skills, + ), + }; + } + + /// copyWith 메서드 + SkillSlots copyWith({ + List? attackSkills, + List? healSkills, + List? buffSkills, + List? debuffSkills, + }) { + return SkillSlots( + attackSkills: attackSkills ?? this.attackSkills, + healSkills: healSkills ?? this.healSkills, + buffSkills: buffSkills ?? this.buffSkills, + debuffSkills: debuffSkills ?? this.debuffSkills, + ); + } + + /// JSON 직렬화 (저장용) + Map toJson() { + return { + 'attackSkills': attackSkills.map((s) => s.id).toList(), + 'healSkills': healSkills.map((s) => s.id).toList(), + 'buffSkills': buffSkills.map((s) => s.id).toList(), + 'debuffSkills': debuffSkills.map((s) => s.id).toList(), + }; + } + + /// JSON 역직렬화 + factory SkillSlots.fromJson(Map json) { + List parseSkillIds(List? ids) { + if (ids == null) return []; + return ids + .map((id) => SkillData.getSkillById(id as String)) + .whereType() + .toList(); + } + + return SkillSlots( + attackSkills: parseSkillIds(json['attackSkills'] as List?), + healSkills: parseSkillIds(json['healSkills'] as List?), + buffSkills: parseSkillIds(json['buffSkills'] as List?), + debuffSkills: parseSkillIds(json['debuffSkills'] as List?), + ); + } + + @override + String toString() { + return 'SkillSlots(' + 'attack: ${attackSkills.length}/${SkillSlotLimits.attack}, ' + 'heal: ${healSkills.length}/${SkillSlotLimits.heal}, ' + 'buff: ${buffSkills.length}/${SkillSlotLimits.buff}, ' + 'debuff: ${debuffSkills.length}/${SkillSlotLimits.debuff})'; + } +} + +/// 스킬 추가 결과 +class SkillAddResult { + const SkillAddResult({ + required this.success, + required this.slots, + required this.replacedSkill, + required this.reason, + }); + + /// 추가 성공 여부 + final bool success; + + /// 결과 SkillSlots (성공 시 새 슬롯, 실패 시 기존 슬롯) + final SkillSlots slots; + + /// 교체된 스킬 (슬롯이 가득 차서 교체된 경우) + final Skill? replacedSkill; + + /// 실패 사유 (실패 시에만 설정) + final SkillAddFailReason? reason; +} + +/// 스킬 추가 실패 사유 +enum SkillAddFailReason { + /// 이미 장착된 스킬 + alreadyEquipped, + + /// 기존 스킬보다 약함 + weakerThanExisting, +}