Files
asciinevrdie/lib/src/core/model/skill.dart
JiWoong Sul 2621942ced feat(model): 스킬 티어 및 파워 스코어 시스템 추가
- Skill 클래스에 tier 필드 추가 (1~5, 높을수록 강함)
- 타입별 powerScore 동적 계산 로직 구현
- isStrongerThan() 메서드로 스킬 강도 비교 지원
- SkillFailReason에 onGlobalCooldown 추가
- SkillSlots 클래스 신규 추가 (타입별 슬롯 제한)
2026-01-14 23:03:51 +09:00

579 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 랭크 스케일링 (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,
);
}
}