refactor(engine): ProgressService 경량화
- CombatTickService, MarketService 사용으로 전환 - 중복 로직 제거로 577줄 감소 - item_stats.dart 불필요 코드 정리
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/data/skill_data.dart';
|
||||
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||
import 'package:asciineverdie/src/core/engine/combat_tick_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:asciineverdie/src/core/engine/market_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/potion_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/shop_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';
|
||||
@@ -14,12 +14,10 @@ import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
|
||||
@@ -210,18 +208,19 @@ class ProgressService {
|
||||
? progress.task.max
|
||||
: uncapped;
|
||||
|
||||
// 킬 태스크 중 전투 진행 (스킬 자동 사용, DOT, 물약 포함)
|
||||
// 킬 태스크 중 전투 진행 (CombatTickService 사용)
|
||||
var updatedCombat = progress.currentCombat;
|
||||
var updatedSkillSystem = nextState.skillSystem;
|
||||
var updatedPotionInventory = nextState.potionInventory;
|
||||
if (progress.currentTask.type == TaskType.kill &&
|
||||
updatedCombat != null &&
|
||||
updatedCombat.isActive) {
|
||||
final combatResult = _processCombatTickWithSkills(
|
||||
nextState,
|
||||
updatedCombat,
|
||||
updatedSkillSystem,
|
||||
clamped,
|
||||
final combatTickService = CombatTickService(rng: nextState.rng);
|
||||
final combatResult = combatTickService.processTick(
|
||||
state: nextState,
|
||||
combat: updatedCombat,
|
||||
skillSystem: updatedSkillSystem,
|
||||
elapsedMs: clamped,
|
||||
);
|
||||
updatedCombat = combatResult.combat;
|
||||
updatedSkillSystem = combatResult.skillSystem;
|
||||
@@ -353,15 +352,16 @@ class ProgressService {
|
||||
}
|
||||
}
|
||||
|
||||
// 시장/판매/구매 태스크 완료 시 처리 (원본 Main.pas:631-649)
|
||||
// 시장/판매/구매 태스크 완료 시 처리 (MarketService 사용)
|
||||
final marketService = MarketService(rng: nextState.rng);
|
||||
final taskType = progress.currentTask.type;
|
||||
if (taskType == TaskType.buying) {
|
||||
// 장비 구매 완료 (원본 631-634)
|
||||
nextState = _completeBuying(nextState);
|
||||
nextState = marketService.completeBuying(nextState);
|
||||
progress = nextState.progress;
|
||||
} else if (taskType == TaskType.market || taskType == TaskType.sell) {
|
||||
// 시장 도착 또는 판매 완료 (원본 635-649)
|
||||
final sellResult = _processSell(nextState);
|
||||
final sellResult = marketService.processSell(nextState);
|
||||
nextState = sellResult.state;
|
||||
progress = nextState.progress;
|
||||
queue = nextState.queue;
|
||||
@@ -524,7 +524,7 @@ class ProgressService {
|
||||
oldTaskType != TaskType.buying) {
|
||||
// Gold가 충분하면 장비 구매 (Common 장비 가격 기준)
|
||||
// 실제 구매 가격과 동일한 공식 사용: level * 50
|
||||
final gold = _getGold(state);
|
||||
final gold = state.inventory.gold;
|
||||
final equipPrice = state.traits.level * 50; // Common 장비 1개 가격
|
||||
if (gold > equipPrice) {
|
||||
final taskResult = pq_logic.startTask(
|
||||
@@ -1096,555 +1096,6 @@ class ProgressService {
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
|
||||
/// 인벤토리에서 Gold 수량 반환
|
||||
int _getGold(GameState state) {
|
||||
return state.inventory.gold;
|
||||
}
|
||||
|
||||
/// 장비 구매 완료 처리 (개선된 로직)
|
||||
///
|
||||
/// 1순위: 빈 슬롯에 Common 장비 최대한 채우기
|
||||
/// 2순위: 골드 남으면 물약 구매
|
||||
GameState _completeBuying(GameState state) {
|
||||
var nextState = state;
|
||||
final level = state.traits.level;
|
||||
final shopService = ShopService(rng: nextState.rng);
|
||||
|
||||
// 1. 빈 슬롯 목록 수집
|
||||
final emptySlots = <int>[];
|
||||
for (var i = 0; i < Equipment.slotCount; i++) {
|
||||
if (nextState.equipment.getItemByIndex(i).isEmpty) {
|
||||
emptySlots.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 골드가 허용하는 한 빈 슬롯에 Common 장비 구매
|
||||
for (final slotIndex in emptySlots) {
|
||||
final slot = EquipmentSlot.values[slotIndex];
|
||||
final item = shopService.generateShopItem(
|
||||
playerLevel: level,
|
||||
slot: slot,
|
||||
targetRarity: ItemRarity.common,
|
||||
);
|
||||
final price = shopService.calculateBuyPrice(item);
|
||||
|
||||
if (nextState.inventory.gold >= price) {
|
||||
nextState = nextState.copyWith(
|
||||
inventory: nextState.inventory.copyWith(
|
||||
gold: nextState.inventory.gold - price,
|
||||
),
|
||||
equipment: nextState.equipment
|
||||
.setItemByIndex(slotIndex, item)
|
||||
.copyWith(bestIndex: slotIndex),
|
||||
);
|
||||
} else {
|
||||
break; // 골드 부족 시 중단
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 물약 자동 구매 (남은 골드의 20% 사용)
|
||||
final potionService = const PotionService();
|
||||
final purchaseResult = potionService.autoPurchasePotions(
|
||||
playerLevel: level,
|
||||
inventory: nextState.potionInventory,
|
||||
gold: nextState.inventory.gold,
|
||||
spendRatio: 0.20,
|
||||
);
|
||||
|
||||
if (purchaseResult.success && purchaseResult.newInventory != null) {
|
||||
nextState = nextState.copyWith(
|
||||
inventory: nextState.inventory.copyWith(gold: purchaseResult.newGold),
|
||||
potionInventory: purchaseResult.newInventory,
|
||||
);
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/// 판매 처리 결과
|
||||
({GameState state, bool continuesSelling}) _processSell(GameState state) {
|
||||
final taskType = state.progress.currentTask.type;
|
||||
var items = [...state.inventory.items];
|
||||
var goldAmount = state.inventory.gold;
|
||||
|
||||
// sell 태스크 완료 시 아이템 판매 (원본 Main.pas:636-643)
|
||||
if (taskType == TaskType.sell) {
|
||||
// 첫 번째 아이템 찾기 (items에는 Gold가 없음)
|
||||
if (items.isNotEmpty) {
|
||||
final item = items.first;
|
||||
final level = state.traits.level;
|
||||
|
||||
// 가격 계산: 수량 * 레벨
|
||||
var price = item.count * level;
|
||||
|
||||
// " of " 포함 시 보너스 (원본 639-640)
|
||||
if (item.name.contains(' of ')) {
|
||||
price =
|
||||
price *
|
||||
(1 + pq_logic.randomLow(state.rng, 10)) *
|
||||
(1 + pq_logic.randomLow(state.rng, level));
|
||||
}
|
||||
|
||||
// 아이템 삭제
|
||||
items.removeAt(0);
|
||||
|
||||
// Gold 추가 (inventory.gold 필드 사용)
|
||||
goldAmount += price;
|
||||
}
|
||||
}
|
||||
|
||||
// 판매할 아이템이 남아있는지 확인
|
||||
final hasItemsToSell = items.isNotEmpty;
|
||||
|
||||
if (hasItemsToSell) {
|
||||
// 다음 아이템 판매 태스크 시작
|
||||
final nextItem = items.first;
|
||||
final translatedName = l10n.translateItemNameL10n(nextItem.name);
|
||||
final itemDesc = l10n.indefiniteL10n(translatedName, nextItem.count);
|
||||
final taskResult = pq_logic.startTask(
|
||||
state.progress,
|
||||
l10n.taskSelling(itemDesc),
|
||||
1 * 1000,
|
||||
);
|
||||
final progress = taskResult.progress.copyWith(
|
||||
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
||||
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||
);
|
||||
return (
|
||||
state: state.copyWith(
|
||||
inventory: state.inventory.copyWith(gold: goldAmount, items: items),
|
||||
progress: progress,
|
||||
),
|
||||
continuesSelling: true,
|
||||
);
|
||||
}
|
||||
|
||||
// 판매 완료 - 인벤토리 업데이트만 하고 다음 태스크로
|
||||
return (
|
||||
state: state.copyWith(
|
||||
inventory: state.inventory.copyWith(gold: goldAmount, items: items),
|
||||
),
|
||||
continuesSelling: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 전투 틱 처리 (스킬 자동 사용, DOT, 물약 포함)
|
||||
///
|
||||
/// [state] 현재 게임 상태
|
||||
/// [combat] 현재 전투 상태
|
||||
/// [skillSystem] 스킬 시스템 상태
|
||||
/// [elapsedMs] 경과 시간 (밀리초)
|
||||
/// Returns: 업데이트된 전투 상태, 스킬 시스템 상태, 물약 인벤토리
|
||||
({
|
||||
CombatState combat,
|
||||
SkillSystemState skillSystem,
|
||||
PotionInventory? potionInventory,
|
||||
})
|
||||
_processCombatTickWithSkills(
|
||||
GameState state,
|
||||
CombatState combat,
|
||||
SkillSystemState skillSystem,
|
||||
int elapsedMs,
|
||||
) {
|
||||
if (!combat.isActive || combat.isCombatOver) {
|
||||
return (combat: combat, skillSystem: skillSystem, potionInventory: null);
|
||||
}
|
||||
|
||||
final calculator = CombatCalculator(rng: state.rng);
|
||||
final skillService = SkillService(rng: state.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 틱 처리
|
||||
// =========================================================================
|
||||
var dotDamageThisTick = 0;
|
||||
final updatedDoTs = <DotEffect>[];
|
||||
|
||||
for (final dot in activeDoTs) {
|
||||
final (updatedDot, ticksTriggered) = dot.tick(elapsedMs);
|
||||
|
||||
if (ticksTriggered > 0) {
|
||||
final damage = dot.damagePerTick * ticksTriggered;
|
||||
dotDamageThisTick += damage;
|
||||
|
||||
// DOT 데미지 이벤트 생성 (skillId → name 변환)
|
||||
final dotSkillName =
|
||||
SkillData.getSkillById(dot.skillId)?.name ?? dot.skillId;
|
||||
newEvents.add(
|
||||
CombatEvent.dotTick(
|
||||
timestamp: timestamp,
|
||||
skillName: dotSkillName,
|
||||
damage: damage,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 만료되지 않은 DOT만 유지
|
||||
if (updatedDot.isActive) {
|
||||
updatedDoTs.add(updatedDot);
|
||||
}
|
||||
}
|
||||
|
||||
// DOT 데미지 적용
|
||||
if (dotDamageThisTick > 0 && monsterStats.isAlive) {
|
||||
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick).clamp(
|
||||
0,
|
||||
monsterStats.hpMax,
|
||||
);
|
||||
monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp);
|
||||
totalDamageDealt += dotDamageThisTick;
|
||||
}
|
||||
|
||||
activeDoTs = updatedDoTs;
|
||||
|
||||
// =========================================================================
|
||||
// 긴급 물약 자동 사용 (HP < 30%)
|
||||
// =========================================================================
|
||||
final hpRatio = playerStats.hpCurrent / playerStats.hpMax;
|
||||
if (hpRatio <= PotionService.emergencyHpThreshold) {
|
||||
final emergencyPotion = potionService.selectEmergencyHpPotion(
|
||||
currentHp: playerStats.hpCurrent,
|
||||
maxHp: playerStats.hpMax,
|
||||
inventory: state.potionInventory,
|
||||
playerLevel: state.traits.level,
|
||||
);
|
||||
|
||||
if (emergencyPotion != null && !usedPotionTypes.contains(PotionType.hp)) {
|
||||
final result = potionService.usePotion(
|
||||
potionId: emergencyPotion.id,
|
||||
inventory: state.potionInventory,
|
||||
currentHp: playerStats.hpCurrent,
|
||||
maxHp: playerStats.hpMax,
|
||||
currentMp: playerStats.mpCurrent,
|
||||
maxMp: playerStats.mpMax,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
playerStats = playerStats.copyWith(hpCurrent: result.newHp);
|
||||
usedPotionTypes = {...usedPotionTypes, PotionType.hp};
|
||||
updatedPotionInventory = result.newInventory;
|
||||
|
||||
newEvents.add(
|
||||
CombatEvent.playerPotion(
|
||||
timestamp: timestamp,
|
||||
potionName: emergencyPotion.name,
|
||||
healAmount: result.healedAmount,
|
||||
isHp: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 플레이어 공격 체크
|
||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||
// 장착된 스킬 슬롯에서 사용 가능한 스킬 ID 목록 조회
|
||||
var availableSkillIds = state.skillSystem.equippedSkills.allSkills
|
||||
.map((s) => s.id)
|
||||
.toList();
|
||||
// 장착된 스킬이 없으면 기본 스킬 사용
|
||||
if (availableSkillIds.isEmpty) {
|
||||
availableSkillIds = SkillData.defaultSkillIds;
|
||||
}
|
||||
|
||||
final selectedSkill = skillService.selectAutoSkill(
|
||||
player: playerStats,
|
||||
monster: monsterStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
availableSkillIds: availableSkillIds,
|
||||
activeDoTs: activeDoTs,
|
||||
activeDebuffs: activeDebuffs,
|
||||
);
|
||||
|
||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||
// 스킬 랭크 조회 (SkillBook 기반)
|
||||
final skillRank = skillService.getSkillRankFromSkillBook(
|
||||
state.skillBook,
|
||||
selectedSkill.id,
|
||||
);
|
||||
// 랭크 스케일링 적용된 공격 스킬 사용
|
||||
final skillResult = skillService.useAttackSkillWithRank(
|
||||
skill: selectedSkill,
|
||||
player: playerStats,
|
||||
monster: monsterStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
rank: skillRank,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
monsterStats = skillResult.updatedMonster;
|
||||
totalDamageDealt += skillResult.result.damage;
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// GCD 시작 (스킬 사용 후)
|
||||
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||
|
||||
// 스킬 공격 이벤트 생성
|
||||
newEvents.add(
|
||||
CombatEvent.playerSkill(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
attackDelayMs: playerStats.attackDelayMs,
|
||||
),
|
||||
);
|
||||
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||
// DOT 스킬 사용
|
||||
final skillResult = skillService.useDotSkill(
|
||||
skill: selectedSkill,
|
||||
player: playerStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
playerInt: state.stats.intelligence,
|
||||
playerWis: state.stats.wis,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// GCD 시작 (스킬 사용 후)
|
||||
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||
|
||||
// DOT 효과 추가
|
||||
if (skillResult.dotEffect != null) {
|
||||
activeDoTs.add(skillResult.dotEffect!);
|
||||
}
|
||||
|
||||
// DOT 스킬 사용 이벤트 생성
|
||||
newEvents.add(
|
||||
CombatEvent.playerSkill(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
attackDelayMs: playerStats.attackDelayMs,
|
||||
),
|
||||
);
|
||||
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||
// 회복 스킬 사용
|
||||
final skillResult = skillService.useHealSkill(
|
||||
skill: selectedSkill,
|
||||
player: playerStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// GCD 시작 (스킬 사용 후)
|
||||
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||
|
||||
// 회복 이벤트 생성
|
||||
newEvents.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: playerStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// GCD 시작 (스킬 사용 후)
|
||||
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||
|
||||
// 버프 이벤트 생성
|
||||
newEvents.add(
|
||||
CombatEvent.playerBuff(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
),
|
||||
);
|
||||
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
||||
// 디버프 스킬 사용
|
||||
final skillResult = skillService.useDebuffSkill(
|
||||
skill: selectedSkill,
|
||||
player: playerStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
currentDebuffs: activeDebuffs,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// GCD 시작 (스킬 사용 후)
|
||||
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||
|
||||
// 디버프 효과 추가 (기존 같은 디버프 제거 후)
|
||||
if (skillResult.debuffEffect != null) {
|
||||
activeDebuffs =
|
||||
activeDebuffs
|
||||
.where(
|
||||
(d) => d.effect.id != skillResult.debuffEffect!.effect.id,
|
||||
)
|
||||
.toList()
|
||||
..add(skillResult.debuffEffect!);
|
||||
}
|
||||
|
||||
// 디버프 이벤트 생성
|
||||
newEvents.add(
|
||||
CombatEvent.playerDebuff(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 일반 공격
|
||||
final attackResult = calculator.playerAttackMonster(
|
||||
attacker: playerStats,
|
||||
defender: monsterStats,
|
||||
);
|
||||
monsterStats = attackResult.updatedDefender;
|
||||
totalDamageDealt += attackResult.result.damage;
|
||||
|
||||
// 일반 공격 이벤트 생성
|
||||
final result = attackResult.result;
|
||||
if (result.isEvaded) {
|
||||
newEvents.add(
|
||||
CombatEvent.monsterEvade(
|
||||
timestamp: timestamp,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
newEvents.add(
|
||||
CombatEvent.playerAttack(
|
||||
timestamp: timestamp,
|
||||
damage: result.damage,
|
||||
targetName: monsterStats.name,
|
||||
isCritical: result.isCritical,
|
||||
attackDelayMs: playerStats.attackDelayMs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
playerAccumulator -= playerStats.attackDelayMs;
|
||||
turnsElapsed++;
|
||||
}
|
||||
|
||||
// 몬스터가 살아있으면 반격
|
||||
if (monsterStats.isAlive &&
|
||||
monsterAccumulator >= monsterStats.attackDelayMs) {
|
||||
// 디버프 효과 적용된 몬스터 스탯 계산
|
||||
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,
|
||||
);
|
||||
playerStats = attackResult.updatedDefender;
|
||||
totalDamageTaken += attackResult.result.damage;
|
||||
monsterAccumulator -= monsterStats.attackDelayMs;
|
||||
|
||||
// 몬스터 공격 이벤트 생성
|
||||
final result = attackResult.result;
|
||||
if (result.isEvaded) {
|
||||
newEvents.add(
|
||||
CombatEvent.playerEvade(
|
||||
timestamp: timestamp,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else if (result.isBlocked) {
|
||||
newEvents.add(
|
||||
CombatEvent.playerBlock(
|
||||
timestamp: timestamp,
|
||||
reducedDamage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else if (result.isParried) {
|
||||
newEvents.add(
|
||||
CombatEvent.playerParry(
|
||||
timestamp: timestamp,
|
||||
reducedDamage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
newEvents.add(
|
||||
CombatEvent.monsterAttack(
|
||||
timestamp: timestamp,
|
||||
damage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
attackDelayMs: 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 (
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
/// Act Boss 생성 (Act 완료 시)
|
||||
///
|
||||
/// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)로 설정하여
|
||||
|
||||
Reference in New Issue
Block a user