feat(engine): GCD 체크 및 스킬 자동 장착 로직 구현
SkillService: - canUseSkill()에 GCD 체크 추가 - selectAutoSkill() 확률 조정 (70% 일반공격, 30% 스킬) - 버프/디버프 조건 강화 (HP>80%, 활성 효과 체크) ProgressService: - 스킬 사용 후 GCD 시작 로직 추가 - 장착된 스킬 슬롯에서 사용 가능 스킬 조회 - 비전투 태스크 시 currentCombat 초기화 GameMutations: - winSpell()에서 스펠 획득 시 전투 스킬 자동 장착
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:asciineverdie/data/skill_data.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/item_service.dart';
|
import 'package:asciineverdie/src/core/engine/item_service.dart';
|
||||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
@@ -43,12 +44,17 @@ class GameMutations {
|
|||||||
return state.copyWith(rng: state.rng, stats: updatedStats);
|
return state.copyWith(rng: state.rng, stats: updatedStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 스펠 획득 (원본 WinSpell)
|
||||||
|
///
|
||||||
|
/// 스펠북에 추가하고, 전투용 스킬 슬롯에도 자동으로 장착 시도.
|
||||||
|
/// 슬롯이 가득 찬 경우 기존 스킬보다 강할 때만 교체됨.
|
||||||
GameState winSpell(GameState state, int wisdom, int level) {
|
GameState winSpell(GameState state, int wisdom, int level) {
|
||||||
final result = pq_logic.winSpell(config, state.rng, wisdom, level);
|
final result = pq_logic.winSpell(config, state.rng, wisdom, level);
|
||||||
final parts = result.split('|');
|
final parts = result.split('|');
|
||||||
final name = parts[0];
|
final name = parts[0];
|
||||||
final rank = parts.length > 1 ? parts[1] : 'I';
|
final rank = parts.length > 1 ? parts[1] : 'I';
|
||||||
|
|
||||||
|
// 스펠북 업데이트
|
||||||
final skills = [...state.skillBook.skills];
|
final skills = [...state.skillBook.skills];
|
||||||
final index = skills.indexWhere((s) => s.name == name);
|
final index = skills.indexWhere((s) => s.name == name);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -57,9 +63,20 @@ class GameMutations {
|
|||||||
skills.add(SkillEntry(name: name, rank: rank));
|
skills.add(SkillEntry(name: name, rank: rank));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 전투 스킬 슬롯에 추가 시도
|
||||||
|
var skillSystem = state.skillSystem;
|
||||||
|
final combatSkill = SkillData.getSkillBySpellName(name);
|
||||||
|
if (combatSkill != null) {
|
||||||
|
final addResult = skillSystem.equippedSkills.tryAddSkill(combatSkill);
|
||||||
|
if (addResult.success) {
|
||||||
|
skillSystem = skillSystem.copyWith(equippedSkills: addResult.slots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
rng: state.rng,
|
rng: state.rng,
|
||||||
skillBook: state.skillBook.copyWith(skills: skills),
|
skillBook: state.skillBook.copyWith(skills: skills),
|
||||||
|
skillSystem: skillSystem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ class ProgressService {
|
|||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.market,
|
type: TaskType.market,
|
||||||
),
|
),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
@@ -536,6 +537,7 @@ class ProgressService {
|
|||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.buying,
|
type: TaskType.buying,
|
||||||
),
|
),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
@@ -551,6 +553,7 @@ class ProgressService {
|
|||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.neutral,
|
type: TaskType.neutral,
|
||||||
),
|
),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
@@ -672,7 +675,7 @@ class ProgressService {
|
|||||||
type: TaskType.kill,
|
type: TaskType.kill,
|
||||||
monsterBaseName: monsterResult.baseName,
|
monsterBaseName: monsterResult.baseName,
|
||||||
monsterPart: monsterResult.part,
|
monsterPart: monsterResult.part,
|
||||||
monsterLevel: monsterResult.level,
|
monsterLevel: effectiveMonsterLevel,
|
||||||
monsterGrade: monsterResult.grade,
|
monsterGrade: monsterResult.grade,
|
||||||
),
|
),
|
||||||
currentCombat: combatState,
|
currentCombat: combatState,
|
||||||
@@ -1205,6 +1208,7 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
final progress = taskResult.progress.copyWith(
|
final progress = taskResult.progress.copyWith(
|
||||||
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
state: state.copyWith(
|
state: state.copyWith(
|
||||||
@@ -1358,11 +1362,11 @@ class ProgressService {
|
|||||||
|
|
||||||
// 플레이어 공격 체크
|
// 플레이어 공격 체크
|
||||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
// SkillBook에서 사용 가능한 스킬 ID 목록 조회
|
// 장착된 스킬 슬롯에서 사용 가능한 스킬 ID 목록 조회
|
||||||
var availableSkillIds = skillService.getAvailableSkillIdsFromSkillBook(
|
var availableSkillIds = state.skillSystem.equippedSkills.allSkills
|
||||||
state.skillBook,
|
.map((s) => s.id)
|
||||||
);
|
.toList();
|
||||||
// SkillBook에 스킬이 없으면 기본 스킬 사용
|
// 장착된 스킬이 없으면 기본 스킬 사용
|
||||||
if (availableSkillIds.isEmpty) {
|
if (availableSkillIds.isEmpty) {
|
||||||
availableSkillIds = SkillData.defaultSkillIds;
|
availableSkillIds = SkillData.defaultSkillIds;
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1399,9 @@ class ProgressService {
|
|||||||
totalDamageDealt += skillResult.result.damage;
|
totalDamageDealt += skillResult.result.damage;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 스킬 공격 이벤트 생성
|
// 스킬 공격 이벤트 생성
|
||||||
newEvents.add(
|
newEvents.add(
|
||||||
CombatEvent.playerSkill(
|
CombatEvent.playerSkill(
|
||||||
@@ -1417,6 +1424,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// DOT 효과 추가
|
// DOT 효과 추가
|
||||||
if (skillResult.dotEffect != null) {
|
if (skillResult.dotEffect != null) {
|
||||||
activeDoTs.add(skillResult.dotEffect!);
|
activeDoTs.add(skillResult.dotEffect!);
|
||||||
@@ -1442,6 +1452,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 회복 이벤트 생성
|
// 회복 이벤트 생성
|
||||||
newEvents.add(
|
newEvents.add(
|
||||||
CombatEvent.playerHeal(
|
CombatEvent.playerHeal(
|
||||||
@@ -1460,6 +1473,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 버프 이벤트 생성
|
// 버프 이벤트 생성
|
||||||
newEvents.add(
|
newEvents.add(
|
||||||
CombatEvent.playerBuff(
|
CombatEvent.playerBuff(
|
||||||
@@ -1478,6 +1494,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 디버프 효과 추가 (기존 같은 디버프 제거 후)
|
// 디버프 효과 추가 (기존 같은 디버프 제거 후)
|
||||||
if (skillResult.debuffEffect != null) {
|
if (skillResult.debuffEffect != null) {
|
||||||
activeDebuffs =
|
activeDebuffs =
|
||||||
@@ -1708,8 +1727,10 @@ class ProgressService {
|
|||||||
if (equippedNonWeaponSlots.isNotEmpty) {
|
if (equippedNonWeaponSlots.isNotEmpty) {
|
||||||
lostCount = 1;
|
lostCount = 1;
|
||||||
// 랜덤하게 1개 슬롯 선택
|
// 랜덤하게 1개 슬롯 선택
|
||||||
final sacrificeIndex = equippedNonWeaponSlots[
|
final sacrificeIndex =
|
||||||
state.rng.nextInt(equippedNonWeaponSlots.length)];
|
equippedNonWeaponSlots[state.rng.nextInt(
|
||||||
|
equippedNonWeaponSlots.length,
|
||||||
|
)];
|
||||||
final slot = EquipmentSlot.values[sacrificeIndex];
|
final slot = EquipmentSlot.values[sacrificeIndex];
|
||||||
|
|
||||||
// 해당 슬롯을 빈 장비로 교체
|
// 해당 슬롯을 빈 장비로 교체
|
||||||
@@ -1733,7 +1754,8 @@ class ProgressService {
|
|||||||
|
|
||||||
// 보스전 사망 시 5분 레벨링 모드 진입
|
// 보스전 사망 시 5분 레벨링 모드 진입
|
||||||
final bossLevelingEndTime = isBossDeath
|
final bossLevelingEndTime = isBossDeath
|
||||||
? DateTime.now().millisecondsSinceEpoch + (5 * 60 * 1000) // 5분
|
? DateTime.now().millisecondsSinceEpoch +
|
||||||
|
(5 * 60 * 1000) // 5분
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 전투 상태 초기화 및 사망 횟수 증가
|
// 전투 상태 초기화 및 사망 횟수 증가
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class SkillService {
|
|||||||
required int currentMp,
|
required int currentMp,
|
||||||
required SkillSystemState skillSystem,
|
required SkillSystemState skillSystem,
|
||||||
}) {
|
}) {
|
||||||
|
// GCD 체크 (글로벌 쿨타임 1500ms)
|
||||||
|
if (skillSystem.isGlobalCooldownActive) {
|
||||||
|
return SkillFailReason.onGlobalCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
// MP 체크
|
// MP 체크
|
||||||
if (currentMp < skill.mpCost) {
|
if (currentMp < skill.mpCost) {
|
||||||
return SkillFailReason.notEnoughMp;
|
return SkillFailReason.notEnoughMp;
|
||||||
@@ -297,13 +302,14 @@ class SkillService {
|
|||||||
/// 전투 중 자동 스킬 선택
|
/// 전투 중 자동 스킬 선택
|
||||||
///
|
///
|
||||||
/// 우선순위:
|
/// 우선순위:
|
||||||
/// 1. HP < 30% → 회복 스킬
|
/// 1. HP < 30% → 회복 스킬 (최우선)
|
||||||
/// 2. HP > 70% & MP > 50% → 버프 스킬 (안전할 때)
|
/// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투)
|
||||||
/// 3. 몬스터 HP > 70% & 활성 디버프 없음 → 디버프 스킬
|
/// 3. 30% 확률로 스킬 사용:
|
||||||
/// 4. 몬스터 HP > 50% & DOT 없음 → DOT 스킬 (장기전 유리)
|
/// - 버프: HP > 80% & MP > 60% & 활성 버프 없음
|
||||||
/// 5. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
|
/// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음
|
||||||
/// 6. 일반 전투 → MP 효율이 좋은 스킬
|
/// - DOT: 몬스터 HP > 60% & 활성 DOT 없음
|
||||||
/// 7. MP < 20% → null (일반 공격)
|
/// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬
|
||||||
|
/// 4. MP < 20% → 일반 공격
|
||||||
Skill? selectAutoSkill({
|
Skill? selectAutoSkill({
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required MonsterCombatStats monster,
|
required MonsterCombatStats monster,
|
||||||
@@ -336,39 +342,49 @@ class SkillService {
|
|||||||
|
|
||||||
if (availableSkills.isEmpty) return null;
|
if (availableSkills.isEmpty) return null;
|
||||||
|
|
||||||
// HP < 30% → 회복 스킬 우선
|
// HP < 30% → 회복 스킬 최우선 (생존)
|
||||||
if (hpRatio < 0.3) {
|
if (hpRatio < 0.3) {
|
||||||
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
||||||
if (healSkill != null) return healSkill;
|
if (healSkill != null) return healSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HP > 70% & MP > 50% → 버프 스킬 (안전할 때)
|
// 70% 확률로 일반 공격 (스킬은 특별한 상황에서만)
|
||||||
if (hpRatio > 0.7 && mpRatio > 0.5) {
|
final useNormalAttack = rng.nextInt(100) < 70;
|
||||||
final buffSkill = _findBestBuffSkill(availableSkills, currentMp);
|
if (useNormalAttack) return null;
|
||||||
if (buffSkill != null) return buffSkill;
|
|
||||||
|
// === 아래부터 30% 확률로 스킬 사용 ===
|
||||||
|
|
||||||
|
// 버프: HP > 80% & MP > 60% (매우 안전할 때만)
|
||||||
|
// 활성 버프가 있으면 건너뜀 (중복 방지)
|
||||||
|
if (hpRatio > 0.8 && mpRatio > 0.6) {
|
||||||
|
final hasActiveBuff = skillSystem.activeBuffs.isNotEmpty;
|
||||||
|
if (!hasActiveBuff) {
|
||||||
|
final buffSkill = _findBestBuffSkill(availableSkills, currentMp);
|
||||||
|
if (buffSkill != null) return buffSkill;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 몬스터 HP > 70% & 활성 디버프 없음 → 디버프 스킬
|
// 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반)
|
||||||
if (monster.hpRatio > 0.7 && activeDebuffs.isEmpty) {
|
if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) {
|
||||||
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
|
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
|
||||||
if (debuffSkill != null) return debuffSkill;
|
if (debuffSkill != null) return debuffSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 몬스터 HP > 50% & 활성 DOT 없음 → DOT 스킬 사용
|
// DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리)
|
||||||
if (monster.hpRatio > 0.5 && activeDoTs.isEmpty) {
|
if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) {
|
||||||
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
|
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
|
||||||
if (dotSkill != null) return dotSkill;
|
if (dotSkill != null) return dotSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 보스전 판단 (몬스터 레벨이 높음)
|
// 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상)
|
||||||
final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5;
|
final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5;
|
||||||
|
|
||||||
if (isBossFight) {
|
if (isBossFight) {
|
||||||
// 가장 강력한 공격 스킬
|
// 가장 강력한 공격 스킬
|
||||||
return _findStrongestAttackSkill(availableSkills);
|
return _findStrongestAttackSkill(availableSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 전투 → MP 효율 좋은 스킬
|
// 일반 전투 → MP 효율 좋은 공격 스킬
|
||||||
return _findEfficientAttackSkill(availableSkills);
|
return _findEfficientAttackSkill(availableSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user