feat(skill): Phase 3 MP 기반 스킬 시스템 구현
- Skill, SkillType, BuffEffect, SkillState, SkillUseResult 클래스 정의 (skill.dart) - SkillSystemState를 GameState에 통합 (activeBuffs, skillStates, elapsedMs) - 프로그래밍 테마 스킬 데이터 정의 (skill_data.dart) - 공격: Debug Strike, Memory Leak, Core Dump, Kernel Panic 등 - 회복: Hot Reload, Garbage Collection, Quick Fix - 버프: Safe Mode, Overclock, Firewall - SkillService 구현 (skill_service.dart) - 스킬 사용 가능 여부 확인 (MP, 쿨타임) - 공격/회복/버프 스킬 사용 로직 - 자동 스킬 선택 (HP < 30% → 회복, 보스전 → 강력한 공격, 일반 → MP 효율) - MP 자연 회복 (비전투: 50ms당 1, 전투: WIS 기반) - progress_service.dart에 스킬 시스템 통합 - tick()에서 스킬 시간 업데이트 및 버프 만료 처리 - _processCombatTickWithSkills()로 전투 중 자동 스킬 사용
This commit is contained in:
233
lib/data/skill_data.dart
Normal file
233
lib/data/skill_data.dart
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
|
|
||||||
|
/// 게임 내 스킬 정의
|
||||||
|
///
|
||||||
|
/// 프로그래밍 테마에 맞춘 스킬 목록
|
||||||
|
class SkillData {
|
||||||
|
SkillData._();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 공격 스킬
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Debug Strike - 기본 공격 스킬
|
||||||
|
static const debugStrike = Skill(
|
||||||
|
id: 'debug_strike',
|
||||||
|
name: 'Debug Strike',
|
||||||
|
type: SkillType.attack,
|
||||||
|
mpCost: 10,
|
||||||
|
cooldownMs: 3000, // 3초
|
||||||
|
power: 15,
|
||||||
|
damageMultiplier: 1.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Memory Leak - 방어력 감소 효과
|
||||||
|
static const memoryLeak = Skill(
|
||||||
|
id: 'memory_leak',
|
||||||
|
name: 'Memory Leak',
|
||||||
|
type: SkillType.attack,
|
||||||
|
mpCost: 25,
|
||||||
|
cooldownMs: 8000, // 8초
|
||||||
|
power: 25,
|
||||||
|
damageMultiplier: 2.5,
|
||||||
|
targetDefReduction: 0.2, // 적 방어력 -20%
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Core Dump - 강력한 공격
|
||||||
|
static const coreDump = Skill(
|
||||||
|
id: 'core_dump',
|
||||||
|
name: 'Core Dump',
|
||||||
|
type: SkillType.attack,
|
||||||
|
mpCost: 50,
|
||||||
|
cooldownMs: 20000, // 20초
|
||||||
|
power: 40,
|
||||||
|
damageMultiplier: 4.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Kernel Panic - 최강 공격 (자해 데미지)
|
||||||
|
static const kernelPanic = Skill(
|
||||||
|
id: 'kernel_panic',
|
||||||
|
name: 'Kernel Panic',
|
||||||
|
type: SkillType.attack,
|
||||||
|
mpCost: 100,
|
||||||
|
cooldownMs: 60000, // 60초
|
||||||
|
power: 80,
|
||||||
|
damageMultiplier: 8.0,
|
||||||
|
selfDamagePercent: 0.1, // 자신 HP -10%
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Stack Overflow - 중급 공격
|
||||||
|
static const stackOverflow = Skill(
|
||||||
|
id: 'stack_overflow',
|
||||||
|
name: 'Stack Overflow',
|
||||||
|
type: SkillType.attack,
|
||||||
|
mpCost: 35,
|
||||||
|
cooldownMs: 12000, // 12초
|
||||||
|
power: 30,
|
||||||
|
damageMultiplier: 3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Null Pointer - 빠른 공격
|
||||||
|
static const nullPointer = Skill(
|
||||||
|
id: 'null_pointer',
|
||||||
|
name: 'Null Pointer',
|
||||||
|
type: SkillType.attack,
|
||||||
|
mpCost: 15,
|
||||||
|
cooldownMs: 4000, // 4초
|
||||||
|
power: 18,
|
||||||
|
damageMultiplier: 1.8,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 회복 스킬
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Hot Reload - HP 회복
|
||||||
|
static const hotReload = Skill(
|
||||||
|
id: 'hot_reload',
|
||||||
|
name: 'Hot Reload',
|
||||||
|
type: SkillType.heal,
|
||||||
|
mpCost: 20,
|
||||||
|
cooldownMs: 10000, // 10초
|
||||||
|
power: 0,
|
||||||
|
healPercent: 0.3, // HP 30% 회복
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Garbage Collection - 대량 회복
|
||||||
|
static const garbageCollection = Skill(
|
||||||
|
id: 'garbage_collection',
|
||||||
|
name: 'Garbage Collection',
|
||||||
|
type: SkillType.heal,
|
||||||
|
mpCost: 45,
|
||||||
|
cooldownMs: 25000, // 25초
|
||||||
|
power: 0,
|
||||||
|
healPercent: 0.5, // HP 50% 회복
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Quick Fix - 빠른 소량 회복
|
||||||
|
static const quickFix = Skill(
|
||||||
|
id: 'quick_fix',
|
||||||
|
name: 'Quick Fix',
|
||||||
|
type: SkillType.heal,
|
||||||
|
mpCost: 10,
|
||||||
|
cooldownMs: 5000, // 5초
|
||||||
|
power: 0,
|
||||||
|
healAmount: 20, // 고정 20 회복
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 버프 스킬
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Safe Mode - 방어 버프
|
||||||
|
static const safeMode = Skill(
|
||||||
|
id: 'safe_mode',
|
||||||
|
name: 'Safe Mode',
|
||||||
|
type: SkillType.buff,
|
||||||
|
mpCost: 30,
|
||||||
|
cooldownMs: 30000, // 30초
|
||||||
|
power: 0,
|
||||||
|
buff: BuffEffect(
|
||||||
|
id: 'safe_mode_buff',
|
||||||
|
name: 'Safe Mode',
|
||||||
|
durationMs: 10000, // 10초 지속
|
||||||
|
defModifier: 0.5, // 방어력 +50%
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Overclock - 공격 버프
|
||||||
|
static const overclock = Skill(
|
||||||
|
id: 'overclock',
|
||||||
|
name: 'Overclock',
|
||||||
|
type: SkillType.buff,
|
||||||
|
mpCost: 25,
|
||||||
|
cooldownMs: 25000, // 25초
|
||||||
|
power: 0,
|
||||||
|
buff: BuffEffect(
|
||||||
|
id: 'overclock_buff',
|
||||||
|
name: 'Overclock',
|
||||||
|
durationMs: 8000, // 8초 지속
|
||||||
|
atkModifier: 0.4, // 공격력 +40%
|
||||||
|
criRateModifier: 0.1, // 크리티컬 +10%
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Firewall - 회피 버프
|
||||||
|
static const firewall = Skill(
|
||||||
|
id: 'firewall',
|
||||||
|
name: 'Firewall',
|
||||||
|
type: SkillType.buff,
|
||||||
|
mpCost: 20,
|
||||||
|
cooldownMs: 20000, // 20초
|
||||||
|
power: 0,
|
||||||
|
buff: BuffEffect(
|
||||||
|
id: 'firewall_buff',
|
||||||
|
name: 'Firewall',
|
||||||
|
durationMs: 12000, // 12초 지속
|
||||||
|
evasionModifier: 0.15, // 회피율 +15%
|
||||||
|
defModifier: 0.2, // 방어력 +20%
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스킬 목록
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 모든 스킬 목록
|
||||||
|
static const List<Skill> allSkills = [
|
||||||
|
// 공격 스킬
|
||||||
|
debugStrike,
|
||||||
|
nullPointer,
|
||||||
|
memoryLeak,
|
||||||
|
stackOverflow,
|
||||||
|
coreDump,
|
||||||
|
kernelPanic,
|
||||||
|
// 회복 스킬
|
||||||
|
quickFix,
|
||||||
|
hotReload,
|
||||||
|
garbageCollection,
|
||||||
|
// 버프 스킬
|
||||||
|
overclock,
|
||||||
|
safeMode,
|
||||||
|
firewall,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// ID로 스킬 찾기
|
||||||
|
static Skill? getSkillById(String id) {
|
||||||
|
for (final skill in allSkills) {
|
||||||
|
if (skill.id == id) return skill;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 타입별 스킬 목록
|
||||||
|
static List<Skill> getSkillsByType(SkillType type) {
|
||||||
|
return allSkills.where((s) => s.type == type).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 공격 스킬 목록 (MP 효율순 정렬)
|
||||||
|
static List<Skill> get attackSkillsByEfficiency {
|
||||||
|
final attacks = getSkillsByType(SkillType.attack);
|
||||||
|
attacks.sort((a, b) => b.mpEfficiency.compareTo(a.mpEfficiency));
|
||||||
|
return attacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회복 스킬 목록
|
||||||
|
static List<Skill> get healSkills => getSkillsByType(SkillType.heal);
|
||||||
|
|
||||||
|
/// 버프 스킬 목록
|
||||||
|
static List<Skill> get buffSkills => getSkillsByType(SkillType.buff);
|
||||||
|
|
||||||
|
/// 기본 스킬 세트 (새 캐릭터용)
|
||||||
|
static List<String> get defaultSkillIds => [
|
||||||
|
debugStrike.id,
|
||||||
|
quickFix.id,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// MP 비용 이하의 사용 가능한 공격 스킬
|
||||||
|
static List<Skill> getAffordableAttackSkills(int currentMp) {
|
||||||
|
return getSkillsByType(SkillType.attack)
|
||||||
|
.where((s) => s.mpCost <= currentMp)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:askiineverdie/data/skill_data.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/combat_calculator.dart';
|
import 'package:askiineverdie/src/core/engine/combat_calculator.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:askiineverdie/src/core/engine/skill_service.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_state.dart';
|
import 'package:askiineverdie/src/core/model/combat_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
@@ -141,6 +143,34 @@ class ProgressService {
|
|||||||
var questDone = false;
|
var questDone = false;
|
||||||
var actDone = false;
|
var actDone = false;
|
||||||
|
|
||||||
|
// 스킬 시스템 시간 업데이트 (Phase 3)
|
||||||
|
final skillService = SkillService(rng: state.rng);
|
||||||
|
var skillSystem = skillService.updateElapsedTime(state.skillSystem, clamped);
|
||||||
|
|
||||||
|
// 만료된 버프 정리
|
||||||
|
skillSystem = skillService.cleanupExpiredBuffs(skillSystem);
|
||||||
|
|
||||||
|
// 비전투 시 MP 회복
|
||||||
|
final isInCombat = progress.currentTask.type == TaskType.kill &&
|
||||||
|
progress.currentCombat != null &&
|
||||||
|
progress.currentCombat!.isActive;
|
||||||
|
|
||||||
|
if (!isInCombat && nextState.stats.mp < nextState.stats.mpMax) {
|
||||||
|
final mpRegen = skillService.calculateMpRegen(
|
||||||
|
elapsedMs: clamped,
|
||||||
|
isInCombat: false,
|
||||||
|
wis: nextState.stats.wis,
|
||||||
|
);
|
||||||
|
if (mpRegen > 0) {
|
||||||
|
final newMp = (nextState.stats.mp + mpRegen).clamp(0, nextState.stats.mpMax);
|
||||||
|
nextState = nextState.copyWith(
|
||||||
|
stats: nextState.stats.copyWith(mpCurrent: newMp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextState = nextState.copyWith(skillSystem: skillSystem);
|
||||||
|
|
||||||
// Advance task bar if still running.
|
// Advance task bar if still running.
|
||||||
if (progress.task.position < progress.task.max) {
|
if (progress.task.position < progress.task.max) {
|
||||||
final uncapped = progress.task.position + clamped;
|
final uncapped = progress.task.position + clamped;
|
||||||
@@ -148,10 +178,18 @@ class ProgressService {
|
|||||||
? progress.task.max
|
? progress.task.max
|
||||||
: uncapped;
|
: uncapped;
|
||||||
|
|
||||||
// 킬 태스크 중 전투 진행
|
// 킬 태스크 중 전투 진행 (스킬 자동 사용 포함)
|
||||||
var updatedCombat = progress.currentCombat;
|
var updatedCombat = progress.currentCombat;
|
||||||
|
var updatedSkillSystem = nextState.skillSystem;
|
||||||
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
||||||
updatedCombat = _processCombatTick(nextState, updatedCombat, clamped);
|
final combatResult = _processCombatTickWithSkills(
|
||||||
|
nextState,
|
||||||
|
updatedCombat,
|
||||||
|
updatedSkillSystem,
|
||||||
|
clamped,
|
||||||
|
);
|
||||||
|
updatedCombat = combatResult.combat;
|
||||||
|
updatedSkillSystem = combatResult.skillSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress = progress.copyWith(
|
progress = progress.copyWith(
|
||||||
@@ -159,7 +197,7 @@ class ProgressService {
|
|||||||
currentCombat: updatedCombat,
|
currentCombat: updatedCombat,
|
||||||
);
|
);
|
||||||
nextState = _recalculateEncumbrance(
|
nextState = _recalculateEncumbrance(
|
||||||
nextState.copyWith(progress: progress),
|
nextState.copyWith(progress: progress, skillSystem: updatedSkillSystem),
|
||||||
);
|
);
|
||||||
return ProgressTickResult(state: nextState);
|
return ProgressTickResult(state: nextState);
|
||||||
}
|
}
|
||||||
@@ -791,22 +829,25 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 틱 처리
|
/// 전투 틱 처리 (스킬 자동 사용 포함)
|
||||||
///
|
///
|
||||||
/// [state] 현재 게임 상태
|
/// [state] 현재 게임 상태
|
||||||
/// [combat] 현재 전투 상태
|
/// [combat] 현재 전투 상태
|
||||||
|
/// [skillSystem] 스킬 시스템 상태
|
||||||
/// [elapsedMs] 경과 시간 (밀리초)
|
/// [elapsedMs] 경과 시간 (밀리초)
|
||||||
/// Returns: 업데이트된 전투 상태
|
/// Returns: 업데이트된 전투 상태 및 스킬 시스템 상태
|
||||||
CombatState _processCombatTick(
|
({CombatState combat, SkillSystemState skillSystem}) _processCombatTickWithSkills(
|
||||||
GameState state,
|
GameState state,
|
||||||
CombatState combat,
|
CombatState combat,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
int elapsedMs,
|
int elapsedMs,
|
||||||
) {
|
) {
|
||||||
if (!combat.isActive || combat.isCombatOver) {
|
if (!combat.isActive || combat.isCombatOver) {
|
||||||
return combat;
|
return (combat: combat, skillSystem: skillSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
final calculator = CombatCalculator(rng: state.rng);
|
final calculator = CombatCalculator(rng: state.rng);
|
||||||
|
final skillService = SkillService(rng: state.rng);
|
||||||
var playerStats = combat.playerStats;
|
var playerStats = combat.playerStats;
|
||||||
var monsterStats = combat.monsterStats;
|
var monsterStats = combat.monsterStats;
|
||||||
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
|
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
|
||||||
@@ -814,15 +855,66 @@ class ProgressService {
|
|||||||
var totalDamageDealt = combat.totalDamageDealt;
|
var totalDamageDealt = combat.totalDamageDealt;
|
||||||
var totalDamageTaken = combat.totalDamageTaken;
|
var totalDamageTaken = combat.totalDamageTaken;
|
||||||
var turnsElapsed = combat.turnsElapsed;
|
var turnsElapsed = combat.turnsElapsed;
|
||||||
|
var updatedSkillSystem = skillSystem;
|
||||||
|
|
||||||
// 플레이어 공격 체크
|
// 플레이어 공격 체크
|
||||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
final attackResult = calculator.playerAttackMonster(
|
// 스킬 자동 선택
|
||||||
attacker: playerStats,
|
final availableSkillIds = updatedSkillSystem.skillStates
|
||||||
defender: monsterStats,
|
.map((s) => s.skillId)
|
||||||
|
.toList();
|
||||||
|
// 기본 스킬이 없으면 기본 스킬 추가
|
||||||
|
if (availableSkillIds.isEmpty) {
|
||||||
|
availableSkillIds.addAll(SkillData.defaultSkillIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedSkill = skillService.selectAutoSkill(
|
||||||
|
player: playerStats,
|
||||||
|
monster: monsterStats,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
availableSkillIds: availableSkillIds,
|
||||||
);
|
);
|
||||||
monsterStats = attackResult.updatedDefender;
|
|
||||||
totalDamageDealt += attackResult.result.damage;
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
|
// 공격 스킬 사용
|
||||||
|
final skillResult = skillService.useAttackSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerStats,
|
||||||
|
monster: monsterStats,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
playerStats = skillResult.updatedPlayer;
|
||||||
|
monsterStats = skillResult.updatedMonster;
|
||||||
|
totalDamageDealt += skillResult.result.damage;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
|
// 회복 스킬 사용
|
||||||
|
final skillResult = skillService.useHealSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerStats,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
playerStats = skillResult.updatedPlayer;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
|
// 버프 스킬 사용
|
||||||
|
final skillResult = skillService.useBuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerStats,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
playerStats = skillResult.updatedPlayer;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
} else {
|
||||||
|
// 일반 공격
|
||||||
|
final attackResult = calculator.playerAttackMonster(
|
||||||
|
attacker: playerStats,
|
||||||
|
defender: monsterStats,
|
||||||
|
);
|
||||||
|
monsterStats = attackResult.updatedDefender;
|
||||||
|
totalDamageDealt += attackResult.result.damage;
|
||||||
|
}
|
||||||
|
|
||||||
playerAccumulator -= playerStats.attackDelayMs;
|
playerAccumulator -= playerStats.attackDelayMs;
|
||||||
turnsElapsed++;
|
turnsElapsed++;
|
||||||
}
|
}
|
||||||
@@ -841,15 +933,18 @@ class ProgressService {
|
|||||||
// 전투 종료 체크
|
// 전투 종료 체크
|
||||||
final isActive = playerStats.isAlive && monsterStats.isAlive;
|
final isActive = playerStats.isAlive && monsterStats.isAlive;
|
||||||
|
|
||||||
return combat.copyWith(
|
return (
|
||||||
playerStats: playerStats,
|
combat: combat.copyWith(
|
||||||
monsterStats: monsterStats,
|
playerStats: playerStats,
|
||||||
playerAttackAccumulatorMs: playerAccumulator,
|
monsterStats: monsterStats,
|
||||||
monsterAttackAccumulatorMs: monsterAccumulator,
|
playerAttackAccumulatorMs: playerAccumulator,
|
||||||
totalDamageDealt: totalDamageDealt,
|
monsterAttackAccumulatorMs: monsterAccumulator,
|
||||||
totalDamageTaken: totalDamageTaken,
|
totalDamageDealt: totalDamageDealt,
|
||||||
turnsElapsed: turnsElapsed,
|
totalDamageTaken: totalDamageTaken,
|
||||||
isActive: isActive,
|
turnsElapsed: turnsElapsed,
|
||||||
|
isActive: isActive,
|
||||||
|
),
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
345
lib/src/core/engine/skill_service.dart
Normal file
345
lib/src/core/engine/skill_service.dart
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import 'package:askiineverdie/data/skill_data.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 스킬 시스템 서비스
|
||||||
|
///
|
||||||
|
/// 스킬 사용, 쿨타임 관리, MP 관리, 자동 스킬 선택 등을 담당
|
||||||
|
class SkillService {
|
||||||
|
const SkillService({required this.rng});
|
||||||
|
|
||||||
|
final DeterministicRandom rng;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스킬 사용 가능 여부 확인
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 스킬 사용 가능 여부 확인
|
||||||
|
SkillFailReason? canUseSkill({
|
||||||
|
required Skill skill,
|
||||||
|
required int currentMp,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
}) {
|
||||||
|
// MP 체크
|
||||||
|
if (currentMp < skill.mpCost) {
|
||||||
|
return SkillFailReason.notEnoughMp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 쿨타임 체크
|
||||||
|
final skillState = skillSystem.getSkillState(skill.id);
|
||||||
|
if (skillState != null && !skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||||||
|
return SkillFailReason.onCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // 사용 가능
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스킬 사용
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 공격 스킬 사용
|
||||||
|
///
|
||||||
|
/// Returns: (결과, 업데이트된 플레이어 스탯, 업데이트된 몬스터 스탯)
|
||||||
|
({
|
||||||
|
SkillUseResult result,
|
||||||
|
CombatStats updatedPlayer,
|
||||||
|
MonsterCombatStats updatedMonster,
|
||||||
|
SkillSystemState updatedSkillSystem,
|
||||||
|
}) useAttackSkill({
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats player,
|
||||||
|
required MonsterCombatStats monster,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
}) {
|
||||||
|
// 기본 데미지 계산
|
||||||
|
final baseDamage = player.atk * skill.damageMultiplier;
|
||||||
|
|
||||||
|
// 버프 효과 적용
|
||||||
|
final buffMods = skillSystem.totalBuffModifiers;
|
||||||
|
final buffedDamage = baseDamage * (1 + buffMods.atkMod);
|
||||||
|
|
||||||
|
// 적 방어력 감소 적용
|
||||||
|
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
||||||
|
|
||||||
|
// 최종 데미지 계산 (방어력 감산)
|
||||||
|
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5).round().clamp(1, 9999);
|
||||||
|
|
||||||
|
// 몬스터에 데미지 적용
|
||||||
|
var updatedMonster = monster.applyDamage(finalDamage);
|
||||||
|
|
||||||
|
// 자해 데미지 적용
|
||||||
|
var updatedPlayer = player;
|
||||||
|
if (skill.selfDamagePercent > 0) {
|
||||||
|
final selfDamage = (player.hpMax * skill.selfDamagePercent).round();
|
||||||
|
updatedPlayer = player.applyDamage(selfDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP 소모
|
||||||
|
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
||||||
|
|
||||||
|
// 스킬 상태 업데이트 (쿨타임 시작)
|
||||||
|
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
result: SkillUseResult(
|
||||||
|
skill: skill,
|
||||||
|
success: true,
|
||||||
|
damage: finalDamage,
|
||||||
|
),
|
||||||
|
updatedPlayer: updatedPlayer,
|
||||||
|
updatedMonster: updatedMonster,
|
||||||
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회복 스킬 사용
|
||||||
|
({
|
||||||
|
SkillUseResult result,
|
||||||
|
CombatStats updatedPlayer,
|
||||||
|
SkillSystemState updatedSkillSystem,
|
||||||
|
}) useHealSkill({
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats player,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
}) {
|
||||||
|
// 회복량 계산
|
||||||
|
int healAmount = skill.healAmount;
|
||||||
|
if (skill.healPercent > 0) {
|
||||||
|
healAmount += (player.hpMax * skill.healPercent).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HP 회복
|
||||||
|
var updatedPlayer = player.applyHeal(healAmount);
|
||||||
|
|
||||||
|
// MP 소모
|
||||||
|
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
||||||
|
|
||||||
|
// 스킬 상태 업데이트
|
||||||
|
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
result: SkillUseResult(
|
||||||
|
skill: skill,
|
||||||
|
success: true,
|
||||||
|
healedAmount: healAmount,
|
||||||
|
),
|
||||||
|
updatedPlayer: updatedPlayer,
|
||||||
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 버프 스킬 사용
|
||||||
|
({
|
||||||
|
SkillUseResult result,
|
||||||
|
CombatStats updatedPlayer,
|
||||||
|
SkillSystemState updatedSkillSystem,
|
||||||
|
}) useBuffSkill({
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats player,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
}) {
|
||||||
|
if (skill.buff == null) {
|
||||||
|
return (
|
||||||
|
result: SkillUseResult.failed(skill, SkillFailReason.invalidState),
|
||||||
|
updatedPlayer: player,
|
||||||
|
updatedSkillSystem: skillSystem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 버프 적용
|
||||||
|
final newBuff = ActiveBuff(
|
||||||
|
effect: skill.buff!,
|
||||||
|
startedMs: skillSystem.elapsedMs,
|
||||||
|
sourceSkillId: skill.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 기존 같은 버프 제거 후 새 버프 추가
|
||||||
|
final updatedBuffs = skillSystem.activeBuffs
|
||||||
|
.where((b) => b.effect.id != skill.buff!.id)
|
||||||
|
.toList()
|
||||||
|
..add(newBuff);
|
||||||
|
|
||||||
|
// MP 소모
|
||||||
|
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||||||
|
|
||||||
|
// 스킬 상태 업데이트
|
||||||
|
var updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||||
|
updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
result: SkillUseResult(
|
||||||
|
skill: skill,
|
||||||
|
success: true,
|
||||||
|
appliedBuff: newBuff,
|
||||||
|
),
|
||||||
|
updatedPlayer: updatedPlayer,
|
||||||
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 자동 스킬 선택
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 전투 중 자동 스킬 선택
|
||||||
|
///
|
||||||
|
/// 우선순위:
|
||||||
|
/// 1. HP < 30% → 회복 스킬
|
||||||
|
/// 2. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
|
||||||
|
/// 3. 일반 전투 → MP 효율이 좋은 스킬
|
||||||
|
/// 4. MP < 20% → null (일반 공격)
|
||||||
|
Skill? selectAutoSkill({
|
||||||
|
required CombatStats player,
|
||||||
|
required MonsterCombatStats monster,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required List<String> availableSkillIds,
|
||||||
|
}) {
|
||||||
|
final currentMp = player.mpCurrent;
|
||||||
|
final mpRatio = player.mpRatio;
|
||||||
|
final hpRatio = player.hpRatio;
|
||||||
|
|
||||||
|
// MP 20% 미만이면 일반 공격
|
||||||
|
if (mpRatio < 0.2) return null;
|
||||||
|
|
||||||
|
// 사용 가능한 스킬 필터링
|
||||||
|
final availableSkills = availableSkillIds
|
||||||
|
.map((id) => SkillData.getSkillById(id))
|
||||||
|
.whereType<Skill>()
|
||||||
|
.where((skill) => canUseSkill(
|
||||||
|
skill: skill,
|
||||||
|
currentMp: currentMp,
|
||||||
|
skillSystem: skillSystem,
|
||||||
|
) ==
|
||||||
|
null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (availableSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
// HP < 30% → 회복 스킬 우선
|
||||||
|
if (hpRatio < 0.3) {
|
||||||
|
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
||||||
|
if (healSkill != null) return healSkill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보스전 판단 (몬스터 레벨이 높음)
|
||||||
|
final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5;
|
||||||
|
|
||||||
|
if (isBossFight) {
|
||||||
|
// 가장 강력한 공격 스킬
|
||||||
|
return _findStrongestAttackSkill(availableSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 전투 → MP 효율 좋은 스킬
|
||||||
|
return _findEfficientAttackSkill(availableSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 좋은 회복 스킬 찾기
|
||||||
|
Skill? _findBestHealSkill(List<Skill> skills, int currentMp) {
|
||||||
|
final healSkills = skills
|
||||||
|
.where((s) => s.isHeal && s.mpCost <= currentMp)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (healSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
// 회복량 기준 정렬 (% 회복 > 고정 회복)
|
||||||
|
healSkills.sort((a, b) {
|
||||||
|
final aValue = a.healPercent * 100 + a.healAmount;
|
||||||
|
final bValue = b.healPercent * 100 + b.healAmount;
|
||||||
|
return bValue.compareTo(aValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return healSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 강력한 공격 스킬 찾기
|
||||||
|
Skill? _findStrongestAttackSkill(List<Skill> skills) {
|
||||||
|
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||||
|
if (attackSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
attackSkills.sort((a, b) => b.damageMultiplier.compareTo(a.damageMultiplier));
|
||||||
|
return attackSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MP 효율 좋은 공격 스킬 찾기
|
||||||
|
Skill? _findEfficientAttackSkill(List<Skill> skills) {
|
||||||
|
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||||
|
if (attackSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
attackSkills.sort((a, b) => b.mpEfficiency.compareTo(a.mpEfficiency));
|
||||||
|
return attackSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MP 회복
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// MP 자연 회복
|
||||||
|
///
|
||||||
|
/// [elapsedMs] 경과 시간 (밀리초)
|
||||||
|
/// [isInCombat] 전투 중 여부
|
||||||
|
/// [wis] 지혜 스탯 (회복 속도 보정)
|
||||||
|
int calculateMpRegen({
|
||||||
|
required int elapsedMs,
|
||||||
|
required bool isInCombat,
|
||||||
|
required int wis,
|
||||||
|
}) {
|
||||||
|
if (isInCombat) {
|
||||||
|
// 전투 중: WIS에 비례한 느린 회복 (500ms당 1 + WIS/20)
|
||||||
|
final regenPerTick = 1 + wis ~/ 20;
|
||||||
|
return (elapsedMs ~/ 500) * regenPerTick;
|
||||||
|
} else {
|
||||||
|
// 비전투: 50ms당 1 회복
|
||||||
|
return elapsedMs ~/ 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 버프 관리
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 만료된 버프 제거
|
||||||
|
SkillSystemState cleanupExpiredBuffs(SkillSystemState state) {
|
||||||
|
final activeBuffs = state.activeBuffs
|
||||||
|
.where((b) => !b.isExpired(state.elapsedMs))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return state.copyWith(activeBuffs: activeBuffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 유틸리티
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 스킬 쿨타임 업데이트
|
||||||
|
SkillSystemState _updateSkillCooldown(SkillSystemState state, String skillId) {
|
||||||
|
final skillStates = List<SkillState>.from(state.skillStates);
|
||||||
|
|
||||||
|
// 기존 상태 찾기
|
||||||
|
final existingIndex = skillStates.indexWhere((s) => s.skillId == skillId);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// 기존 상태 업데이트
|
||||||
|
skillStates[existingIndex] = skillStates[existingIndex].copyWith(
|
||||||
|
lastUsedMs: state.elapsedMs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 새 상태 추가
|
||||||
|
skillStates.add(SkillState(
|
||||||
|
skillId: skillId,
|
||||||
|
lastUsedMs: state.elapsedMs,
|
||||||
|
rank: 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.copyWith(skillStates: skillStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스킬 시스템 시간 업데이트
|
||||||
|
SkillSystemState updateElapsedTime(SkillSystemState state, int deltaMs) {
|
||||||
|
return state.copyWith(elapsedMs: state.elapsedMs + deltaMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:askiineverdie/src/core/model/combat_state.dart';
|
|||||||
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
/// Minimal skeletal state to mirror Progress Quest structures.
|
/// Minimal skeletal state to mirror Progress Quest structures.
|
||||||
@@ -20,6 +21,7 @@ class GameState {
|
|||||||
SpellBook? spellBook,
|
SpellBook? spellBook,
|
||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
|
SkillSystemState? skillSystem,
|
||||||
}) : rng = DeterministicRandom.clone(rng),
|
}) : rng = DeterministicRandom.clone(rng),
|
||||||
traits = traits ?? Traits.empty(),
|
traits = traits ?? Traits.empty(),
|
||||||
stats = stats ?? Stats.empty(),
|
stats = stats ?? Stats.empty(),
|
||||||
@@ -27,7 +29,8 @@ class GameState {
|
|||||||
equipment = equipment ?? Equipment.empty(),
|
equipment = equipment ?? Equipment.empty(),
|
||||||
spellBook = spellBook ?? SpellBook.empty(),
|
spellBook = spellBook ?? SpellBook.empty(),
|
||||||
progress = progress ?? ProgressState.empty(),
|
progress = progress ?? ProgressState.empty(),
|
||||||
queue = queue ?? QueueState.empty();
|
queue = queue ?? QueueState.empty(),
|
||||||
|
skillSystem = skillSystem ?? SkillSystemState.empty();
|
||||||
|
|
||||||
factory GameState.withSeed({
|
factory GameState.withSeed({
|
||||||
required int seed,
|
required int seed,
|
||||||
@@ -38,6 +41,7 @@ class GameState {
|
|||||||
SpellBook? spellBook,
|
SpellBook? spellBook,
|
||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
|
SkillSystemState? skillSystem,
|
||||||
}) {
|
}) {
|
||||||
return GameState(
|
return GameState(
|
||||||
rng: DeterministicRandom(seed),
|
rng: DeterministicRandom(seed),
|
||||||
@@ -48,6 +52,7 @@ class GameState {
|
|||||||
spellBook: spellBook,
|
spellBook: spellBook,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
queue: queue,
|
queue: queue,
|
||||||
|
skillSystem: skillSystem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +65,9 @@ class GameState {
|
|||||||
final ProgressState progress;
|
final ProgressState progress;
|
||||||
final QueueState queue;
|
final QueueState queue;
|
||||||
|
|
||||||
|
/// 스킬 시스템 상태 (Phase 3)
|
||||||
|
final SkillSystemState skillSystem;
|
||||||
|
|
||||||
GameState copyWith({
|
GameState copyWith({
|
||||||
DeterministicRandom? rng,
|
DeterministicRandom? rng,
|
||||||
Traits? traits,
|
Traits? traits,
|
||||||
@@ -69,6 +77,7 @@ class GameState {
|
|||||||
SpellBook? spellBook,
|
SpellBook? spellBook,
|
||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
|
SkillSystemState? skillSystem,
|
||||||
}) {
|
}) {
|
||||||
return GameState(
|
return GameState(
|
||||||
rng: rng ?? DeterministicRandom.clone(this.rng),
|
rng: rng ?? DeterministicRandom.clone(this.rng),
|
||||||
@@ -79,6 +88,76 @@ class GameState {
|
|||||||
spellBook: spellBook ?? this.spellBook,
|
spellBook: spellBook ?? this.spellBook,
|
||||||
progress: progress ?? this.progress,
|
progress: progress ?? this.progress,
|
||||||
queue: queue ?? this.queue,
|
queue: queue ?? this.queue,
|
||||||
|
skillSystem: skillSystem ?? this.skillSystem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스킬 시스템 상태 (Phase 3)
|
||||||
|
///
|
||||||
|
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리
|
||||||
|
class SkillSystemState {
|
||||||
|
const SkillSystemState({
|
||||||
|
required this.skillStates,
|
||||||
|
required this.activeBuffs,
|
||||||
|
required this.elapsedMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 스킬별 쿨타임 상태
|
||||||
|
final List<SkillState> skillStates;
|
||||||
|
|
||||||
|
/// 현재 활성화된 버프 목록
|
||||||
|
final List<ActiveBuff> activeBuffs;
|
||||||
|
|
||||||
|
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
||||||
|
final int elapsedMs;
|
||||||
|
|
||||||
|
factory SkillSystemState.empty() => const SkillSystemState(
|
||||||
|
skillStates: [],
|
||||||
|
activeBuffs: [],
|
||||||
|
elapsedMs: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 특정 스킬 상태 가져오기
|
||||||
|
SkillState? getSkillState(String skillId) {
|
||||||
|
for (final state in skillStates) {
|
||||||
|
if (state.skillId == skillId) return state;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 버프 효과 합산 (동일 버프는 중복 적용 안 됨)
|
||||||
|
({double atkMod, double defMod, double criMod, double evasionMod}) get totalBuffModifiers {
|
||||||
|
double atkMod = 0;
|
||||||
|
double defMod = 0;
|
||||||
|
double criMod = 0;
|
||||||
|
double evasionMod = 0;
|
||||||
|
|
||||||
|
final seenBuffIds = <String>{};
|
||||||
|
for (final buff in activeBuffs) {
|
||||||
|
if (seenBuffIds.contains(buff.effect.id)) continue;
|
||||||
|
seenBuffIds.add(buff.effect.id);
|
||||||
|
|
||||||
|
if (!buff.isExpired(elapsedMs)) {
|
||||||
|
atkMod += buff.effect.atkModifier;
|
||||||
|
defMod += buff.effect.defModifier;
|
||||||
|
criMod += buff.effect.criRateModifier;
|
||||||
|
evasionMod += buff.effect.evasionModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (atkMod: atkMod, defMod: defMod, criMod: criMod, evasionMod: evasionMod);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkillSystemState copyWith({
|
||||||
|
List<SkillState>? skillStates,
|
||||||
|
List<ActiveBuff>? activeBuffs,
|
||||||
|
int? elapsedMs,
|
||||||
|
}) {
|
||||||
|
return SkillSystemState(
|
||||||
|
skillStates: skillStates ?? this.skillStates,
|
||||||
|
activeBuffs: activeBuffs ?? this.activeBuffs,
|
||||||
|
elapsedMs: elapsedMs ?? this.elapsedMs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
267
lib/src/core/model/skill.dart
Normal file
267
lib/src/core/model/skill.dart
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/// 스킬 타입
|
||||||
|
enum SkillType {
|
||||||
|
/// 공격 스킬
|
||||||
|
attack,
|
||||||
|
|
||||||
|
/// 회복 스킬
|
||||||
|
heal,
|
||||||
|
|
||||||
|
/// 버프 스킬
|
||||||
|
buff,
|
||||||
|
|
||||||
|
/// 디버프 스킬
|
||||||
|
debuff,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 버프 효과
|
||||||
|
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.damageMultiplier = 1.0,
|
||||||
|
this.healAmount = 0,
|
||||||
|
this.healPercent = 0.0,
|
||||||
|
this.buff,
|
||||||
|
this.selfDamagePercent = 0.0,
|
||||||
|
this.targetDefReduction = 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 스킬 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;
|
||||||
|
|
||||||
|
/// 공격 스킬 여부
|
||||||
|
bool get isAttack => type == SkillType.attack;
|
||||||
|
|
||||||
|
/// 회복 스킬 여부
|
||||||
|
bool get isHeal => type == SkillType.heal;
|
||||||
|
|
||||||
|
/// 버프 스킬 여부
|
||||||
|
bool get isBuff => type == SkillType.buff;
|
||||||
|
|
||||||
|
/// 디버프 스킬 여부
|
||||||
|
bool get isDebuff => type == SkillType.debuff;
|
||||||
|
|
||||||
|
/// MP 효율 (데미지 당 MP 비용)
|
||||||
|
double get mpEfficiency {
|
||||||
|
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
||||||
|
return damageMultiplier / mpCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스킬 사용 상태 (쿨타임 추적)
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// 스킬 없음
|
||||||
|
skillNotFound,
|
||||||
|
|
||||||
|
/// 사용 불가 상태
|
||||||
|
invalidState,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user