refactor(engine): 전투 및 진행 로직 개선
- combat_tick_service: 전투 틱 처리 로직 확장 - progress_service: 진행 상태 처리 개선 - skill_service: 스킬 시스템 업데이트 - potion_service: 포션 처리 로직 수정
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; // 실제 회복량
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user