From 02d4d1d3978f5941e706f99e15ed38d16515d526 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 14 Jan 2026 23:04:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(engine):=20GCD=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=ED=82=AC=20=EC=9E=90=EB=8F=99=20=EC=9E=A5?= =?UTF-8?q?=EC=B0=A9=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SkillService: - canUseSkill()에 GCD 체크 추가 - selectAutoSkill() 확률 조정 (70% 일반공격, 30% 스킬) - 버프/디버프 조건 강화 (HP>80%, 활성 효과 체크) ProgressService: - 스킬 사용 후 GCD 시작 로직 추가 - 장착된 스킬 슬롯에서 사용 가능 스킬 조회 - 비전투 태스크 시 currentCombat 초기화 GameMutations: - winSpell()에서 스펠 획득 시 전투 스킬 자동 장착 --- lib/src/core/engine/game_mutations.dart | 17 +++++++ lib/src/core/engine/progress_service.dart | 40 +++++++++++++---- lib/src/core/engine/skill_service.dart | 54 +++++++++++++++-------- 3 files changed, 83 insertions(+), 28 deletions(-) diff --git a/lib/src/core/engine/game_mutations.dart b/lib/src/core/engine/game_mutations.dart index 3665505..e7a7756 100644 --- a/lib/src/core/engine/game_mutations.dart +++ b/lib/src/core/engine/game_mutations.dart @@ -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/model/equipment_slot.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; @@ -43,12 +44,17 @@ class GameMutations { return state.copyWith(rng: state.rng, stats: updatedStats); } + /// 스펠 획득 (원본 WinSpell) + /// + /// 스펠북에 추가하고, 전투용 스킬 슬롯에도 자동으로 장착 시도. + /// 슬롯이 가득 찬 경우 기존 스킬보다 강할 때만 교체됨. GameState winSpell(GameState state, int wisdom, int level) { final result = pq_logic.winSpell(config, state.rng, wisdom, level); final parts = result.split('|'); final name = parts[0]; final rank = parts.length > 1 ? parts[1] : 'I'; + // 스펠북 업데이트 final skills = [...state.skillBook.skills]; final index = skills.indexWhere((s) => s.name == name); if (index >= 0) { @@ -57,9 +63,20 @@ class GameMutations { 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( rng: state.rng, skillBook: state.skillBook.copyWith(skills: skills), + skillSystem: skillSystem, ); } diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index fcb2e24..39f09a6 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -512,6 +512,7 @@ class ProgressService { caption: taskResult.caption, type: TaskType.market, ), + currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 ); return (progress: progress, queue: queue); } @@ -536,6 +537,7 @@ class ProgressService { caption: taskResult.caption, type: TaskType.buying, ), + currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 ); return (progress: progress, queue: queue); } @@ -551,6 +553,7 @@ class ProgressService { caption: taskResult.caption, type: TaskType.neutral, ), + currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 ); return (progress: progress, queue: queue); } @@ -672,7 +675,7 @@ class ProgressService { type: TaskType.kill, monsterBaseName: monsterResult.baseName, monsterPart: monsterResult.part, - monsterLevel: monsterResult.level, + monsterLevel: effectiveMonsterLevel, monsterGrade: monsterResult.grade, ), currentCombat: combatState, @@ -1205,6 +1208,7 @@ class ProgressService { ); final progress = taskResult.progress.copyWith( currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell), + currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 ); return ( state: state.copyWith( @@ -1358,11 +1362,11 @@ class ProgressService { // 플레이어 공격 체크 if (playerAccumulator >= playerStats.attackDelayMs) { - // SkillBook에서 사용 가능한 스킬 ID 목록 조회 - var availableSkillIds = skillService.getAvailableSkillIdsFromSkillBook( - state.skillBook, - ); - // SkillBook에 스킬이 없으면 기본 스킬 사용 + // 장착된 스킬 슬롯에서 사용 가능한 스킬 ID 목록 조회 + var availableSkillIds = state.skillSystem.equippedSkills.allSkills + .map((s) => s.id) + .toList(); + // 장착된 스킬이 없으면 기본 스킬 사용 if (availableSkillIds.isEmpty) { availableSkillIds = SkillData.defaultSkillIds; } @@ -1395,6 +1399,9 @@ class ProgressService { totalDamageDealt += skillResult.result.damage; updatedSkillSystem = skillResult.updatedSkillSystem; + // GCD 시작 (스킬 사용 후) + updatedSkillSystem = updatedSkillSystem.startGlobalCooldown(); + // 스킬 공격 이벤트 생성 newEvents.add( CombatEvent.playerSkill( @@ -1417,6 +1424,9 @@ class ProgressService { playerStats = skillResult.updatedPlayer; updatedSkillSystem = skillResult.updatedSkillSystem; + // GCD 시작 (스킬 사용 후) + updatedSkillSystem = updatedSkillSystem.startGlobalCooldown(); + // DOT 효과 추가 if (skillResult.dotEffect != null) { activeDoTs.add(skillResult.dotEffect!); @@ -1442,6 +1452,9 @@ class ProgressService { playerStats = skillResult.updatedPlayer; updatedSkillSystem = skillResult.updatedSkillSystem; + // GCD 시작 (스킬 사용 후) + updatedSkillSystem = updatedSkillSystem.startGlobalCooldown(); + // 회복 이벤트 생성 newEvents.add( CombatEvent.playerHeal( @@ -1460,6 +1473,9 @@ class ProgressService { playerStats = skillResult.updatedPlayer; updatedSkillSystem = skillResult.updatedSkillSystem; + // GCD 시작 (스킬 사용 후) + updatedSkillSystem = updatedSkillSystem.startGlobalCooldown(); + // 버프 이벤트 생성 newEvents.add( CombatEvent.playerBuff( @@ -1478,6 +1494,9 @@ class ProgressService { playerStats = skillResult.updatedPlayer; updatedSkillSystem = skillResult.updatedSkillSystem; + // GCD 시작 (스킬 사용 후) + updatedSkillSystem = updatedSkillSystem.startGlobalCooldown(); + // 디버프 효과 추가 (기존 같은 디버프 제거 후) if (skillResult.debuffEffect != null) { activeDebuffs = @@ -1708,8 +1727,10 @@ class ProgressService { if (equippedNonWeaponSlots.isNotEmpty) { lostCount = 1; // 랜덤하게 1개 슬롯 선택 - final sacrificeIndex = equippedNonWeaponSlots[ - state.rng.nextInt(equippedNonWeaponSlots.length)]; + final sacrificeIndex = + equippedNonWeaponSlots[state.rng.nextInt( + equippedNonWeaponSlots.length, + )]; final slot = EquipmentSlot.values[sacrificeIndex]; // 해당 슬롯을 빈 장비로 교체 @@ -1733,7 +1754,8 @@ class ProgressService { // 보스전 사망 시 5분 레벨링 모드 진입 final bossLevelingEndTime = isBossDeath - ? DateTime.now().millisecondsSinceEpoch + (5 * 60 * 1000) // 5분 + ? DateTime.now().millisecondsSinceEpoch + + (5 * 60 * 1000) // 5분 : null; // 전투 상태 초기화 및 사망 횟수 증가 diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart index f2c00da..8837644 100644 --- a/lib/src/core/engine/skill_service.dart +++ b/lib/src/core/engine/skill_service.dart @@ -24,6 +24,11 @@ class SkillService { required int currentMp, required SkillSystemState skillSystem, }) { + // GCD 체크 (글로벌 쿨타임 1500ms) + if (skillSystem.isGlobalCooldownActive) { + return SkillFailReason.onGlobalCooldown; + } + // MP 체크 if (currentMp < skill.mpCost) { return SkillFailReason.notEnoughMp; @@ -297,13 +302,14 @@ class SkillService { /// 전투 중 자동 스킬 선택 /// /// 우선순위: - /// 1. HP < 30% → 회복 스킬 - /// 2. HP > 70% & MP > 50% → 버프 스킬 (안전할 때) - /// 3. 몬스터 HP > 70% & 활성 디버프 없음 → 디버프 스킬 - /// 4. 몬스터 HP > 50% & DOT 없음 → DOT 스킬 (장기전 유리) - /// 5. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬 - /// 6. 일반 전투 → MP 효율이 좋은 스킬 - /// 7. MP < 20% → null (일반 공격) + /// 1. HP < 30% → 회복 스킬 (최우선) + /// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투) + /// 3. 30% 확률로 스킬 사용: + /// - 버프: HP > 80% & MP > 60% & 활성 버프 없음 + /// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음 + /// - DOT: 몬스터 HP > 60% & 활성 DOT 없음 + /// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬 + /// 4. MP < 20% → 일반 공격 Skill? selectAutoSkill({ required CombatStats player, required MonsterCombatStats monster, @@ -336,39 +342,49 @@ class SkillService { if (availableSkills.isEmpty) return null; - // HP < 30% → 회복 스킬 우선 + // HP < 30% → 회복 스킬 최우선 (생존) if (hpRatio < 0.3) { final healSkill = _findBestHealSkill(availableSkills, currentMp); if (healSkill != null) return healSkill; } - // HP > 70% & MP > 50% → 버프 스킬 (안전할 때) - if (hpRatio > 0.7 && mpRatio > 0.5) { - final buffSkill = _findBestBuffSkill(availableSkills, currentMp); - if (buffSkill != null) return buffSkill; + // 70% 확률로 일반 공격 (스킬은 특별한 상황에서만) + final useNormalAttack = rng.nextInt(100) < 70; + if (useNormalAttack) return null; + + // === 아래부터 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% & 활성 디버프 없음 → 디버프 스킬 - if (monster.hpRatio > 0.7 && activeDebuffs.isEmpty) { + // 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반) + if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) { final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp); if (debuffSkill != null) return debuffSkill; } - // 몬스터 HP > 50% & 활성 DOT 없음 → DOT 스킬 사용 - if (monster.hpRatio > 0.5 && activeDoTs.isEmpty) { + // DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리) + if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) { final dotSkill = _findBestDotSkill(availableSkills, currentMp); if (dotSkill != null) return dotSkill; } - // 보스전 판단 (몬스터 레벨이 높음) - final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5; + // 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상) + final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5; if (isBossFight) { // 가장 강력한 공격 스킬 return _findStrongestAttackSkill(availableSkills); } - // 일반 전투 → MP 효율 좋은 스킬 + // 일반 전투 → MP 효율 좋은 공격 스킬 return _findEfficientAttackSkill(availableSkills); }