- Skill 클래스에 tier 필드 추가 (1~5, 높을수록 강함) - 타입별 powerScore 동적 계산 로직 구현 - isStrongerThan() 메서드로 스킬 강도 비교 지원 - SkillFailReason에 onGlobalCooldown 추가 - SkillSlots 클래스 신규 추가 (타입별 슬롯 제한)
579 lines
15 KiB
Dart
579 lines
15 KiB
Dart
// ============================================================================
|
||
// 랭크 스케일링 (Rank Scaling)
|
||
// ============================================================================
|
||
|
||
/// 스펠 랭크에 따른 스킬 배율 계산
|
||
///
|
||
/// 랭크 1: 1.0x, 랭크 2: 1.15x, 랭크 3: 1.30x, ...
|
||
double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.15;
|
||
|
||
/// 랭크에 따른 쿨타임 감소율 계산
|
||
///
|
||
/// 랭크당 5% 감소 (최대 50% 감소)
|
||
double getRankCooldownMultiplier(int rank) =>
|
||
(1.0 - (rank - 1) * 0.05).clamp(0.5, 1.0);
|
||
|
||
/// 랭크에 따른 MP 비용 감소율 계산
|
||
///
|
||
/// 랭크당 3% 감소 (최대 30% 감소)
|
||
double getRankMpMultiplier(int rank) =>
|
||
(1.0 - (rank - 1) * 0.03).clamp(0.7, 1.0);
|
||
|
||
// ============================================================================
|
||
// 스킬 타입 (Skill Types)
|
||
// ============================================================================
|
||
|
||
/// 스킬 타입
|
||
enum SkillType {
|
||
/// 공격 스킬
|
||
attack,
|
||
|
||
/// 회복 스킬
|
||
heal,
|
||
|
||
/// 버프 스킬
|
||
buff,
|
||
|
||
/// 디버프 스킬
|
||
debuff,
|
||
}
|
||
|
||
/// 스킬 속성 (하이브리드: 코드 + 시스템)
|
||
enum SkillElement {
|
||
/// 논리 (Logic) - 순수 데미지
|
||
logic,
|
||
|
||
/// 메모리 (Memory) - DoT 특화
|
||
memory,
|
||
|
||
/// 네트워크 (Network) - 다중 타격
|
||
network,
|
||
|
||
/// 화염 (Overheat) - 높은 순간 데미지
|
||
fire,
|
||
|
||
/// 빙결 (Freeze) - 슬로우 효과
|
||
ice,
|
||
|
||
/// 전기 (Surge) - 빠른 연속 타격
|
||
lightning,
|
||
|
||
/// 공허 (Null) - 방어 무시
|
||
voidElement,
|
||
|
||
/// 혼돈 (Glitch) - 랜덤 효과
|
||
chaos,
|
||
}
|
||
|
||
/// 공격 방식
|
||
enum AttackMode {
|
||
/// 단발성 - 즉시 데미지
|
||
instant,
|
||
|
||
/// 지속 피해 - N초간 틱당 데미지
|
||
dot,
|
||
}
|
||
|
||
/// 버프 효과
|
||
class BuffEffect {
|
||
const BuffEffect({
|
||
required this.id,
|
||
required this.name,
|
||
required this.durationMs,
|
||
this.atkModifier = 0.0,
|
||
this.defModifier = 0.0,
|
||
this.criRateModifier = 0.0,
|
||
this.evasionModifier = 0.0,
|
||
});
|
||
|
||
/// 버프 ID
|
||
final String id;
|
||
|
||
/// 버프 이름
|
||
final String name;
|
||
|
||
/// 지속 시간 (밀리초)
|
||
final int durationMs;
|
||
|
||
/// 공격력 배율 보정 (0.0 = 변화 없음, 0.5 = +50%)
|
||
final double atkModifier;
|
||
|
||
/// 방어력 배율 보정
|
||
final double defModifier;
|
||
|
||
/// 크리티컬 확률 보정
|
||
final double criRateModifier;
|
||
|
||
/// 회피율 보정
|
||
final double evasionModifier;
|
||
}
|
||
|
||
/// 스킬 정의
|
||
class Skill {
|
||
const Skill({
|
||
required this.id,
|
||
required this.name,
|
||
required this.type,
|
||
required this.mpCost,
|
||
required this.cooldownMs,
|
||
required this.power,
|
||
this.tier = 1,
|
||
this.damageMultiplier = 1.0,
|
||
this.healAmount = 0,
|
||
this.healPercent = 0.0,
|
||
this.buff,
|
||
this.selfDamagePercent = 0.0,
|
||
this.targetDefReduction = 0.0,
|
||
this.element,
|
||
this.attackMode = AttackMode.instant,
|
||
this.baseDotDamage,
|
||
this.baseDotDurationMs,
|
||
this.baseDotTickMs,
|
||
this.hitCount = 1,
|
||
this.lifestealPercent = 0.0,
|
||
this.mpHealAmount = 0,
|
||
});
|
||
|
||
/// 스킬 티어 (1~5, 높을수록 강함)
|
||
final int tier;
|
||
|
||
/// 스킬 ID
|
||
final String id;
|
||
|
||
/// 스킬 이름
|
||
final String name;
|
||
|
||
/// 스킬 타입
|
||
final SkillType type;
|
||
|
||
/// MP 소모량
|
||
final int mpCost;
|
||
|
||
/// 쿨타임 (밀리초)
|
||
final int cooldownMs;
|
||
|
||
/// 스킬 위력 (기본 값)
|
||
final int power;
|
||
|
||
/// 데미지 배율 (공격 스킬용)
|
||
final double damageMultiplier;
|
||
|
||
/// 고정 회복량 (회복 스킬용)
|
||
final int healAmount;
|
||
|
||
/// HP% 회복 (회복 스킬용, 0.0 ~ 1.0)
|
||
final double healPercent;
|
||
|
||
/// 버프 효과 (버프/디버프 스킬용)
|
||
final BuffEffect? buff;
|
||
|
||
/// 자해 데미지 % (일부 강력한 스킬)
|
||
final double selfDamagePercent;
|
||
|
||
/// 적 방어력 감소 % (일부 공격 스킬)
|
||
final double targetDefReduction;
|
||
|
||
/// 스킬 속성 (element) - 하이브리드 시스템
|
||
final SkillElement? element;
|
||
|
||
/// 공격 방식 (instant: 단발성, dot: 지속 피해)
|
||
final AttackMode attackMode;
|
||
|
||
/// DOT 기본 틱당 데미지 (스킬 레벨로 결정)
|
||
final int? baseDotDamage;
|
||
|
||
/// DOT 기본 지속시간 (밀리초, 스킬 레벨로 결정)
|
||
final int? baseDotDurationMs;
|
||
|
||
/// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정)
|
||
final int? baseDotTickMs;
|
||
|
||
/// 다중 타격 횟수 (기본 1)
|
||
final int hitCount;
|
||
|
||
/// HP 흡수율 (0.0 ~ 1.0, 데미지의 N% 회복)
|
||
final double lifestealPercent;
|
||
|
||
/// MP 회복량 (MP 회복 스킬용)
|
||
final int mpHealAmount;
|
||
|
||
/// 공격 스킬 여부
|
||
bool get isAttack => type == SkillType.attack;
|
||
|
||
/// 회복 스킬 여부
|
||
bool get isHeal => type == SkillType.heal;
|
||
|
||
/// 버프 스킬 여부
|
||
bool get isBuff => type == SkillType.buff;
|
||
|
||
/// 디버프 스킬 여부
|
||
bool get isDebuff => type == SkillType.debuff;
|
||
|
||
/// DOT 스킬 여부
|
||
bool get isDot => attackMode == AttackMode.dot;
|
||
|
||
/// MP 효율 (데미지 당 MP 비용)
|
||
double get mpEfficiency {
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// 스킬 사용 상태 (쿨타임 추적)
|
||
class SkillState {
|
||
const SkillState({
|
||
required this.skillId,
|
||
required this.lastUsedMs,
|
||
required this.rank,
|
||
});
|
||
|
||
/// 스킬 ID
|
||
final String skillId;
|
||
|
||
/// 마지막 사용 시간 (게임 내 경과 시간, 밀리초)
|
||
final int lastUsedMs;
|
||
|
||
/// 스킬 랭크 (레벨)
|
||
final int rank;
|
||
|
||
/// 쿨타임 완료 여부
|
||
bool isReady(int currentMs, int cooldownMs) {
|
||
return currentMs - lastUsedMs >= cooldownMs;
|
||
}
|
||
|
||
/// 남은 쿨타임 (밀리초)
|
||
int remainingCooldown(int currentMs, int cooldownMs) {
|
||
final elapsed = currentMs - lastUsedMs;
|
||
if (elapsed >= cooldownMs) return 0;
|
||
return cooldownMs - elapsed;
|
||
}
|
||
|
||
SkillState copyWith({String? skillId, int? lastUsedMs, int? rank}) {
|
||
return SkillState(
|
||
skillId: skillId ?? this.skillId,
|
||
lastUsedMs: lastUsedMs ?? this.lastUsedMs,
|
||
rank: rank ?? this.rank,
|
||
);
|
||
}
|
||
|
||
/// 새 스킬 상태 생성 (쿨타임 0)
|
||
factory SkillState.fresh(String skillId, {int rank = 1}) {
|
||
return SkillState(
|
||
skillId: skillId,
|
||
lastUsedMs: -999999, // 즉시 사용 가능하도록 먼 과거
|
||
rank: rank,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 활성 버프 상태
|
||
class ActiveBuff {
|
||
const ActiveBuff({
|
||
required this.effect,
|
||
required this.startedMs,
|
||
required this.sourceSkillId,
|
||
});
|
||
|
||
/// 버프 효과
|
||
final BuffEffect effect;
|
||
|
||
/// 버프 시작 시간 (게임 내 경과 시간)
|
||
final int startedMs;
|
||
|
||
/// 버프를 발동한 스킬 ID
|
||
final String sourceSkillId;
|
||
|
||
/// 버프 만료 여부
|
||
bool isExpired(int currentMs) {
|
||
return currentMs - startedMs >= effect.durationMs;
|
||
}
|
||
|
||
/// 남은 지속 시간 (밀리초)
|
||
int remainingDuration(int currentMs) {
|
||
final elapsed = currentMs - startedMs;
|
||
if (elapsed >= effect.durationMs) return 0;
|
||
return effect.durationMs - elapsed;
|
||
}
|
||
|
||
ActiveBuff copyWith({
|
||
BuffEffect? effect,
|
||
int? startedMs,
|
||
String? sourceSkillId,
|
||
}) {
|
||
return ActiveBuff(
|
||
effect: effect ?? this.effect,
|
||
startedMs: startedMs ?? this.startedMs,
|
||
sourceSkillId: sourceSkillId ?? this.sourceSkillId,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 스킬 사용 결과
|
||
class SkillUseResult {
|
||
const SkillUseResult({
|
||
required this.skill,
|
||
required this.success,
|
||
this.damage = 0,
|
||
this.healedAmount = 0,
|
||
this.appliedBuff,
|
||
this.failReason,
|
||
});
|
||
|
||
/// 사용한 스킬
|
||
final Skill skill;
|
||
|
||
/// 성공 여부
|
||
final bool success;
|
||
|
||
/// 데미지 (공격 스킬)
|
||
final int damage;
|
||
|
||
/// 회복량 (회복 스킬)
|
||
final int healedAmount;
|
||
|
||
/// 적용된 버프 (버프 스킬)
|
||
final ActiveBuff? appliedBuff;
|
||
|
||
/// 실패 사유
|
||
final SkillFailReason? failReason;
|
||
|
||
/// 실패 결과 생성
|
||
factory SkillUseResult.failed(Skill skill, SkillFailReason reason) {
|
||
return SkillUseResult(skill: skill, success: false, failReason: reason);
|
||
}
|
||
}
|
||
|
||
/// 스킬 실패 사유
|
||
enum SkillFailReason {
|
||
/// MP 부족
|
||
notEnoughMp,
|
||
|
||
/// 쿨타임 중
|
||
onCooldown,
|
||
|
||
/// 글로벌 쿨타임(GCD) 중
|
||
onGlobalCooldown,
|
||
|
||
/// 스킬 없음
|
||
skillNotFound,
|
||
|
||
/// 사용 불가 상태
|
||
invalidState,
|
||
}
|
||
|
||
/// DOT (지속 피해) 효과
|
||
///
|
||
/// 스킬 사용 시 생성되어 전투 중 틱마다 데미지를 적용.
|
||
/// - INT: 틱당 데미지 증가
|
||
/// - WIS: 틱 간격 감소 (더 빠른 피해)
|
||
/// - 스킬 레벨: 기본 데미지, 지속시간, 틱 간격 결정
|
||
class DotEffect {
|
||
const DotEffect({
|
||
required this.skillId,
|
||
required this.baseDamage,
|
||
required this.damagePerTick,
|
||
required this.tickIntervalMs,
|
||
required this.totalDurationMs,
|
||
this.remainingDurationMs = 0,
|
||
this.tickAccumulatorMs = 0,
|
||
this.element,
|
||
});
|
||
|
||
/// 원본 스킬 ID
|
||
final String skillId;
|
||
|
||
/// 스킬 기본 데미지 (스킬 레벨로 결정)
|
||
final int baseDamage;
|
||
|
||
/// INT 보정 적용된 틱당 실제 데미지
|
||
final int damagePerTick;
|
||
|
||
/// WIS 보정 적용된 틱 간격 (밀리초)
|
||
final int tickIntervalMs;
|
||
|
||
/// 총 지속시간 (밀리초, 스킬 레벨로 결정)
|
||
final int totalDurationMs;
|
||
|
||
/// 남은 지속시간 (밀리초)
|
||
final int remainingDurationMs;
|
||
|
||
/// 다음 틱까지 누적 시간 (밀리초)
|
||
final int tickAccumulatorMs;
|
||
|
||
/// 속성 (선택)
|
||
final SkillElement? element;
|
||
|
||
/// DOT 만료 여부
|
||
bool get isExpired => remainingDurationMs <= 0;
|
||
|
||
/// DOT 활성 여부
|
||
bool get isActive => remainingDurationMs > 0;
|
||
|
||
/// 예상 남은 틱 수
|
||
int get remainingTicks {
|
||
if (tickIntervalMs <= 0) return 0;
|
||
return (remainingDurationMs / tickIntervalMs).ceil();
|
||
}
|
||
|
||
/// 스킬과 플레이어 스탯으로 DotEffect 생성
|
||
///
|
||
/// [skill] DOT 스킬
|
||
/// [playerInt] 플레이어 INT (틱당 데미지 보정)
|
||
/// [playerWis] 플레이어 WIS (틱 간격 보정)
|
||
factory DotEffect.fromSkill(
|
||
Skill skill, {
|
||
int playerInt = 10,
|
||
int playerWis = 10,
|
||
}) {
|
||
assert(skill.isDot, 'DOT 스킬만 DotEffect 생성 가능');
|
||
assert(skill.baseDotDamage != null, 'baseDotDamage 필수');
|
||
assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수');
|
||
assert(skill.baseDotTickMs != null, 'baseDotTickMs 필수');
|
||
|
||
// INT → 데미지 보정 (INT 10 기준, ±3%/포인트)
|
||
final intMod = 1.0 + (playerInt - 10) * 0.03;
|
||
final actualDamage = (skill.baseDotDamage! * intMod).round();
|
||
|
||
// WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐)
|
||
final wisMod = 1.0 + (playerWis - 10) * 0.02;
|
||
final actualTickMs = (skill.baseDotTickMs! / wisMod)
|
||
.clamp(200, 2000)
|
||
.round();
|
||
|
||
return DotEffect(
|
||
skillId: skill.id,
|
||
baseDamage: skill.baseDotDamage!,
|
||
damagePerTick: actualDamage.clamp(1, 9999),
|
||
tickIntervalMs: actualTickMs,
|
||
totalDurationMs: skill.baseDotDurationMs!,
|
||
remainingDurationMs: skill.baseDotDurationMs!,
|
||
tickAccumulatorMs: 0,
|
||
element: skill.element,
|
||
);
|
||
}
|
||
|
||
/// 시간 경과 후 새 DotEffect 반환
|
||
///
|
||
/// [elapsedMs] 경과 시간 (밀리초)
|
||
/// Returns: (새 DotEffect, 이번에 발생한 틱 수)
|
||
(DotEffect, int) tick(int elapsedMs) {
|
||
var newAccumulator = tickAccumulatorMs + elapsedMs;
|
||
var newRemaining = remainingDurationMs - elapsedMs;
|
||
var ticksTriggered = 0;
|
||
|
||
// 틱 발생 체크
|
||
while (newAccumulator >= tickIntervalMs && newRemaining > 0) {
|
||
newAccumulator -= tickIntervalMs;
|
||
ticksTriggered++;
|
||
}
|
||
|
||
final updated = DotEffect(
|
||
skillId: skillId,
|
||
baseDamage: baseDamage,
|
||
damagePerTick: damagePerTick,
|
||
tickIntervalMs: tickIntervalMs,
|
||
totalDurationMs: totalDurationMs,
|
||
remainingDurationMs: newRemaining.clamp(0, totalDurationMs),
|
||
tickAccumulatorMs: newRemaining > 0 ? newAccumulator : 0,
|
||
element: element,
|
||
);
|
||
|
||
return (updated, ticksTriggered);
|
||
}
|
||
|
||
DotEffect copyWith({
|
||
String? skillId,
|
||
int? baseDamage,
|
||
int? damagePerTick,
|
||
int? tickIntervalMs,
|
||
int? totalDurationMs,
|
||
int? remainingDurationMs,
|
||
int? tickAccumulatorMs,
|
||
SkillElement? element,
|
||
}) {
|
||
return DotEffect(
|
||
skillId: skillId ?? this.skillId,
|
||
baseDamage: baseDamage ?? this.baseDamage,
|
||
damagePerTick: damagePerTick ?? this.damagePerTick,
|
||
tickIntervalMs: tickIntervalMs ?? this.tickIntervalMs,
|
||
totalDurationMs: totalDurationMs ?? this.totalDurationMs,
|
||
remainingDurationMs: remainingDurationMs ?? this.remainingDurationMs,
|
||
tickAccumulatorMs: tickAccumulatorMs ?? this.tickAccumulatorMs,
|
||
element: element ?? this.element,
|
||
);
|
||
}
|
||
}
|