From 5cccd28b7795d85b1b46e2884d6f9fc8893150bf Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 19 Jan 2026 19:40:42 +0900 Subject: [PATCH] =?UTF-8?q?refactor(engine):=20=EC=A0=84=ED=88=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=84=ED=96=89=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - combat_tick_service: 전투 틱 처리 로직 확장 - progress_service: 진행 상태 처리 개선 - skill_service: 스킬 시스템 업데이트 - potion_service: 포션 처리 로직 수정 --- lib/src/core/engine/combat_tick_service.dart | 71 +++++++++++++++++++- lib/src/core/engine/potion_service.dart | 10 ++- lib/src/core/engine/progress_service.dart | 24 ++++++- lib/src/core/engine/skill_service.dart | 6 ++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/lib/src/core/engine/combat_tick_service.dart b/lib/src/core/engine/combat_tick_service.dart index afbb5c3..cd7a7b0 100644 --- a/lib/src/core/engine/combat_tick_service.dart +++ b/lib/src/core/engine/combat_tick_service.dart @@ -1,5 +1,7 @@ +import 'package:asciineverdie/data/class_data.dart'; import 'package:asciineverdie/data/skill_data.dart'; import 'package:asciineverdie/src/core/engine/combat_calculator.dart'; +import 'package:asciineverdie/src/core/model/class_traits.dart'; import 'package:asciineverdie/src/core/engine/potion_service.dart'; import 'package:asciineverdie/src/core/engine/skill_service.dart'; import 'package:asciineverdie/src/core/model/combat_event.dart'; @@ -94,6 +96,17 @@ class CombatTickService { totalDamageDealt = dotResult.totalDamageDealt; newEvents.addAll(dotResult.events); + // 클래스 패시브 조회 (healingBonus, firstStrikeBonus, multiAttack) + final klass = ClassData.findById(state.traits.classId); + final healingBonus = + klass?.getPassiveValue(ClassPassiveType.healingBonus) ?? 0.0; + final healingMultiplier = 1.0 + healingBonus; + final firstStrikeBonus = + klass?.getPassiveValue(ClassPassiveType.firstStrikeBonus) ?? 0.0; + final hasMultiAttack = + klass?.hasPassive(ClassPassiveType.multiAttack) ?? false; + var isFirstPlayerAttack = combat.isFirstPlayerAttack; + // 긴급 물약 자동 사용 (HP < 30% 또는 MP < 50%) final potionResult = _tryEmergencyPotion( playerStats: playerStats, @@ -102,6 +115,7 @@ class CombatTickService { playerLevel: state.traits.level, timestamp: timestamp, potionService: potionService, + healingMultiplier: healingMultiplier, ); if (potionResult != null) { playerStats = potionResult.playerStats; @@ -123,6 +137,10 @@ class CombatTickService { timestamp: timestamp, calculator: calculator, skillService: skillService, + isFirstPlayerAttack: isFirstPlayerAttack, + firstStrikeBonus: firstStrikeBonus > 0 ? firstStrikeBonus : 1.0, + hasMultiAttack: hasMultiAttack, + healingMultiplier: healingMultiplier, ); playerStats = attackResult.playerStats; @@ -132,6 +150,7 @@ class CombatTickService { activeDebuffs = attackResult.activeDebuffs; totalDamageDealt = attackResult.totalDamageDealt; newEvents.addAll(attackResult.events); + isFirstPlayerAttack = attackResult.isFirstPlayerAttack; playerAccumulator -= playerStats.attackDelayMs; turnsElapsed++; @@ -178,6 +197,7 @@ class CombatTickService { activeDoTs: activeDoTs, lastPotionUsedMs: lastPotionUsedMs, activeDebuffs: activeDebuffs, + isFirstPlayerAttack: isFirstPlayerAttack, ), skillSystem: updatedSkillSystem, potionInventory: updatedPotionInventory, @@ -259,6 +279,7 @@ class CombatTickService { required int playerLevel, required int timestamp, required PotionService potionService, + double healingMultiplier = 1.0, }) { // 글로벌 쿨타임 체크 if (timestamp - lastPotionUsedMs < PotionService.globalPotionCooldownMs) { @@ -281,6 +302,7 @@ class CombatTickService { maxHp: playerStats.hpMax, currentMp: playerStats.mpCurrent, maxMp: playerStats.mpMax, + healingMultiplier: healingMultiplier, ); if (result.success) { @@ -316,6 +338,7 @@ class CombatTickService { maxHp: playerStats.hpMax, currentMp: playerStats.mpCurrent, maxMp: playerStats.mpMax, + healingMultiplier: healingMultiplier, ); if (result.success) { @@ -347,6 +370,7 @@ class CombatTickService { List activeDebuffs, int totalDamageDealt, List events, + bool isFirstPlayerAttack, }) _processPlayerAttack({ required GameState state, required CombatStats playerStats, @@ -358,6 +382,10 @@ class CombatTickService { required int timestamp, required CombatCalculator calculator, required SkillService skillService, + required bool isFirstPlayerAttack, + required double firstStrikeBonus, + required bool hasMultiAttack, + double healingMultiplier = 1.0, }) { final events = []; var newPlayerStats = playerStats; @@ -442,6 +470,7 @@ class CombatTickService { skill: selectedSkill, player: newPlayerStats, skillSystem: newSkillSystem, + healingMultiplier: healingMultiplier, ); newPlayerStats = skillResult.updatedPlayer; newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown(); @@ -499,7 +528,22 @@ class CombatTickService { defender: newMonsterStats, ); newMonsterStats = attackResult.updatedDefender; - newTotalDamageDealt += attackResult.result.damage; + + // 첫 공격 배율 적용 (예: Pointer Assassin 1.5배) + var damage = attackResult.result.damage; + if (isFirstPlayerAttack && firstStrikeBonus > 1.0) { + damage = (damage * firstStrikeBonus).round(); + // 첫 공격 배율이 적용된 데미지로 몬스터 HP 재계산 + final extraDamage = damage - attackResult.result.damage; + if (extraDamage > 0) { + final newHp = (newMonsterStats.hpCurrent - extraDamage).clamp( + 0, + newMonsterStats.hpMax, + ); + newMonsterStats = newMonsterStats.copyWith(hpCurrent: newHp); + } + } + newTotalDamageDealt += damage; final result = attackResult.result; if (result.isEvaded) { @@ -513,13 +557,35 @@ class CombatTickService { events.add( CombatEvent.playerAttack( timestamp: timestamp, - damage: result.damage, + damage: damage, targetName: newMonsterStats.name, isCritical: result.isCritical, attackDelayMs: newPlayerStats.attackDelayMs, ), ); } + + // 연속 공격 (Refactor Monk 패시브) - 30% 확률로 추가 공격 + if (hasMultiAttack && newMonsterStats.isAlive && rng.nextDouble() < 0.3) { + final extraAttack = calculator.playerAttackMonster( + attacker: newPlayerStats, + defender: newMonsterStats, + ); + newMonsterStats = extraAttack.updatedDefender; + newTotalDamageDealt += extraAttack.result.damage; + + if (!extraAttack.result.isEvaded) { + events.add( + CombatEvent.playerAttack( + timestamp: timestamp, + damage: extraAttack.result.damage, + targetName: newMonsterStats.name, + isCritical: extraAttack.result.isCritical, + attackDelayMs: newPlayerStats.attackDelayMs, + ), + ); + } + } } return ( @@ -530,6 +596,7 @@ class CombatTickService { activeDebuffs: newActiveBuffs, totalDamageDealt: newTotalDamageDealt, events: events, + isFirstPlayerAttack: false, // 첫 공격 이후에는 false ); } diff --git a/lib/src/core/engine/potion_service.dart b/lib/src/core/engine/potion_service.dart index 3069fb9..8faecbd 100644 --- a/lib/src/core/engine/potion_service.dart +++ b/lib/src/core/engine/potion_service.dart @@ -52,6 +52,7 @@ class PotionService { /// [maxHp] 최대 HP /// [currentMp] 현재 MP /// [maxMp] 최대 MP + /// [healingMultiplier] 회복력 배율 (기본 1.0, 클래스 패시브 적용) PotionUseResult usePotion({ required String potionId, required PotionInventory inventory, @@ -59,6 +60,7 @@ class PotionService { required int maxHp, required int currentMp, required int maxMp, + double healingMultiplier = 1.0, }) { final (canUse, failReason) = canUsePotion(potionId, inventory); if (!canUse) { @@ -71,11 +73,15 @@ class PotionService { int newMp = currentMp; if (potion.isHpPotion) { - healedAmount = potion.calculateHeal(maxHp); + // 회복력 보너스 적용 (예: Debugger Paladin +10%) + final baseHeal = potion.calculateHeal(maxHp); + healedAmount = (baseHeal * healingMultiplier).round(); newHp = (currentHp + healedAmount).clamp(0, maxHp); healedAmount = newHp - currentHp; // 실제 회복량 } else if (potion.isMpPotion) { - healedAmount = potion.calculateHeal(maxMp); + // MP 물약에도 회복력 보너스 적용 + final baseHeal = potion.calculateHeal(maxMp); + healedAmount = (baseHeal * healingMultiplier).round(); newMp = (currentMp + healedAmount).clamp(0, maxMp); healedAmount = newMp - currentMp; // 실제 회복량 } diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index 33c9ec0..fc43010 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -1,6 +1,9 @@ import 'dart:math' as math; +import 'package:asciineverdie/data/class_data.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; +import 'package:asciineverdie/data/race_data.dart'; +import 'package:asciineverdie/src/core/model/class_traits.dart'; import 'package:asciineverdie/src/core/animation/monster_size.dart'; import 'package:asciineverdie/src/core/engine/act_progression_service.dart'; import 'package:asciineverdie/src/core/engine/combat_calculator.dart'; @@ -273,10 +276,21 @@ class ProgressService { final remainingHp = combat.playerStats.hpCurrent; final maxHp = combat.playerStats.hpMax; - // 전투 승리 시 HP 회복 (50% + CON/2) + // 전투 승리 시 HP 회복 (50% + CON/2 + 클래스 패시브) // 아이들 게임 특성상 전투 사이 HP가 회복되어야 지속 플레이 가능 final conBonus = nextState.stats.con ~/ 2; - final healAmount = (maxHp * 0.5).round() + conBonus; + var healAmount = (maxHp * 0.5).round() + conBonus; + + // 클래스 패시브: 전투 후 HP 회복 (예: Garbage Collector +5%) + final klass = ClassData.findById(nextState.traits.classId); + if (klass != null) { + final postCombatHealRate = + klass.getPassiveValue(ClassPassiveType.postCombatHeal); + if (postCombatHealRate > 0) { + healAmount += (maxHp * postCombatHealRate).round(); + } + } + final newHp = (remainingHp + healAmount).clamp(0, maxHp); nextState = nextState.copyWith( @@ -384,7 +398,11 @@ class ProgressService { // Gain XP / level up (몬스터 경험치 기반) // 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음 if (gain && nextState.traits.level < 100 && monsterExpReward > 0) { - final newExpPos = progress.exp.position + monsterExpReward; + // 종족 경험치 배율 적용 (예: Byte Human +5%, Callback Seraph +3%) + final race = RaceData.findById(nextState.traits.raceId); + final expMultiplier = race?.expMultiplier ?? 1.0; + final adjustedExp = (monsterExpReward * expMultiplier).round(); + final newExpPos = progress.exp.position + adjustedExp; // 레벨업 체크 (경험치가 필요량 이상일 때) if (newExpPos >= progress.exp.max) { diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart index d12b876..48b667d 100644 --- a/lib/src/core/engine/skill_service.dart +++ b/lib/src/core/engine/skill_service.dart @@ -112,6 +112,8 @@ class SkillService { } /// 회복 스킬 사용 + /// + /// [healingMultiplier] 회복력 배율 (기본 1.0, 클래스 패시브 적용) ({ SkillUseResult result, CombatStats updatedPlayer, @@ -121,6 +123,7 @@ class SkillService { required Skill skill, required CombatStats player, required SkillSystemState skillSystem, + double healingMultiplier = 1.0, }) { // 회복량 계산 int healAmount = skill.healAmount; @@ -128,6 +131,9 @@ class SkillService { healAmount += (player.hpMax * skill.healPercent).round(); } + // 회복력 보너스 적용 (예: Debugger Paladin +10%, Exception Handler +15%) + healAmount = (healAmount * healingMultiplier).round(); + // HP 회복 var updatedPlayer = player.applyHeal(healAmount);