refactor(engine): 전투 틱 로직을 CombatTickService로 분리
- ProgressService에서 전투 처리 로직 추출 - 스킬 자동 사용, DOT, 물약 사용 로직 포함 - CombatTickResult 결과 클래스 정의
This commit is contained in:
580
lib/src/core/engine/combat_tick_service.dart
Normal file
580
lib/src/core/engine/combat_tick_service.dart
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
import 'package:asciineverdie/data/skill_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/combat_calculator.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';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 전투 틱 처리 결과
|
||||||
|
class CombatTickResult {
|
||||||
|
const CombatTickResult({
|
||||||
|
required this.combat,
|
||||||
|
required this.skillSystem,
|
||||||
|
this.potionInventory,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CombatState combat;
|
||||||
|
final SkillSystemState skillSystem;
|
||||||
|
final PotionInventory? potionInventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 틱 처리 서비스
|
||||||
|
///
|
||||||
|
/// ProgressService에서 분리된 전투 로직 담당:
|
||||||
|
/// - 스킬 자동 사용
|
||||||
|
/// - DOT 처리
|
||||||
|
/// - 물약 자동 사용
|
||||||
|
/// - 플레이어/몬스터 공격 처리
|
||||||
|
class CombatTickService {
|
||||||
|
CombatTickService({required this.rng});
|
||||||
|
|
||||||
|
final DeterministicRandom rng;
|
||||||
|
|
||||||
|
/// 전투 틱 처리 (스킬 자동 사용, DOT, 물약 포함)
|
||||||
|
///
|
||||||
|
/// [state] 현재 게임 상태
|
||||||
|
/// [combat] 현재 전투 상태
|
||||||
|
/// [skillSystem] 스킬 시스템 상태
|
||||||
|
/// [elapsedMs] 경과 시간 (밀리초)
|
||||||
|
CombatTickResult processTick({
|
||||||
|
required GameState state,
|
||||||
|
required CombatState combat,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required int elapsedMs,
|
||||||
|
}) {
|
||||||
|
if (!combat.isActive || combat.isCombatOver) {
|
||||||
|
return CombatTickResult(
|
||||||
|
combat: combat,
|
||||||
|
skillSystem: skillSystem,
|
||||||
|
potionInventory: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
final skillService = SkillService(rng: rng);
|
||||||
|
final potionService = const PotionService();
|
||||||
|
var playerStats = combat.playerStats;
|
||||||
|
var monsterStats = combat.monsterStats;
|
||||||
|
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
|
||||||
|
var monsterAccumulator = combat.monsterAttackAccumulatorMs + elapsedMs;
|
||||||
|
var totalDamageDealt = combat.totalDamageDealt;
|
||||||
|
var totalDamageTaken = combat.totalDamageTaken;
|
||||||
|
var turnsElapsed = combat.turnsElapsed;
|
||||||
|
var updatedSkillSystem = skillSystem;
|
||||||
|
var activeDoTs = [...combat.activeDoTs];
|
||||||
|
var usedPotionTypes = {...combat.usedPotionTypes};
|
||||||
|
var activeDebuffs = [...combat.activeDebuffs];
|
||||||
|
PotionInventory? updatedPotionInventory;
|
||||||
|
|
||||||
|
// 새 전투 이벤트 수집
|
||||||
|
final newEvents = <CombatEvent>[];
|
||||||
|
final timestamp = updatedSkillSystem.elapsedMs;
|
||||||
|
|
||||||
|
// 만료된 디버프 정리
|
||||||
|
activeDebuffs = activeDebuffs
|
||||||
|
.where((debuff) => !debuff.isExpired(timestamp))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// DOT 틱 처리
|
||||||
|
final dotResult = _processDotTicks(
|
||||||
|
activeDoTs: activeDoTs,
|
||||||
|
monsterStats: monsterStats,
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
timestamp: timestamp,
|
||||||
|
totalDamageDealt: totalDamageDealt,
|
||||||
|
);
|
||||||
|
activeDoTs = dotResult.activeDoTs;
|
||||||
|
monsterStats = dotResult.monsterStats;
|
||||||
|
totalDamageDealt = dotResult.totalDamageDealt;
|
||||||
|
newEvents.addAll(dotResult.events);
|
||||||
|
|
||||||
|
// 긴급 물약 자동 사용 (HP < 30%)
|
||||||
|
final potionResult = _tryEmergencyPotion(
|
||||||
|
playerStats: playerStats,
|
||||||
|
potionInventory: state.potionInventory,
|
||||||
|
usedPotionTypes: usedPotionTypes,
|
||||||
|
playerLevel: state.traits.level,
|
||||||
|
timestamp: timestamp,
|
||||||
|
potionService: potionService,
|
||||||
|
);
|
||||||
|
if (potionResult != null) {
|
||||||
|
playerStats = potionResult.playerStats;
|
||||||
|
usedPotionTypes = potionResult.usedPotionTypes;
|
||||||
|
updatedPotionInventory = potionResult.potionInventory;
|
||||||
|
newEvents.addAll(potionResult.events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 플레이어 공격 체크
|
||||||
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
|
final attackResult = _processPlayerAttack(
|
||||||
|
state: state,
|
||||||
|
playerStats: playerStats,
|
||||||
|
monsterStats: monsterStats,
|
||||||
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
|
activeDoTs: activeDoTs,
|
||||||
|
activeDebuffs: activeDebuffs,
|
||||||
|
totalDamageDealt: totalDamageDealt,
|
||||||
|
timestamp: timestamp,
|
||||||
|
calculator: calculator,
|
||||||
|
skillService: skillService,
|
||||||
|
);
|
||||||
|
|
||||||
|
playerStats = attackResult.playerStats;
|
||||||
|
monsterStats = attackResult.monsterStats;
|
||||||
|
updatedSkillSystem = attackResult.skillSystem;
|
||||||
|
activeDoTs = attackResult.activeDoTs;
|
||||||
|
activeDebuffs = attackResult.activeDebuffs;
|
||||||
|
totalDamageDealt = attackResult.totalDamageDealt;
|
||||||
|
newEvents.addAll(attackResult.events);
|
||||||
|
|
||||||
|
playerAccumulator -= playerStats.attackDelayMs;
|
||||||
|
turnsElapsed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 몬스터가 살아있으면 반격
|
||||||
|
if (monsterStats.isAlive &&
|
||||||
|
monsterAccumulator >= monsterStats.attackDelayMs) {
|
||||||
|
final monsterAttackResult = _processMonsterAttack(
|
||||||
|
playerStats: playerStats,
|
||||||
|
monsterStats: monsterStats,
|
||||||
|
activeDebuffs: activeDebuffs,
|
||||||
|
totalDamageTaken: totalDamageTaken,
|
||||||
|
timestamp: timestamp,
|
||||||
|
calculator: calculator,
|
||||||
|
);
|
||||||
|
|
||||||
|
playerStats = monsterAttackResult.playerStats;
|
||||||
|
totalDamageTaken = monsterAttackResult.totalDamageTaken;
|
||||||
|
newEvents.addAll(monsterAttackResult.events);
|
||||||
|
monsterAccumulator -= monsterStats.attackDelayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전투 종료 체크
|
||||||
|
final isActive = playerStats.isAlive && monsterStats.isAlive;
|
||||||
|
|
||||||
|
// 기존 이벤트와 합쳐서 최대 10개 유지
|
||||||
|
final combinedEvents = [...combat.recentEvents, ...newEvents];
|
||||||
|
final recentEvents = combinedEvents.length > 10
|
||||||
|
? combinedEvents.sublist(combinedEvents.length - 10)
|
||||||
|
: combinedEvents;
|
||||||
|
|
||||||
|
return CombatTickResult(
|
||||||
|
combat: combat.copyWith(
|
||||||
|
playerStats: playerStats,
|
||||||
|
monsterStats: monsterStats,
|
||||||
|
playerAttackAccumulatorMs: playerAccumulator,
|
||||||
|
monsterAttackAccumulatorMs: monsterAccumulator,
|
||||||
|
totalDamageDealt: totalDamageDealt,
|
||||||
|
totalDamageTaken: totalDamageTaken,
|
||||||
|
turnsElapsed: turnsElapsed,
|
||||||
|
isActive: isActive,
|
||||||
|
recentEvents: recentEvents,
|
||||||
|
activeDoTs: activeDoTs,
|
||||||
|
usedPotionTypes: usedPotionTypes,
|
||||||
|
activeDebuffs: activeDebuffs,
|
||||||
|
),
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
potionInventory: updatedPotionInventory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DOT 틱 처리
|
||||||
|
({
|
||||||
|
List<DotEffect> activeDoTs,
|
||||||
|
MonsterCombatStats monsterStats,
|
||||||
|
int totalDamageDealt,
|
||||||
|
List<CombatEvent> events,
|
||||||
|
}) _processDotTicks({
|
||||||
|
required List<DotEffect> activeDoTs,
|
||||||
|
required MonsterCombatStats monsterStats,
|
||||||
|
required int elapsedMs,
|
||||||
|
required int timestamp,
|
||||||
|
required int totalDamageDealt,
|
||||||
|
}) {
|
||||||
|
var dotDamageThisTick = 0;
|
||||||
|
final updatedDoTs = <DotEffect>[];
|
||||||
|
final events = <CombatEvent>[];
|
||||||
|
var updatedMonster = monsterStats;
|
||||||
|
|
||||||
|
for (final dot in activeDoTs) {
|
||||||
|
final (updatedDot, ticksTriggered) = dot.tick(elapsedMs);
|
||||||
|
|
||||||
|
if (ticksTriggered > 0) {
|
||||||
|
final damage = dot.damagePerTick * ticksTriggered;
|
||||||
|
dotDamageThisTick += damage;
|
||||||
|
|
||||||
|
// DOT 데미지 이벤트 생성
|
||||||
|
final dotSkillName =
|
||||||
|
SkillData.getSkillById(dot.skillId)?.name ?? dot.skillId;
|
||||||
|
events.add(
|
||||||
|
CombatEvent.dotTick(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: dotSkillName,
|
||||||
|
damage: damage,
|
||||||
|
targetName: updatedMonster.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 만료되지 않은 DOT만 유지
|
||||||
|
if (updatedDot.isActive) {
|
||||||
|
updatedDoTs.add(updatedDot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOT 데미지 적용
|
||||||
|
if (dotDamageThisTick > 0 && updatedMonster.isAlive) {
|
||||||
|
final newMonsterHp = (updatedMonster.hpCurrent - dotDamageThisTick).clamp(
|
||||||
|
0,
|
||||||
|
updatedMonster.hpMax,
|
||||||
|
);
|
||||||
|
updatedMonster = updatedMonster.copyWith(hpCurrent: newMonsterHp);
|
||||||
|
totalDamageDealt += dotDamageThisTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
activeDoTs: updatedDoTs,
|
||||||
|
monsterStats: updatedMonster,
|
||||||
|
totalDamageDealt: totalDamageDealt,
|
||||||
|
events: events,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 긴급 물약 자동 사용
|
||||||
|
({
|
||||||
|
CombatStats playerStats,
|
||||||
|
Set<PotionType> usedPotionTypes,
|
||||||
|
PotionInventory potionInventory,
|
||||||
|
List<CombatEvent> events,
|
||||||
|
})? _tryEmergencyPotion({
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required PotionInventory potionInventory,
|
||||||
|
required Set<PotionType> usedPotionTypes,
|
||||||
|
required int playerLevel,
|
||||||
|
required int timestamp,
|
||||||
|
required PotionService potionService,
|
||||||
|
}) {
|
||||||
|
final hpRatio = playerStats.hpCurrent / playerStats.hpMax;
|
||||||
|
if (hpRatio > PotionService.emergencyHpThreshold) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final emergencyPotion = potionService.selectEmergencyHpPotion(
|
||||||
|
currentHp: playerStats.hpCurrent,
|
||||||
|
maxHp: playerStats.hpMax,
|
||||||
|
inventory: potionInventory,
|
||||||
|
playerLevel: playerLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emergencyPotion == null || usedPotionTypes.contains(PotionType.hp)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = potionService.usePotion(
|
||||||
|
potionId: emergencyPotion.id,
|
||||||
|
inventory: potionInventory,
|
||||||
|
currentHp: playerStats.hpCurrent,
|
||||||
|
maxHp: playerStats.hpMax,
|
||||||
|
currentMp: playerStats.mpCurrent,
|
||||||
|
maxMp: playerStats.mpMax,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
playerStats: playerStats.copyWith(hpCurrent: result.newHp),
|
||||||
|
usedPotionTypes: {...usedPotionTypes, PotionType.hp},
|
||||||
|
potionInventory: result.newInventory!,
|
||||||
|
events: [
|
||||||
|
CombatEvent.playerPotion(
|
||||||
|
timestamp: timestamp,
|
||||||
|
potionName: emergencyPotion.name,
|
||||||
|
healAmount: result.healedAmount,
|
||||||
|
isHp: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 플레이어 공격 처리
|
||||||
|
({
|
||||||
|
CombatStats playerStats,
|
||||||
|
MonsterCombatStats monsterStats,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
List<DotEffect> activeDoTs,
|
||||||
|
List<ActiveBuff> activeDebuffs,
|
||||||
|
int totalDamageDealt,
|
||||||
|
List<CombatEvent> events,
|
||||||
|
}) _processPlayerAttack({
|
||||||
|
required GameState state,
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required MonsterCombatStats monsterStats,
|
||||||
|
required SkillSystemState updatedSkillSystem,
|
||||||
|
required List<DotEffect> activeDoTs,
|
||||||
|
required List<ActiveBuff> activeDebuffs,
|
||||||
|
required int totalDamageDealt,
|
||||||
|
required int timestamp,
|
||||||
|
required CombatCalculator calculator,
|
||||||
|
required SkillService skillService,
|
||||||
|
}) {
|
||||||
|
final events = <CombatEvent>[];
|
||||||
|
var newPlayerStats = playerStats;
|
||||||
|
var newMonsterStats = monsterStats;
|
||||||
|
var newSkillSystem = updatedSkillSystem;
|
||||||
|
var newActiveDoTs = [...activeDoTs];
|
||||||
|
var newActiveBuffs = [...activeDebuffs];
|
||||||
|
var newTotalDamageDealt = totalDamageDealt;
|
||||||
|
|
||||||
|
// 장착된 스킬 슬롯에서 사용 가능한 스킬 ID 목록 조회
|
||||||
|
var availableSkillIds = state.skillSystem.equippedSkills.allSkills
|
||||||
|
.map((s) => s.id)
|
||||||
|
.toList();
|
||||||
|
// 장착된 스킬이 없으면 기본 스킬 사용
|
||||||
|
if (availableSkillIds.isEmpty) {
|
||||||
|
availableSkillIds = SkillData.defaultSkillIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedSkill = skillService.selectAutoSkill(
|
||||||
|
player: newPlayerStats,
|
||||||
|
monster: newMonsterStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
availableSkillIds: availableSkillIds,
|
||||||
|
activeDoTs: newActiveDoTs,
|
||||||
|
activeDebuffs: newActiveBuffs,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
|
// 스킬 랭크 조회
|
||||||
|
final skillRank = skillService.getSkillRankFromSkillBook(
|
||||||
|
state.skillBook,
|
||||||
|
selectedSkill.id,
|
||||||
|
);
|
||||||
|
// 랭크 스케일링 적용된 공격 스킬 사용
|
||||||
|
final skillResult = skillService.useAttackSkillWithRank(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: newPlayerStats,
|
||||||
|
monster: newMonsterStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
rank: skillRank,
|
||||||
|
);
|
||||||
|
newPlayerStats = skillResult.updatedPlayer;
|
||||||
|
newMonsterStats = skillResult.updatedMonster;
|
||||||
|
newTotalDamageDealt += skillResult.result.damage;
|
||||||
|
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerSkill(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: selectedSkill.name,
|
||||||
|
damage: skillResult.result.damage,
|
||||||
|
targetName: newMonsterStats.name,
|
||||||
|
attackDelayMs: newPlayerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||||
|
final skillResult = skillService.useDotSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
playerInt: state.stats.intelligence,
|
||||||
|
playerWis: state.stats.wis,
|
||||||
|
);
|
||||||
|
newPlayerStats = skillResult.updatedPlayer;
|
||||||
|
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
|
if (skillResult.dotEffect != null) {
|
||||||
|
newActiveDoTs.add(skillResult.dotEffect!);
|
||||||
|
}
|
||||||
|
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerSkill(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: selectedSkill.name,
|
||||||
|
damage: skillResult.result.damage,
|
||||||
|
targetName: newMonsterStats.name,
|
||||||
|
attackDelayMs: newPlayerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
|
final skillResult = skillService.useHealSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
);
|
||||||
|
newPlayerStats = skillResult.updatedPlayer;
|
||||||
|
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerHeal(
|
||||||
|
timestamp: timestamp,
|
||||||
|
healAmount: skillResult.result.healedAmount,
|
||||||
|
skillName: selectedSkill.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
|
final skillResult = skillService.useBuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
);
|
||||||
|
newPlayerStats = skillResult.updatedPlayer;
|
||||||
|
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerBuff(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: selectedSkill.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
||||||
|
final skillResult = skillService.useDebuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
currentDebuffs: newActiveBuffs,
|
||||||
|
);
|
||||||
|
newPlayerStats = skillResult.updatedPlayer;
|
||||||
|
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
|
if (skillResult.debuffEffect != null) {
|
||||||
|
newActiveBuffs = newActiveBuffs
|
||||||
|
.where((d) => d.effect.id != skillResult.debuffEffect!.effect.id)
|
||||||
|
.toList()
|
||||||
|
..add(skillResult.debuffEffect!);
|
||||||
|
}
|
||||||
|
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerDebuff(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: selectedSkill.name,
|
||||||
|
targetName: newMonsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 일반 공격
|
||||||
|
final attackResult = calculator.playerAttackMonster(
|
||||||
|
attacker: newPlayerStats,
|
||||||
|
defender: newMonsterStats,
|
||||||
|
);
|
||||||
|
newMonsterStats = attackResult.updatedDefender;
|
||||||
|
newTotalDamageDealt += attackResult.result.damage;
|
||||||
|
|
||||||
|
final result = attackResult.result;
|
||||||
|
if (result.isEvaded) {
|
||||||
|
events.add(
|
||||||
|
CombatEvent.monsterEvade(
|
||||||
|
timestamp: timestamp,
|
||||||
|
targetName: newMonsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerAttack(
|
||||||
|
timestamp: timestamp,
|
||||||
|
damage: result.damage,
|
||||||
|
targetName: newMonsterStats.name,
|
||||||
|
isCritical: result.isCritical,
|
||||||
|
attackDelayMs: newPlayerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
monsterStats: newMonsterStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
activeDoTs: newActiveDoTs,
|
||||||
|
activeDebuffs: newActiveBuffs,
|
||||||
|
totalDamageDealt: newTotalDamageDealt,
|
||||||
|
events: events,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 공격 처리
|
||||||
|
({
|
||||||
|
CombatStats playerStats,
|
||||||
|
int totalDamageTaken,
|
||||||
|
List<CombatEvent> events,
|
||||||
|
}) _processMonsterAttack({
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required MonsterCombatStats monsterStats,
|
||||||
|
required List<ActiveBuff> activeDebuffs,
|
||||||
|
required int totalDamageTaken,
|
||||||
|
required int timestamp,
|
||||||
|
required CombatCalculator calculator,
|
||||||
|
}) {
|
||||||
|
final events = <CombatEvent>[];
|
||||||
|
|
||||||
|
// 디버프 효과 적용된 몬스터 스탯 계산
|
||||||
|
var debuffedMonster = monsterStats;
|
||||||
|
if (activeDebuffs.isNotEmpty) {
|
||||||
|
double atkMod = 0;
|
||||||
|
for (final debuff in activeDebuffs) {
|
||||||
|
if (!debuff.isExpired(timestamp)) {
|
||||||
|
atkMod += debuff.effect.atkModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ATK 감소 적용 (최소 10% ATK 유지)
|
||||||
|
final newAtk = (monsterStats.atk * (1 + atkMod)).round().clamp(
|
||||||
|
monsterStats.atk ~/ 10,
|
||||||
|
monsterStats.atk,
|
||||||
|
);
|
||||||
|
debuffedMonster = monsterStats.copyWith(atk: newAtk);
|
||||||
|
}
|
||||||
|
|
||||||
|
final attackResult = calculator.monsterAttackPlayer(
|
||||||
|
attacker: debuffedMonster,
|
||||||
|
defender: playerStats,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = attackResult.result;
|
||||||
|
if (result.isEvaded) {
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerEvade(
|
||||||
|
timestamp: timestamp,
|
||||||
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (result.isBlocked) {
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerBlock(
|
||||||
|
timestamp: timestamp,
|
||||||
|
reducedDamage: result.damage,
|
||||||
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (result.isParried) {
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerParry(
|
||||||
|
timestamp: timestamp,
|
||||||
|
reducedDamage: result.damage,
|
||||||
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
events.add(
|
||||||
|
CombatEvent.monsterAttack(
|
||||||
|
timestamp: timestamp,
|
||||||
|
damage: result.damage,
|
||||||
|
attackerName: monsterStats.name,
|
||||||
|
attackDelayMs: monsterStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
playerStats: attackResult.updatedDefender,
|
||||||
|
totalDamageTaken: totalDamageTaken + attackResult.result.damage,
|
||||||
|
events: events,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user