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/data/skill_data.dart';
import 'package:asciineverdie/src/core/engine/combat_calculator.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/potion_service.dart';
import 'package:asciineverdie/src/core/engine/skill_service.dart'; import 'package:asciineverdie/src/core/engine/skill_service.dart';
import 'package:asciineverdie/src/core/model/combat_event.dart'; import 'package:asciineverdie/src/core/model/combat_event.dart';
@@ -94,6 +96,17 @@ class CombatTickService {
totalDamageDealt = dotResult.totalDamageDealt; totalDamageDealt = dotResult.totalDamageDealt;
newEvents.addAll(dotResult.events); 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%) // 긴급 물약 자동 사용 (HP < 30% 또는 MP < 50%)
final potionResult = _tryEmergencyPotion( final potionResult = _tryEmergencyPotion(
playerStats: playerStats, playerStats: playerStats,
@@ -102,6 +115,7 @@ class CombatTickService {
playerLevel: state.traits.level, playerLevel: state.traits.level,
timestamp: timestamp, timestamp: timestamp,
potionService: potionService, potionService: potionService,
healingMultiplier: healingMultiplier,
); );
if (potionResult != null) { if (potionResult != null) {
playerStats = potionResult.playerStats; playerStats = potionResult.playerStats;
@@ -123,6 +137,10 @@ class CombatTickService {
timestamp: timestamp, timestamp: timestamp,
calculator: calculator, calculator: calculator,
skillService: skillService, skillService: skillService,
isFirstPlayerAttack: isFirstPlayerAttack,
firstStrikeBonus: firstStrikeBonus > 0 ? firstStrikeBonus : 1.0,
hasMultiAttack: hasMultiAttack,
healingMultiplier: healingMultiplier,
); );
playerStats = attackResult.playerStats; playerStats = attackResult.playerStats;
@@ -132,6 +150,7 @@ class CombatTickService {
activeDebuffs = attackResult.activeDebuffs; activeDebuffs = attackResult.activeDebuffs;
totalDamageDealt = attackResult.totalDamageDealt; totalDamageDealt = attackResult.totalDamageDealt;
newEvents.addAll(attackResult.events); newEvents.addAll(attackResult.events);
isFirstPlayerAttack = attackResult.isFirstPlayerAttack;
playerAccumulator -= playerStats.attackDelayMs; playerAccumulator -= playerStats.attackDelayMs;
turnsElapsed++; turnsElapsed++;
@@ -178,6 +197,7 @@ class CombatTickService {
activeDoTs: activeDoTs, activeDoTs: activeDoTs,
lastPotionUsedMs: lastPotionUsedMs, lastPotionUsedMs: lastPotionUsedMs,
activeDebuffs: activeDebuffs, activeDebuffs: activeDebuffs,
isFirstPlayerAttack: isFirstPlayerAttack,
), ),
skillSystem: updatedSkillSystem, skillSystem: updatedSkillSystem,
potionInventory: updatedPotionInventory, potionInventory: updatedPotionInventory,
@@ -259,6 +279,7 @@ class CombatTickService {
required int playerLevel, required int playerLevel,
required int timestamp, required int timestamp,
required PotionService potionService, required PotionService potionService,
double healingMultiplier = 1.0,
}) { }) {
// 글로벌 쿨타임 체크 // 글로벌 쿨타임 체크
if (timestamp - lastPotionUsedMs < PotionService.globalPotionCooldownMs) { if (timestamp - lastPotionUsedMs < PotionService.globalPotionCooldownMs) {
@@ -281,6 +302,7 @@ class CombatTickService {
maxHp: playerStats.hpMax, maxHp: playerStats.hpMax,
currentMp: playerStats.mpCurrent, currentMp: playerStats.mpCurrent,
maxMp: playerStats.mpMax, maxMp: playerStats.mpMax,
healingMultiplier: healingMultiplier,
); );
if (result.success) { if (result.success) {
@@ -316,6 +338,7 @@ class CombatTickService {
maxHp: playerStats.hpMax, maxHp: playerStats.hpMax,
currentMp: playerStats.mpCurrent, currentMp: playerStats.mpCurrent,
maxMp: playerStats.mpMax, maxMp: playerStats.mpMax,
healingMultiplier: healingMultiplier,
); );
if (result.success) { if (result.success) {
@@ -347,6 +370,7 @@ class CombatTickService {
List<ActiveBuff> activeDebuffs, List<ActiveBuff> activeDebuffs,
int totalDamageDealt, int totalDamageDealt,
List<CombatEvent> events, List<CombatEvent> events,
bool isFirstPlayerAttack,
}) _processPlayerAttack({ }) _processPlayerAttack({
required GameState state, required GameState state,
required CombatStats playerStats, required CombatStats playerStats,
@@ -358,6 +382,10 @@ class CombatTickService {
required int timestamp, required int timestamp,
required CombatCalculator calculator, required CombatCalculator calculator,
required SkillService skillService, required SkillService skillService,
required bool isFirstPlayerAttack,
required double firstStrikeBonus,
required bool hasMultiAttack,
double healingMultiplier = 1.0,
}) { }) {
final events = <CombatEvent>[]; final events = <CombatEvent>[];
var newPlayerStats = playerStats; var newPlayerStats = playerStats;
@@ -442,6 +470,7 @@ class CombatTickService {
skill: selectedSkill, skill: selectedSkill,
player: newPlayerStats, player: newPlayerStats,
skillSystem: newSkillSystem, skillSystem: newSkillSystem,
healingMultiplier: healingMultiplier,
); );
newPlayerStats = skillResult.updatedPlayer; newPlayerStats = skillResult.updatedPlayer;
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown(); newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
@@ -499,7 +528,22 @@ class CombatTickService {
defender: newMonsterStats, defender: newMonsterStats,
); );
newMonsterStats = attackResult.updatedDefender; 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; final result = attackResult.result;
if (result.isEvaded) { if (result.isEvaded) {
@@ -513,13 +557,35 @@ class CombatTickService {
events.add( events.add(
CombatEvent.playerAttack( CombatEvent.playerAttack(
timestamp: timestamp, timestamp: timestamp,
damage: result.damage, damage: damage,
targetName: newMonsterStats.name, targetName: newMonsterStats.name,
isCritical: result.isCritical, isCritical: result.isCritical,
attackDelayMs: newPlayerStats.attackDelayMs, 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 ( return (
@@ -530,6 +596,7 @@ class CombatTickService {
activeDebuffs: newActiveBuffs, activeDebuffs: newActiveBuffs,
totalDamageDealt: newTotalDamageDealt, totalDamageDealt: newTotalDamageDealt,
events: events, events: events,
isFirstPlayerAttack: false, // 첫 공격 이후에는 false
); );
} }

View File

@@ -52,6 +52,7 @@ class PotionService {
/// [maxHp] 최대 HP /// [maxHp] 최대 HP
/// [currentMp] 현재 MP /// [currentMp] 현재 MP
/// [maxMp] 최대 MP /// [maxMp] 최대 MP
/// [healingMultiplier] 회복력 배율 (기본 1.0, 클래스 패시브 적용)
PotionUseResult usePotion({ PotionUseResult usePotion({
required String potionId, required String potionId,
required PotionInventory inventory, required PotionInventory inventory,
@@ -59,6 +60,7 @@ class PotionService {
required int maxHp, required int maxHp,
required int currentMp, required int currentMp,
required int maxMp, required int maxMp,
double healingMultiplier = 1.0,
}) { }) {
final (canUse, failReason) = canUsePotion(potionId, inventory); final (canUse, failReason) = canUsePotion(potionId, inventory);
if (!canUse) { if (!canUse) {
@@ -71,11 +73,15 @@ class PotionService {
int newMp = currentMp; int newMp = currentMp;
if (potion.isHpPotion) { 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); newHp = (currentHp + healedAmount).clamp(0, maxHp);
healedAmount = newHp - currentHp; // 실제 회복량 healedAmount = newHp - currentHp; // 실제 회복량
} else if (potion.isMpPotion) { } else if (potion.isMpPotion) {
healedAmount = potion.calculateHeal(maxMp); // MP 물약에도 회복력 보너스 적용
final baseHeal = potion.calculateHeal(maxMp);
healedAmount = (baseHeal * healingMultiplier).round();
newMp = (currentMp + healedAmount).clamp(0, maxMp); newMp = (currentMp + healedAmount).clamp(0, maxMp);
healedAmount = newMp - currentMp; // 실제 회복량 healedAmount = newMp - currentMp; // 실제 회복량
} }

View File

@@ -1,6 +1,9 @@
import 'dart:math' as math; 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/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/animation/monster_size.dart';
import 'package:asciineverdie/src/core/engine/act_progression_service.dart'; import 'package:asciineverdie/src/core/engine/act_progression_service.dart';
import 'package:asciineverdie/src/core/engine/combat_calculator.dart'; import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
@@ -273,10 +276,21 @@ class ProgressService {
final remainingHp = combat.playerStats.hpCurrent; final remainingHp = combat.playerStats.hpCurrent;
final maxHp = combat.playerStats.hpMax; final maxHp = combat.playerStats.hpMax;
// 전투 승리 시 HP 회복 (50% + CON/2) // 전투 승리 시 HP 회복 (50% + CON/2 + 클래스 패시브)
// 아이들 게임 특성상 전투 사이 HP가 회복되어야 지속 플레이 가능 // 아이들 게임 특성상 전투 사이 HP가 회복되어야 지속 플레이 가능
final conBonus = nextState.stats.con ~/ 2; 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); final newHp = (remainingHp + healAmount).clamp(0, maxHp);
nextState = nextState.copyWith( nextState = nextState.copyWith(
@@ -384,7 +398,11 @@ class ProgressService {
// Gain XP / level up (몬스터 경험치 기반) // Gain XP / level up (몬스터 경험치 기반)
// 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음 // 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) { 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) { if (newExpPos >= progress.exp.max) {

View File

@@ -112,6 +112,8 @@ class SkillService {
} }
/// 회복 스킬 사용 /// 회복 스킬 사용
///
/// [healingMultiplier] 회복력 배율 (기본 1.0, 클래스 패시브 적용)
({ ({
SkillUseResult result, SkillUseResult result,
CombatStats updatedPlayer, CombatStats updatedPlayer,
@@ -121,6 +123,7 @@ class SkillService {
required Skill skill, required Skill skill,
required CombatStats player, required CombatStats player,
required SkillSystemState skillSystem, required SkillSystemState skillSystem,
double healingMultiplier = 1.0,
}) { }) {
// 회복량 계산 // 회복량 계산
int healAmount = skill.healAmount; int healAmount = skill.healAmount;
@@ -128,6 +131,9 @@ class SkillService {
healAmount += (player.hpMax * skill.healPercent).round(); healAmount += (player.hpMax * skill.healPercent).round();
} }
// 회복력 보너스 적용 (예: Debugger Paladin +10%, Exception Handler +15%)
healAmount = (healAmount * healingMultiplier).round();
// HP 회복 // HP 회복
var updatedPlayer = player.applyHeal(healAmount); var updatedPlayer = player.applyHeal(healAmount);