feat(model): 스킬 티어 및 파워 스코어 시스템 추가
- Skill 클래스에 tier 필드 추가 (1~5, 높을수록 강함) - 타입별 powerScore 동적 계산 로직 구현 - isStrongerThan() 메서드로 스킬 강도 비교 지원 - SkillFailReason에 onGlobalCooldown 추가 - SkillSlots 클래스 신규 추가 (타입별 슬롯 제한)
This commit is contained in:
@@ -117,6 +117,7 @@ class Skill {
|
|||||||
required this.mpCost,
|
required this.mpCost,
|
||||||
required this.cooldownMs,
|
required this.cooldownMs,
|
||||||
required this.power,
|
required this.power,
|
||||||
|
this.tier = 1,
|
||||||
this.damageMultiplier = 1.0,
|
this.damageMultiplier = 1.0,
|
||||||
this.healAmount = 0,
|
this.healAmount = 0,
|
||||||
this.healPercent = 0.0,
|
this.healPercent = 0.0,
|
||||||
@@ -133,6 +134,9 @@ class Skill {
|
|||||||
this.mpHealAmount = 0,
|
this.mpHealAmount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 스킬 티어 (1~5, 높을수록 강함)
|
||||||
|
final int tier;
|
||||||
|
|
||||||
/// 스킬 ID
|
/// 스킬 ID
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
@@ -213,6 +217,79 @@ class Skill {
|
|||||||
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
||||||
return damageMultiplier / mpCost;
|
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,
|
onCooldown,
|
||||||
|
|
||||||
|
/// 글로벌 쿨타임(GCD) 중
|
||||||
|
onGlobalCooldown,
|
||||||
|
|
||||||
/// 스킬 없음
|
/// 스킬 없음
|
||||||
skillNotFound,
|
skillNotFound,
|
||||||
|
|
||||||
|
|||||||
277
lib/src/core/model/skill_slots.dart
Normal file
277
lib/src/core/model/skill_slots.dart
Normal file
@@ -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<Skill> attackSkills;
|
||||||
|
|
||||||
|
/// 회복 스킬 (최대 2개)
|
||||||
|
final List<Skill> healSkills;
|
||||||
|
|
||||||
|
/// 버프 스킬 (최대 2개)
|
||||||
|
final List<Skill> buffSkills;
|
||||||
|
|
||||||
|
/// 디버프 스킬 (최대 1개)
|
||||||
|
final List<Skill> debuffSkills;
|
||||||
|
|
||||||
|
/// 장착된 모든 스킬 목록
|
||||||
|
List<Skill> get allSkills => [
|
||||||
|
...attackSkills,
|
||||||
|
...healSkills,
|
||||||
|
...buffSkills,
|
||||||
|
...debuffSkills,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 타입별 스킬 목록 반환
|
||||||
|
List<Skill> 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<Skill>.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<Skill>.from(skills)..removeAt(index);
|
||||||
|
return _copyWith(type, newSkills);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 약한 스킬 찾기 (powerScore 기준)
|
||||||
|
Skill _findWeakestSkill(List<Skill> 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<Skill> 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<Skill>? attackSkills,
|
||||||
|
List<Skill>? healSkills,
|
||||||
|
List<Skill>? buffSkills,
|
||||||
|
List<Skill>? debuffSkills,
|
||||||
|
}) {
|
||||||
|
return SkillSlots(
|
||||||
|
attackSkills: attackSkills ?? this.attackSkills,
|
||||||
|
healSkills: healSkills ?? this.healSkills,
|
||||||
|
buffSkills: buffSkills ?? this.buffSkills,
|
||||||
|
debuffSkills: debuffSkills ?? this.debuffSkills,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON 직렬화 (저장용)
|
||||||
|
Map<String, dynamic> 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<String, dynamic> json) {
|
||||||
|
List<Skill> parseSkillIds(List<dynamic>? ids) {
|
||||||
|
if (ids == null) return [];
|
||||||
|
return ids
|
||||||
|
.map((id) => SkillData.getSkillById(id as String))
|
||||||
|
.whereType<Skill>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SkillSlots(
|
||||||
|
attackSkills: parseSkillIds(json['attackSkills'] as List<dynamic>?),
|
||||||
|
healSkills: parseSkillIds(json['healSkills'] as List<dynamic>?),
|
||||||
|
buffSkills: parseSkillIds(json['buffSkills'] as List<dynamic>?),
|
||||||
|
debuffSkills: parseSkillIds(json['debuffSkills'] as List<dynamic>?),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user