// ============================================================================ // 랭크 스케일링 (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, ); } }