refactor(engine): 전투 및 진행 로직 개선

- combat_tick_service: 전투 틱 처리 로직 확장
- progress_service: 진행 상태 처리 개선
- skill_service: 스킬 시스템 업데이트
- potion_service: 포션 처리 로직 수정
This commit is contained in:
JiWoong Sul
2026-01-19 19:40:42 +09:00
parent 109b4eb678
commit 5cccd28b77
4 changed files with 104 additions and 7 deletions

View File

@@ -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<ActiveBuff> activeDebuffs,
int totalDamageDealt,
List<CombatEvent> 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 = <CombatEvent>[];
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
);
}

View File

@@ -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; // 실제 회복량
}

View File

@@ -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) {

View File

@@ -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);