refactor(engine): 엔진 서비스 분리 및 리팩토링
- progress_service에서 death_handler, loot_handler, task_generator 분리 - combat_tick_service에서 player_attack_processor 분리 - arena_service에서 arena_combat_simulator 분리 - skill_service에서 skill_auto_selector 분리
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
import 'package:asciineverdie/src/shared/animation/monster_size.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/combat_state.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/combat_stats.dart';
|
||||||
|
|||||||
@@ -71,15 +71,14 @@ class AdService {
|
|||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// 프로덕션 광고 ID (AdMob 콘솔에서 생성 후 교체)
|
// 프로덕션 광고 ID (AdMob 콘솔에서 생성 후 교체)
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// TODO: AdMob 콘솔에서 광고 단위 생성 후 아래 ID 교체
|
|
||||||
static const String _prodRewardedAndroid =
|
static const String _prodRewardedAndroid =
|
||||||
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // Android 리워드 광고
|
'ca-app-pub-6691216385521068/3457464395'; // Android 리워드 광고
|
||||||
static const String _prodRewardedIos =
|
static const String _prodRewardedIos =
|
||||||
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // iOS 리워드 광고
|
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // TODO: iOS 리워드 광고 ID 교체
|
||||||
static const String _prodInterstitialAndroid =
|
static const String _prodInterstitialAndroid =
|
||||||
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // Android 인터스티셜 광고
|
'ca-app-pub-6691216385521068/1625507977'; // Android 인터스티셜 광고
|
||||||
static const String _prodInterstitialIos =
|
static const String _prodInterstitialIos =
|
||||||
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // iOS 인터스티셜 광고
|
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // TODO: iOS 인터스티셜 광고 ID 교체
|
||||||
|
|
||||||
/// 리워드 광고 단위 ID (릴리즈 빌드: 프로덕션 ID, 디버그 빌드: 테스트 ID)
|
/// 리워드 광고 단위 ID (릴리즈 빌드: 프로덕션 ID, 디버그 빌드: 테스트 ID)
|
||||||
String get _rewardAdUnitId {
|
String get _rewardAdUnitId {
|
||||||
|
|||||||
497
lib/src/core/engine/arena_combat_simulator.dart
Normal file
497
lib/src/core/engine/arena_combat_simulator.dart
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
import 'package:asciineverdie/data/skill_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/arena_match.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/hall_of_fame.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 아레나 전투 시뮬레이터
|
||||||
|
///
|
||||||
|
/// ArenaService에서 분리된 전투 시뮬레이션 로직.
|
||||||
|
/// 스킬 시스템을 포함한 턴 기반 전투를 처리한다.
|
||||||
|
class ArenaCombatSimulator {
|
||||||
|
ArenaCombatSimulator({required DeterministicRandom rng})
|
||||||
|
: _rng = rng,
|
||||||
|
_skillService = SkillService(rng: rng);
|
||||||
|
|
||||||
|
final DeterministicRandom _rng;
|
||||||
|
final SkillService _skillService;
|
||||||
|
|
||||||
|
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
||||||
|
Stream<ArenaCombatTurn> simulateCombat(ArenaMatch match) async* {
|
||||||
|
final challengerStats = match.challenger.finalStats;
|
||||||
|
final opponentStats = match.opponent.finalStats;
|
||||||
|
|
||||||
|
if (challengerStats == null || opponentStats == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final calculator = CombatCalculator(rng: _rng);
|
||||||
|
|
||||||
|
// 스킬 ID 목록 로드
|
||||||
|
var challengerSkillIds = _getSkillIdsFromEntry(match.challenger);
|
||||||
|
var opponentSkillIds = _getSkillIdsFromEntry(match.opponent);
|
||||||
|
if (challengerSkillIds.isEmpty) {
|
||||||
|
challengerSkillIds = SkillData.defaultSkillIds;
|
||||||
|
}
|
||||||
|
if (opponentSkillIds.isEmpty) {
|
||||||
|
opponentSkillIds = SkillData.defaultSkillIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스킬 시스템 상태 초기화
|
||||||
|
var challengerSkillSystem = SkillSystemState.empty();
|
||||||
|
var opponentSkillSystem = SkillSystemState.empty();
|
||||||
|
|
||||||
|
// DOT 및 디버프 추적
|
||||||
|
var challengerDoTs = <DotEffect>[];
|
||||||
|
var opponentDoTs = <DotEffect>[];
|
||||||
|
var challengerDebuffs = <ActiveBuff>[];
|
||||||
|
var opponentDebuffs = <ActiveBuff>[];
|
||||||
|
|
||||||
|
var playerCombatStats = challengerStats.copyWith(
|
||||||
|
hpCurrent: challengerStats.hpMax,
|
||||||
|
mpCurrent: challengerStats.mpMax,
|
||||||
|
);
|
||||||
|
|
||||||
|
var opponentCombatStats = opponentStats.copyWith(
|
||||||
|
hpCurrent: opponentStats.hpMax,
|
||||||
|
mpCurrent: opponentStats.mpMax,
|
||||||
|
);
|
||||||
|
|
||||||
|
int playerAccum = 0;
|
||||||
|
int opponentAccum = 0;
|
||||||
|
int elapsedMs = 0;
|
||||||
|
const tickMs = 200;
|
||||||
|
int turns = 0;
|
||||||
|
|
||||||
|
// 초기 상태 전송
|
||||||
|
yield ArenaCombatTurn(
|
||||||
|
challengerHp: playerCombatStats.hpCurrent,
|
||||||
|
opponentHp: opponentCombatStats.hpCurrent,
|
||||||
|
challengerHpMax: playerCombatStats.hpMax,
|
||||||
|
opponentHpMax: opponentCombatStats.hpMax,
|
||||||
|
challengerMp: playerCombatStats.mpCurrent,
|
||||||
|
opponentMp: opponentCombatStats.mpCurrent,
|
||||||
|
challengerMpMax: playerCombatStats.mpMax,
|
||||||
|
opponentMpMax: opponentCombatStats.mpMax,
|
||||||
|
);
|
||||||
|
|
||||||
|
while (playerCombatStats.isAlive && opponentCombatStats.hpCurrent > 0) {
|
||||||
|
playerAccum += tickMs;
|
||||||
|
opponentAccum += tickMs;
|
||||||
|
elapsedMs += tickMs;
|
||||||
|
|
||||||
|
// 스킬 시스템 시간 업데이트
|
||||||
|
challengerSkillSystem = challengerSkillSystem.copyWith(
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
);
|
||||||
|
opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs);
|
||||||
|
|
||||||
|
int? challengerDamage;
|
||||||
|
int? opponentDamage;
|
||||||
|
bool isChallengerCritical = false;
|
||||||
|
bool isOpponentCritical = false;
|
||||||
|
bool isChallengerEvaded = false;
|
||||||
|
bool isOpponentEvaded = false;
|
||||||
|
bool isChallengerBlocked = false;
|
||||||
|
bool isOpponentBlocked = false;
|
||||||
|
String? challengerSkillUsed;
|
||||||
|
String? opponentSkillUsed;
|
||||||
|
int? challengerHealAmount;
|
||||||
|
int? opponentHealAmount;
|
||||||
|
|
||||||
|
// DOT 틱 처리
|
||||||
|
final dotResult = _processDotTicks(
|
||||||
|
challengerDoTs: challengerDoTs,
|
||||||
|
opponentDoTs: opponentDoTs,
|
||||||
|
playerStats: playerCombatStats,
|
||||||
|
opponentStats: opponentCombatStats,
|
||||||
|
tickMs: tickMs,
|
||||||
|
);
|
||||||
|
challengerDoTs = dotResult.challengerDoTs;
|
||||||
|
opponentDoTs = dotResult.opponentDoTs;
|
||||||
|
playerCombatStats = dotResult.playerStats;
|
||||||
|
opponentCombatStats = dotResult.opponentStats;
|
||||||
|
|
||||||
|
// 만료된 디버프 정리
|
||||||
|
challengerDebuffs = challengerDebuffs
|
||||||
|
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
||||||
|
.toList();
|
||||||
|
opponentDebuffs = opponentDebuffs
|
||||||
|
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 도전자 턴
|
||||||
|
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
||||||
|
playerAccum = 0;
|
||||||
|
|
||||||
|
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
|
opponentCombatStats,
|
||||||
|
match.opponent.characterName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final turnResult = _processCharacterTurn(
|
||||||
|
player: playerCombatStats,
|
||||||
|
target: opponentCombatStats,
|
||||||
|
targetMonster: opponentMonsterStats,
|
||||||
|
targetName: match.opponent.characterName,
|
||||||
|
entry: match.challenger,
|
||||||
|
skillIds: challengerSkillIds,
|
||||||
|
skillSystem: challengerSkillSystem,
|
||||||
|
activeDoTs: challengerDoTs,
|
||||||
|
activeDebuffs: opponentDebuffs,
|
||||||
|
calculator: calculator,
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
playerCombatStats = turnResult.player;
|
||||||
|
opponentCombatStats = turnResult.target;
|
||||||
|
challengerSkillSystem = turnResult.skillSystem;
|
||||||
|
challengerDoTs = turnResult.activeDoTs;
|
||||||
|
opponentDebuffs = turnResult.targetDebuffs;
|
||||||
|
challengerDamage = turnResult.damage;
|
||||||
|
isChallengerCritical = turnResult.isCritical;
|
||||||
|
isOpponentEvaded = turnResult.isTargetEvaded;
|
||||||
|
challengerSkillUsed = turnResult.skillUsed;
|
||||||
|
challengerHealAmount = turnResult.healAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 턴
|
||||||
|
if (opponentCombatStats.hpCurrent > 0 &&
|
||||||
|
opponentAccum >= opponentCombatStats.attackDelayMs) {
|
||||||
|
opponentAccum = 0;
|
||||||
|
|
||||||
|
var challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
|
playerCombatStats,
|
||||||
|
match.challenger.characterName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final turnResult = _processCharacterTurn(
|
||||||
|
player: opponentCombatStats,
|
||||||
|
target: playerCombatStats,
|
||||||
|
targetMonster: challengerMonsterStats,
|
||||||
|
targetName: match.challenger.characterName,
|
||||||
|
entry: match.opponent,
|
||||||
|
skillIds: opponentSkillIds,
|
||||||
|
skillSystem: opponentSkillSystem,
|
||||||
|
activeDoTs: opponentDoTs,
|
||||||
|
activeDebuffs: challengerDebuffs,
|
||||||
|
calculator: calculator,
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
opponentCombatStats = turnResult.player;
|
||||||
|
playerCombatStats = turnResult.target;
|
||||||
|
opponentSkillSystem = turnResult.skillSystem;
|
||||||
|
opponentDoTs = turnResult.activeDoTs;
|
||||||
|
challengerDebuffs = turnResult.targetDebuffs;
|
||||||
|
opponentDamage = turnResult.damage;
|
||||||
|
isOpponentCritical = turnResult.isCritical;
|
||||||
|
isChallengerEvaded = turnResult.isTargetEvaded;
|
||||||
|
isChallengerBlocked = turnResult.isTargetBlocked;
|
||||||
|
opponentSkillUsed = turnResult.skillUsed;
|
||||||
|
opponentHealAmount = turnResult.healAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 액션이 발생했을 때만 턴 전송
|
||||||
|
final hasAction =
|
||||||
|
challengerDamage != null ||
|
||||||
|
opponentDamage != null ||
|
||||||
|
challengerHealAmount != null ||
|
||||||
|
opponentHealAmount != null ||
|
||||||
|
challengerSkillUsed != null ||
|
||||||
|
opponentSkillUsed != null;
|
||||||
|
|
||||||
|
if (hasAction) {
|
||||||
|
turns++;
|
||||||
|
yield ArenaCombatTurn(
|
||||||
|
challengerDamage: challengerDamage,
|
||||||
|
opponentDamage: opponentDamage,
|
||||||
|
challengerHp: playerCombatStats.hpCurrent,
|
||||||
|
opponentHp: opponentCombatStats.hpCurrent,
|
||||||
|
challengerHpMax: playerCombatStats.hpMax,
|
||||||
|
opponentHpMax: opponentCombatStats.hpMax,
|
||||||
|
challengerMp: playerCombatStats.mpCurrent,
|
||||||
|
opponentMp: opponentCombatStats.mpCurrent,
|
||||||
|
challengerMpMax: playerCombatStats.mpMax,
|
||||||
|
opponentMpMax: opponentCombatStats.mpMax,
|
||||||
|
isChallengerCritical: isChallengerCritical,
|
||||||
|
isOpponentCritical: isOpponentCritical,
|
||||||
|
isChallengerEvaded: isChallengerEvaded,
|
||||||
|
isOpponentEvaded: isOpponentEvaded,
|
||||||
|
isChallengerBlocked: isChallengerBlocked,
|
||||||
|
isOpponentBlocked: isOpponentBlocked,
|
||||||
|
challengerSkillUsed: challengerSkillUsed,
|
||||||
|
opponentSkillUsed: opponentSkillUsed,
|
||||||
|
challengerHealAmount: challengerHealAmount,
|
||||||
|
opponentHealAmount: opponentHealAmount,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turns > 1000) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DOT 틱 처리 (양측)
|
||||||
|
({
|
||||||
|
List<DotEffect> challengerDoTs,
|
||||||
|
List<DotEffect> opponentDoTs,
|
||||||
|
CombatStats playerStats,
|
||||||
|
CombatStats opponentStats,
|
||||||
|
})
|
||||||
|
_processDotTicks({
|
||||||
|
required List<DotEffect> challengerDoTs,
|
||||||
|
required List<DotEffect> opponentDoTs,
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required CombatStats opponentStats,
|
||||||
|
required int tickMs,
|
||||||
|
}) {
|
||||||
|
var updatedPlayerStats = playerStats;
|
||||||
|
var updatedOpponentStats = opponentStats;
|
||||||
|
|
||||||
|
// 도전자 -> 상대에게 적용된 DOT
|
||||||
|
var dotDamageToOpponent = 0;
|
||||||
|
final updatedChallengerDoTs = <DotEffect>[];
|
||||||
|
for (final dot in challengerDoTs) {
|
||||||
|
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
||||||
|
if (ticksTriggered > 0) {
|
||||||
|
dotDamageToOpponent += dot.damagePerTick * ticksTriggered;
|
||||||
|
}
|
||||||
|
if (updatedDot.isActive) updatedChallengerDoTs.add(updatedDot);
|
||||||
|
}
|
||||||
|
if (dotDamageToOpponent > 0 && updatedOpponentStats.hpCurrent > 0) {
|
||||||
|
updatedOpponentStats = updatedOpponentStats.copyWith(
|
||||||
|
hpCurrent: (updatedOpponentStats.hpCurrent - dotDamageToOpponent).clamp(
|
||||||
|
0,
|
||||||
|
updatedOpponentStats.hpMax,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 -> 도전자에게 적용된 DOT
|
||||||
|
var dotDamageToChallenger = 0;
|
||||||
|
final updatedOpponentDoTs = <DotEffect>[];
|
||||||
|
for (final dot in opponentDoTs) {
|
||||||
|
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
||||||
|
if (ticksTriggered > 0) {
|
||||||
|
dotDamageToChallenger += dot.damagePerTick * ticksTriggered;
|
||||||
|
}
|
||||||
|
if (updatedDot.isActive) updatedOpponentDoTs.add(updatedDot);
|
||||||
|
}
|
||||||
|
if (dotDamageToChallenger > 0 && updatedPlayerStats.isAlive) {
|
||||||
|
updatedPlayerStats = updatedPlayerStats.copyWith(
|
||||||
|
hpCurrent: (updatedPlayerStats.hpCurrent - dotDamageToChallenger).clamp(
|
||||||
|
0,
|
||||||
|
updatedPlayerStats.hpMax,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
challengerDoTs: updatedChallengerDoTs,
|
||||||
|
opponentDoTs: updatedOpponentDoTs,
|
||||||
|
playerStats: updatedPlayerStats,
|
||||||
|
opponentStats: updatedOpponentStats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐릭터 턴 처리 (도전자/상대 공통)
|
||||||
|
({
|
||||||
|
CombatStats player,
|
||||||
|
CombatStats target,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
List<DotEffect> activeDoTs,
|
||||||
|
List<ActiveBuff> targetDebuffs,
|
||||||
|
int? damage,
|
||||||
|
bool isCritical,
|
||||||
|
bool isTargetEvaded,
|
||||||
|
bool isTargetBlocked,
|
||||||
|
String? skillUsed,
|
||||||
|
int? healAmount,
|
||||||
|
})
|
||||||
|
_processCharacterTurn({
|
||||||
|
required CombatStats player,
|
||||||
|
required CombatStats target,
|
||||||
|
required MonsterCombatStats targetMonster,
|
||||||
|
required String targetName,
|
||||||
|
required HallOfFameEntry entry,
|
||||||
|
required List<String> skillIds,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required List<DotEffect> activeDoTs,
|
||||||
|
required List<ActiveBuff> activeDebuffs,
|
||||||
|
required CombatCalculator calculator,
|
||||||
|
required int elapsedMs,
|
||||||
|
}) {
|
||||||
|
int? damage;
|
||||||
|
bool isCritical = false;
|
||||||
|
bool isTargetEvaded = false;
|
||||||
|
bool isTargetBlocked = false;
|
||||||
|
String? skillUsed;
|
||||||
|
int? healAmount;
|
||||||
|
var updatedPlayer = player;
|
||||||
|
var updatedTarget = target;
|
||||||
|
var updatedSkillSystem = skillSystem;
|
||||||
|
var updatedDoTs = [...activeDoTs];
|
||||||
|
var updatedDebuffs = [...activeDebuffs];
|
||||||
|
|
||||||
|
final selectedSkill = _skillService.selectAutoSkill(
|
||||||
|
player: updatedPlayer,
|
||||||
|
monster: targetMonster,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
availableSkillIds: skillIds,
|
||||||
|
activeDoTs: updatedDoTs,
|
||||||
|
activeDebuffs: updatedDebuffs,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
|
final skillRank = _getSkillRankFromEntry(entry, selectedSkill.id);
|
||||||
|
final skillResult = _skillService.useAttackSkillWithRank(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: updatedPlayer,
|
||||||
|
monster: targetMonster,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
rank: skillRank,
|
||||||
|
);
|
||||||
|
updatedPlayer = skillResult.updatedPlayer;
|
||||||
|
updatedTarget = updatedTarget.copyWith(
|
||||||
|
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
||||||
|
);
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
skillUsed = selectedSkill.name;
|
||||||
|
damage = skillResult.result.damage;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||||
|
final skillResult = _skillService.useDotSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: updatedPlayer,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
playerInt: updatedPlayer.atk ~/ 10,
|
||||||
|
playerWis: updatedPlayer.def ~/ 10,
|
||||||
|
);
|
||||||
|
updatedPlayer = skillResult.updatedPlayer;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
if (skillResult.dotEffect != null) {
|
||||||
|
updatedDoTs.add(skillResult.dotEffect!);
|
||||||
|
}
|
||||||
|
skillUsed = selectedSkill.name;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
|
final skillResult = _skillService.useHealSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: updatedPlayer,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
updatedPlayer = skillResult.updatedPlayer;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
skillUsed = selectedSkill.name;
|
||||||
|
healAmount = skillResult.result.healedAmount;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
|
final skillResult = _skillService.useBuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: updatedPlayer,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
updatedPlayer = skillResult.updatedPlayer;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
skillUsed = selectedSkill.name;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
||||||
|
final skillResult = _skillService.useDebuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: updatedPlayer,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
currentDebuffs: updatedDebuffs,
|
||||||
|
);
|
||||||
|
updatedPlayer = skillResult.updatedPlayer;
|
||||||
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
final debuffEffect = skillResult.debuffEffect;
|
||||||
|
if (debuffEffect != null) {
|
||||||
|
updatedDebuffs =
|
||||||
|
updatedDebuffs
|
||||||
|
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id)
|
||||||
|
.toList()
|
||||||
|
..add(debuffEffect);
|
||||||
|
}
|
||||||
|
skillUsed = selectedSkill.name;
|
||||||
|
} else {
|
||||||
|
// 일반 공격
|
||||||
|
final opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
|
updatedTarget,
|
||||||
|
targetName,
|
||||||
|
);
|
||||||
|
final result = calculator.playerAttackMonster(
|
||||||
|
attacker: updatedPlayer,
|
||||||
|
defender: opponentMonsterStats,
|
||||||
|
);
|
||||||
|
updatedTarget = updatedTarget.copyWith(
|
||||||
|
hpCurrent: result.updatedDefender.hpCurrent,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.result.isHit) {
|
||||||
|
damage = result.result.damage;
|
||||||
|
isCritical = result.result.isCritical;
|
||||||
|
} else {
|
||||||
|
isTargetEvaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
player: updatedPlayer,
|
||||||
|
target: updatedTarget,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
activeDoTs: updatedDoTs,
|
||||||
|
targetDebuffs: updatedDebuffs,
|
||||||
|
damage: damage,
|
||||||
|
isCritical: isCritical,
|
||||||
|
isTargetEvaded: isTargetEvaded,
|
||||||
|
isTargetBlocked: isTargetBlocked,
|
||||||
|
skillUsed: skillUsed,
|
||||||
|
healAmount: healAmount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스킬 ID 목록 추출 (HallOfFameEntry에서)
|
||||||
|
List<String> _getSkillIdsFromEntry(HallOfFameEntry entry) {
|
||||||
|
final skillData = entry.finalSkills;
|
||||||
|
if (skillData == null || skillData.isEmpty) return [];
|
||||||
|
|
||||||
|
final skillIds = <String>[];
|
||||||
|
for (final data in skillData) {
|
||||||
|
final skillName = data['name'];
|
||||||
|
if (skillName != null) {
|
||||||
|
final skill = SkillData.getSkillBySpellName(skillName);
|
||||||
|
if (skill != null) {
|
||||||
|
skillIds.add(skill.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skillIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스킬 랭크 조회 (HallOfFameEntry의 finalSkills에서)
|
||||||
|
int _getSkillRankFromEntry(HallOfFameEntry entry, String skillId) {
|
||||||
|
final skill = SkillData.getSkillById(skillId);
|
||||||
|
if (skill == null) return 1;
|
||||||
|
|
||||||
|
final skillData = entry.finalSkills;
|
||||||
|
if (skillData == null || skillData.isEmpty) return 1;
|
||||||
|
|
||||||
|
for (final data in skillData) {
|
||||||
|
if (data['name'] == skill.name) {
|
||||||
|
final rankStr = data['rank'] ?? 'I';
|
||||||
|
return switch (rankStr) {
|
||||||
|
'I' => 1,
|
||||||
|
'II' => 2,
|
||||||
|
'III' => 3,
|
||||||
|
'IV' => 4,
|
||||||
|
'V' => 5,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import 'package:asciineverdie/data/skill_data.dart';
|
import 'package:asciineverdie/src/core/engine/arena_combat_simulator.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/item_service.dart';
|
import 'package:asciineverdie/src/core/engine/item_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
||||||
import 'package:asciineverdie/src/core/model/equipment_item.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/equipment_slot.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
|
||||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
/// 아레나 서비스
|
/// 아레나 서비스
|
||||||
@@ -23,64 +20,6 @@ class ArenaService {
|
|||||||
|
|
||||||
final DeterministicRandom _rng;
|
final DeterministicRandom _rng;
|
||||||
|
|
||||||
late final SkillService _skillService = SkillService(rng: _rng);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 스킬 시스템 헬퍼
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// HallOfFameEntry의 finalSkills에서 Skill 목록 추출
|
|
||||||
List<Skill> _getSkillsFromEntry(HallOfFameEntry entry) {
|
|
||||||
final skillData = entry.finalSkills;
|
|
||||||
if (skillData == null || skillData.isEmpty) return [];
|
|
||||||
|
|
||||||
final skills = <Skill>[];
|
|
||||||
for (final data in skillData) {
|
|
||||||
final skillName = data['name'];
|
|
||||||
if (skillName != null) {
|
|
||||||
final skill = SkillData.getSkillBySpellName(skillName);
|
|
||||||
if (skill != null) {
|
|
||||||
skills.add(skill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return skills;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 스킬 ID 목록 추출 (HallOfFameEntry에서)
|
|
||||||
List<String> _getSkillIdsFromEntry(HallOfFameEntry entry) {
|
|
||||||
return _getSkillsFromEntry(entry).map((s) => s.id).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 스킬 랭크 조회 (HallOfFameEntry의 finalSkills에서)
|
|
||||||
int _getSkillRankFromEntry(HallOfFameEntry entry, String skillId) {
|
|
||||||
final skill = SkillData.getSkillById(skillId);
|
|
||||||
if (skill == null) return 1;
|
|
||||||
|
|
||||||
final skillData = entry.finalSkills;
|
|
||||||
if (skillData == null || skillData.isEmpty) return 1;
|
|
||||||
|
|
||||||
for (final data in skillData) {
|
|
||||||
if (data['name'] == skill.name) {
|
|
||||||
final rankStr = data['rank'] ?? 'I';
|
|
||||||
return _romanToInt(rankStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 로마 숫자 → 정수 변환
|
|
||||||
int _romanToInt(String roman) {
|
|
||||||
return switch (roman) {
|
|
||||||
'I' => 1,
|
|
||||||
'II' => 2,
|
|
||||||
'III' => 3,
|
|
||||||
'IV' => 4,
|
|
||||||
'V' => 5,
|
|
||||||
_ => 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 상대 결정
|
// 상대 결정
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -230,452 +169,10 @@ class ArenaService {
|
|||||||
|
|
||||||
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
||||||
///
|
///
|
||||||
/// progress_service._processCombatTickWithSkills()와 동일한 로직 사용
|
/// ArenaCombatSimulator에 위임하여 턴별 전투 상황을 스트림으로 반환.
|
||||||
/// [match] 대전 정보
|
Stream<ArenaCombatTurn> simulateCombat(ArenaMatch match) {
|
||||||
/// Returns: 턴별 전투 상황 스트림
|
final simulator = ArenaCombatSimulator(rng: _rng);
|
||||||
Stream<ArenaCombatTurn> simulateCombat(ArenaMatch match) async* {
|
return simulator.simulateCombat(match);
|
||||||
final calculator = CombatCalculator(rng: _rng);
|
|
||||||
|
|
||||||
final challengerStats = match.challenger.finalStats;
|
|
||||||
final opponentStats = match.opponent.finalStats;
|
|
||||||
|
|
||||||
if (challengerStats == null || opponentStats == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스킬 ID 목록 로드 (SkillBook과 동일한 방식)
|
|
||||||
var challengerSkillIds = _getSkillIdsFromEntry(match.challenger);
|
|
||||||
var opponentSkillIds = _getSkillIdsFromEntry(match.opponent);
|
|
||||||
|
|
||||||
// 스킬이 없으면 기본 스킬 사용
|
|
||||||
if (challengerSkillIds.isEmpty) {
|
|
||||||
challengerSkillIds = SkillData.defaultSkillIds;
|
|
||||||
}
|
|
||||||
if (opponentSkillIds.isEmpty) {
|
|
||||||
opponentSkillIds = SkillData.defaultSkillIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스킬 시스템 상태 초기화
|
|
||||||
var challengerSkillSystem = SkillSystemState.empty();
|
|
||||||
var opponentSkillSystem = SkillSystemState.empty();
|
|
||||||
|
|
||||||
// DOT 및 디버프 추적 (일반 전투와 동일)
|
|
||||||
var challengerDoTs = <DotEffect>[];
|
|
||||||
var opponentDoTs = <DotEffect>[];
|
|
||||||
var challengerDebuffs = <ActiveBuff>[];
|
|
||||||
var opponentDebuffs = <ActiveBuff>[];
|
|
||||||
|
|
||||||
var playerCombatStats = challengerStats.copyWith(
|
|
||||||
hpCurrent: challengerStats.hpMax,
|
|
||||||
mpCurrent: challengerStats.mpMax,
|
|
||||||
);
|
|
||||||
|
|
||||||
var opponentCombatStats = opponentStats.copyWith(
|
|
||||||
hpCurrent: opponentStats.hpMax,
|
|
||||||
mpCurrent: opponentStats.mpMax,
|
|
||||||
);
|
|
||||||
|
|
||||||
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
||||||
opponentCombatStats,
|
|
||||||
match.opponent.characterName,
|
|
||||||
);
|
|
||||||
|
|
||||||
var challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
||||||
playerCombatStats,
|
|
||||||
match.challenger.characterName,
|
|
||||||
);
|
|
||||||
|
|
||||||
int playerAccum = 0;
|
|
||||||
int opponentAccum = 0;
|
|
||||||
int elapsedMs = 0;
|
|
||||||
const tickMs = 200;
|
|
||||||
int turns = 0;
|
|
||||||
|
|
||||||
// 초기 상태 전송
|
|
||||||
yield ArenaCombatTurn(
|
|
||||||
challengerHp: playerCombatStats.hpCurrent,
|
|
||||||
opponentHp: opponentCombatStats.hpCurrent,
|
|
||||||
challengerHpMax: playerCombatStats.hpMax,
|
|
||||||
opponentHpMax: opponentCombatStats.hpMax,
|
|
||||||
challengerMp: playerCombatStats.mpCurrent,
|
|
||||||
opponentMp: opponentCombatStats.mpCurrent,
|
|
||||||
challengerMpMax: playerCombatStats.mpMax,
|
|
||||||
opponentMpMax: opponentCombatStats.mpMax,
|
|
||||||
);
|
|
||||||
|
|
||||||
while (playerCombatStats.isAlive && opponentCombatStats.hpCurrent > 0) {
|
|
||||||
playerAccum += tickMs;
|
|
||||||
opponentAccum += tickMs;
|
|
||||||
elapsedMs += tickMs;
|
|
||||||
|
|
||||||
// 스킬 시스템 시간 업데이트
|
|
||||||
challengerSkillSystem = challengerSkillSystem.copyWith(
|
|
||||||
elapsedMs: elapsedMs,
|
|
||||||
);
|
|
||||||
opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs);
|
|
||||||
|
|
||||||
int? challengerDamage;
|
|
||||||
int? opponentDamage;
|
|
||||||
bool isChallengerCritical = false;
|
|
||||||
bool isOpponentCritical = false;
|
|
||||||
bool isChallengerEvaded = false;
|
|
||||||
bool isOpponentEvaded = false;
|
|
||||||
bool isChallengerBlocked = false;
|
|
||||||
bool isOpponentBlocked = false;
|
|
||||||
String? challengerSkillUsed;
|
|
||||||
String? opponentSkillUsed;
|
|
||||||
int? challengerHealAmount;
|
|
||||||
int? opponentHealAmount;
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// DOT 틱 처리 (도전자 → 상대에게 적용된 DOT)
|
|
||||||
// =========================================================================
|
|
||||||
var dotDamageToOpponent = 0;
|
|
||||||
final updatedChallengerDoTs = <DotEffect>[];
|
|
||||||
for (final dot in challengerDoTs) {
|
|
||||||
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
|
||||||
if (ticksTriggered > 0) {
|
|
||||||
dotDamageToOpponent += dot.damagePerTick * ticksTriggered;
|
|
||||||
}
|
|
||||||
if (updatedDot.isActive) {
|
|
||||||
updatedChallengerDoTs.add(updatedDot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
challengerDoTs = updatedChallengerDoTs;
|
|
||||||
|
|
||||||
if (dotDamageToOpponent > 0 && opponentCombatStats.hpCurrent > 0) {
|
|
||||||
opponentCombatStats = opponentCombatStats.copyWith(
|
|
||||||
hpCurrent: (opponentCombatStats.hpCurrent - dotDamageToOpponent)
|
|
||||||
.clamp(0, opponentCombatStats.hpMax),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOT 틱 처리 (상대 → 도전자에게 적용된 DOT)
|
|
||||||
var dotDamageToChallenger = 0;
|
|
||||||
final updatedOpponentDoTs = <DotEffect>[];
|
|
||||||
for (final dot in opponentDoTs) {
|
|
||||||
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
|
||||||
if (ticksTriggered > 0) {
|
|
||||||
dotDamageToChallenger += dot.damagePerTick * ticksTriggered;
|
|
||||||
}
|
|
||||||
if (updatedDot.isActive) {
|
|
||||||
updatedOpponentDoTs.add(updatedDot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opponentDoTs = updatedOpponentDoTs;
|
|
||||||
|
|
||||||
if (dotDamageToChallenger > 0 && playerCombatStats.isAlive) {
|
|
||||||
playerCombatStats = playerCombatStats.copyWith(
|
|
||||||
hpCurrent: (playerCombatStats.hpCurrent - dotDamageToChallenger)
|
|
||||||
.clamp(0, playerCombatStats.hpMax),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 만료된 디버프 정리
|
|
||||||
// =========================================================================
|
|
||||||
challengerDebuffs = challengerDebuffs
|
|
||||||
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
|
||||||
.toList();
|
|
||||||
opponentDebuffs = opponentDebuffs
|
|
||||||
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 도전자 턴 (selectAutoSkill 사용 - 일반 전투와 동일)
|
|
||||||
// =========================================================================
|
|
||||||
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
|
||||||
playerAccum = 0;
|
|
||||||
|
|
||||||
// 상대 몬스터 스탯 동기화
|
|
||||||
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
||||||
opponentCombatStats,
|
|
||||||
match.opponent.characterName,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 스킬 자동 선택 (progress_service와 동일한 로직)
|
|
||||||
final selectedSkill = _skillService.selectAutoSkill(
|
|
||||||
player: playerCombatStats,
|
|
||||||
monster: opponentMonsterStats,
|
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
availableSkillIds: challengerSkillIds,
|
|
||||||
activeDoTs: challengerDoTs,
|
|
||||||
activeDebuffs: opponentDebuffs,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
|
||||||
// 스킬 랭크 조회 및 적용
|
|
||||||
final skillRank = _getSkillRankFromEntry(
|
|
||||||
match.challenger,
|
|
||||||
selectedSkill.id,
|
|
||||||
);
|
|
||||||
final skillResult = _skillService.useAttackSkillWithRank(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: playerCombatStats,
|
|
||||||
monster: opponentMonsterStats,
|
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
rank: skillRank,
|
|
||||||
);
|
|
||||||
playerCombatStats = skillResult.updatedPlayer;
|
|
||||||
opponentCombatStats = opponentCombatStats.copyWith(
|
|
||||||
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
|
||||||
);
|
|
||||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
challengerSkillUsed = selectedSkill.name;
|
|
||||||
challengerDamage = skillResult.result.damage;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isDot) {
|
|
||||||
// DOT 스킬 사용
|
|
||||||
final skillResult = _skillService.useDotSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: playerCombatStats,
|
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
playerInt: playerCombatStats.atk ~/ 10,
|
|
||||||
playerWis: playerCombatStats.def ~/ 10,
|
|
||||||
);
|
|
||||||
playerCombatStats = skillResult.updatedPlayer;
|
|
||||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
if (skillResult.dotEffect != null) {
|
|
||||||
challengerDoTs.add(skillResult.dotEffect!);
|
|
||||||
}
|
|
||||||
challengerSkillUsed = selectedSkill.name;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
|
||||||
// 회복 스킬 사용
|
|
||||||
final skillResult = _skillService.useHealSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: playerCombatStats,
|
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
);
|
|
||||||
playerCombatStats = skillResult.updatedPlayer;
|
|
||||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
challengerSkillUsed = selectedSkill.name;
|
|
||||||
challengerHealAmount = skillResult.result.healedAmount;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
|
||||||
// 버프 스킬 사용
|
|
||||||
final skillResult = _skillService.useBuffSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: playerCombatStats,
|
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
);
|
|
||||||
playerCombatStats = skillResult.updatedPlayer;
|
|
||||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
challengerSkillUsed = selectedSkill.name;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
|
||||||
// 디버프 스킬 사용
|
|
||||||
final skillResult = _skillService.useDebuffSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: playerCombatStats,
|
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
currentDebuffs: opponentDebuffs,
|
|
||||||
);
|
|
||||||
playerCombatStats = skillResult.updatedPlayer;
|
|
||||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
final debuffEffect = skillResult.debuffEffect;
|
|
||||||
if (debuffEffect != null) {
|
|
||||||
opponentDebuffs =
|
|
||||||
opponentDebuffs
|
|
||||||
.where(
|
|
||||||
(ActiveBuff d) => d.effect.id != debuffEffect.effect.id,
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
..add(debuffEffect);
|
|
||||||
}
|
|
||||||
challengerSkillUsed = selectedSkill.name;
|
|
||||||
} else {
|
|
||||||
// 일반 공격
|
|
||||||
final result = calculator.playerAttackMonster(
|
|
||||||
attacker: playerCombatStats,
|
|
||||||
defender: opponentMonsterStats,
|
|
||||||
);
|
|
||||||
opponentCombatStats = opponentCombatStats.copyWith(
|
|
||||||
hpCurrent: result.updatedDefender.hpCurrent,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.result.isHit) {
|
|
||||||
challengerDamage = result.result.damage;
|
|
||||||
isChallengerCritical = result.result.isCritical;
|
|
||||||
} else {
|
|
||||||
isOpponentEvaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 상대 턴 (selectAutoSkill 사용 - 일반 전투와 동일)
|
|
||||||
// =========================================================================
|
|
||||||
if (opponentCombatStats.hpCurrent > 0 &&
|
|
||||||
opponentAccum >= opponentCombatStats.attackDelayMs) {
|
|
||||||
opponentAccum = 0;
|
|
||||||
|
|
||||||
// 도전자 몬스터 스탯 동기화
|
|
||||||
challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
||||||
playerCombatStats,
|
|
||||||
match.challenger.characterName,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 스킬 자동 선택 (progress_service와 동일한 로직)
|
|
||||||
final selectedSkill = _skillService.selectAutoSkill(
|
|
||||||
player: opponentCombatStats,
|
|
||||||
monster: challengerMonsterStats,
|
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
availableSkillIds: opponentSkillIds,
|
|
||||||
activeDoTs: opponentDoTs,
|
|
||||||
activeDebuffs: challengerDebuffs,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
|
||||||
// 스킬 랭크 조회 및 적용
|
|
||||||
final skillRank = _getSkillRankFromEntry(
|
|
||||||
match.opponent,
|
|
||||||
selectedSkill.id,
|
|
||||||
);
|
|
||||||
final skillResult = _skillService.useAttackSkillWithRank(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: opponentCombatStats,
|
|
||||||
monster: challengerMonsterStats,
|
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
rank: skillRank,
|
|
||||||
);
|
|
||||||
opponentCombatStats = skillResult.updatedPlayer;
|
|
||||||
playerCombatStats = playerCombatStats.copyWith(
|
|
||||||
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
|
||||||
);
|
|
||||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
opponentSkillUsed = selectedSkill.name;
|
|
||||||
opponentDamage = skillResult.result.damage;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isDot) {
|
|
||||||
// DOT 스킬 사용
|
|
||||||
final skillResult = _skillService.useDotSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: opponentCombatStats,
|
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
playerInt: opponentCombatStats.atk ~/ 10,
|
|
||||||
playerWis: opponentCombatStats.def ~/ 10,
|
|
||||||
);
|
|
||||||
opponentCombatStats = skillResult.updatedPlayer;
|
|
||||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
if (skillResult.dotEffect != null) {
|
|
||||||
opponentDoTs.add(skillResult.dotEffect!);
|
|
||||||
}
|
|
||||||
opponentSkillUsed = selectedSkill.name;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
|
||||||
// 회복 스킬 사용
|
|
||||||
final skillResult = _skillService.useHealSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: opponentCombatStats,
|
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
);
|
|
||||||
opponentCombatStats = skillResult.updatedPlayer;
|
|
||||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
opponentSkillUsed = selectedSkill.name;
|
|
||||||
opponentHealAmount = skillResult.result.healedAmount;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
|
||||||
// 버프 스킬 사용
|
|
||||||
final skillResult = _skillService.useBuffSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: opponentCombatStats,
|
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
);
|
|
||||||
opponentCombatStats = skillResult.updatedPlayer;
|
|
||||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
opponentSkillUsed = selectedSkill.name;
|
|
||||||
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
|
||||||
// 디버프 스킬 사용
|
|
||||||
final skillResult = _skillService.useDebuffSkill(
|
|
||||||
skill: selectedSkill,
|
|
||||||
player: opponentCombatStats,
|
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
currentDebuffs: challengerDebuffs,
|
|
||||||
);
|
|
||||||
opponentCombatStats = skillResult.updatedPlayer;
|
|
||||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
|
||||||
final debuffEffect = skillResult.debuffEffect;
|
|
||||||
if (debuffEffect != null) {
|
|
||||||
challengerDebuffs =
|
|
||||||
challengerDebuffs
|
|
||||||
.where(
|
|
||||||
(ActiveBuff d) => d.effect.id != debuffEffect.effect.id,
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
..add(debuffEffect);
|
|
||||||
}
|
|
||||||
opponentSkillUsed = selectedSkill.name;
|
|
||||||
} else {
|
|
||||||
// 일반 공격 (디버프 효과 적용)
|
|
||||||
var debuffedOpponent = opponentCombatStats;
|
|
||||||
if (challengerDebuffs.isNotEmpty) {
|
|
||||||
double atkMod = 0;
|
|
||||||
for (final debuff in challengerDebuffs) {
|
|
||||||
if (!debuff.isExpired(elapsedMs)) {
|
|
||||||
atkMod += debuff.effect.atkModifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final newAtk = (opponentCombatStats.atk * (1 + atkMod))
|
|
||||||
.round()
|
|
||||||
.clamp(opponentCombatStats.atk ~/ 10, opponentCombatStats.atk);
|
|
||||||
debuffedOpponent = opponentCombatStats.copyWith(atk: newAtk);
|
|
||||||
}
|
|
||||||
|
|
||||||
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
|
||||||
debuffedOpponent,
|
|
||||||
match.opponent.characterName,
|
|
||||||
);
|
|
||||||
final result = calculator.monsterAttackPlayer(
|
|
||||||
attacker: opponentMonsterStats,
|
|
||||||
defender: playerCombatStats,
|
|
||||||
);
|
|
||||||
playerCombatStats = result.updatedDefender;
|
|
||||||
|
|
||||||
if (result.result.isHit) {
|
|
||||||
opponentDamage = result.result.damage;
|
|
||||||
isOpponentCritical = result.result.isCritical;
|
|
||||||
isChallengerBlocked = result.result.isBlocked;
|
|
||||||
} else {
|
|
||||||
isChallengerEvaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 액션이 발생했을 때만 턴 전송
|
|
||||||
final hasAction =
|
|
||||||
challengerDamage != null ||
|
|
||||||
opponentDamage != null ||
|
|
||||||
challengerHealAmount != null ||
|
|
||||||
opponentHealAmount != null ||
|
|
||||||
challengerSkillUsed != null ||
|
|
||||||
opponentSkillUsed != null;
|
|
||||||
|
|
||||||
if (hasAction) {
|
|
||||||
turns++;
|
|
||||||
yield ArenaCombatTurn(
|
|
||||||
challengerDamage: challengerDamage,
|
|
||||||
opponentDamage: opponentDamage,
|
|
||||||
challengerHp: playerCombatStats.hpCurrent,
|
|
||||||
opponentHp: opponentCombatStats.hpCurrent,
|
|
||||||
challengerHpMax: playerCombatStats.hpMax,
|
|
||||||
opponentHpMax: opponentCombatStats.hpMax,
|
|
||||||
challengerMp: playerCombatStats.mpCurrent,
|
|
||||||
opponentMp: opponentCombatStats.mpCurrent,
|
|
||||||
challengerMpMax: playerCombatStats.mpMax,
|
|
||||||
opponentMpMax: opponentCombatStats.mpMax,
|
|
||||||
isChallengerCritical: isChallengerCritical,
|
|
||||||
isOpponentCritical: isOpponentCritical,
|
|
||||||
isChallengerEvaded: isChallengerEvaded,
|
|
||||||
isOpponentEvaded: isOpponentEvaded,
|
|
||||||
isChallengerBlocked: isChallengerBlocked,
|
|
||||||
isOpponentBlocked: isOpponentBlocked,
|
|
||||||
challengerSkillUsed: challengerSkillUsed,
|
|
||||||
opponentSkillUsed: opponentSkillUsed,
|
|
||||||
challengerHealAmount: challengerHealAmount,
|
|
||||||
opponentHealAmount: opponentHealAmount,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 애니메이션을 위한 딜레이
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 무한 루프 방지
|
|
||||||
if (turns > 1000) break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AI 베팅 슬롯 선택
|
// AI 베팅 슬롯 선택
|
||||||
|
|||||||
@@ -2,6 +2,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/model/class_traits.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/player_attack_processor.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';
|
||||||
@@ -126,7 +127,8 @@ class CombatTickService {
|
|||||||
|
|
||||||
// 플레이어 공격 체크
|
// 플레이어 공격 체크
|
||||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
final attackResult = _processPlayerAttack(
|
final attackProcessor = PlayerAttackProcessor(rng: rng);
|
||||||
|
final attackResult = attackProcessor.processAttack(
|
||||||
state: state,
|
state: state,
|
||||||
playerStats: playerStats,
|
playerStats: playerStats,
|
||||||
monsterStats: monsterStats,
|
monsterStats: monsterStats,
|
||||||
@@ -363,249 +365,6 @@ class CombatTickService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 플레이어 공격 처리
|
|
||||||
({
|
|
||||||
CombatStats playerStats,
|
|
||||||
MonsterCombatStats monsterStats,
|
|
||||||
SkillSystemState skillSystem,
|
|
||||||
List<DotEffect> activeDoTs,
|
|
||||||
List<ActiveBuff> activeDebuffs,
|
|
||||||
int totalDamageDealt,
|
|
||||||
List<CombatEvent> events,
|
|
||||||
bool isFirstPlayerAttack,
|
|
||||||
})
|
|
||||||
_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,
|
|
||||||
required bool isFirstPlayerAttack,
|
|
||||||
required double firstStrikeBonus,
|
|
||||||
required bool hasMultiAttack,
|
|
||||||
double healingMultiplier = 1.0,
|
|
||||||
}) {
|
|
||||||
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,
|
|
||||||
healingMultiplier: healingMultiplier,
|
|
||||||
);
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 첫 공격 배율 적용 (예: 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) {
|
|
||||||
events.add(
|
|
||||||
CombatEvent.monsterEvade(
|
|
||||||
timestamp: timestamp,
|
|
||||||
targetName: newMonsterStats.name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
events.add(
|
|
||||||
CombatEvent.playerAttack(
|
|
||||||
timestamp: timestamp,
|
|
||||||
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 (
|
|
||||||
playerStats: newPlayerStats,
|
|
||||||
monsterStats: newMonsterStats,
|
|
||||||
skillSystem: newSkillSystem,
|
|
||||||
activeDoTs: newActiveDoTs,
|
|
||||||
activeDebuffs: newActiveBuffs,
|
|
||||||
totalDamageDealt: newTotalDamageDealt,
|
|
||||||
events: events,
|
|
||||||
isFirstPlayerAttack: false, // 첫 공격 이후에는 false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 몬스터 공격 처리
|
/// 몬스터 공격 처리
|
||||||
({CombatStats playerStats, int totalDamageTaken, List<CombatEvent> events})
|
({CombatStats playerStats, int totalDamageTaken, List<CombatEvent> events})
|
||||||
_processMonsterAttack({
|
_processMonsterAttack({
|
||||||
|
|||||||
172
lib/src/core/engine/death_handler.dart
Normal file
172
lib/src/core/engine/death_handler.dart
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// 플레이어 사망 처리 서비스
|
||||||
|
///
|
||||||
|
/// ProgressService에서 분리된 사망 관련 로직 담당:
|
||||||
|
/// - 장비 손실 계산
|
||||||
|
/// - 사망 정보 기록
|
||||||
|
/// - 보스전 레벨링 모드 진입
|
||||||
|
class DeathHandler {
|
||||||
|
const DeathHandler();
|
||||||
|
|
||||||
|
/// 플레이어 사망 처리 (Phase 4)
|
||||||
|
///
|
||||||
|
/// 모든 장비 상실 및 사망 정보 기록.
|
||||||
|
/// 보스전 사망 시: 장비 보호 + 5분 레벨링 모드 진입.
|
||||||
|
GameState processPlayerDeath(
|
||||||
|
GameState state, {
|
||||||
|
required String killerName,
|
||||||
|
required DeathCause cause,
|
||||||
|
}) {
|
||||||
|
// 사망 직전 전투 이벤트 저장 (최대 10개)
|
||||||
|
final lastCombatEvents =
|
||||||
|
state.progress.currentCombat?.recentEvents ?? const [];
|
||||||
|
|
||||||
|
// 보스전 사망 여부 확인 (최종 보스 fighting 상태)
|
||||||
|
final isBossDeath =
|
||||||
|
state.progress.finalBossState == FinalBossState.fighting;
|
||||||
|
|
||||||
|
// 보스전 사망이 아닐 경우에만 장비 손실
|
||||||
|
var newEquipment = state.equipment;
|
||||||
|
var lostCount = 0;
|
||||||
|
String? lostItemName;
|
||||||
|
EquipmentSlot? lostItemSlot;
|
||||||
|
ItemRarity? lostItemRarity;
|
||||||
|
EquipmentItem? lostEquipmentItem; // 광고 부활 시 복구용
|
||||||
|
|
||||||
|
if (!isBossDeath) {
|
||||||
|
final lossResult = _calculateEquipmentLoss(state);
|
||||||
|
newEquipment = lossResult.equipment;
|
||||||
|
lostCount = lossResult.lostCount;
|
||||||
|
lostItemName = lossResult.lostItemName;
|
||||||
|
lostItemSlot = lossResult.lostItemSlot;
|
||||||
|
lostItemRarity = lossResult.lostItemRarity;
|
||||||
|
lostEquipmentItem = lossResult.lostItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사망 정보 생성 (전투 로그 포함)
|
||||||
|
final deathInfo = DeathInfo(
|
||||||
|
cause: cause,
|
||||||
|
killerName: killerName,
|
||||||
|
lostEquipmentCount: lostCount,
|
||||||
|
lostItemName: lostItemName,
|
||||||
|
lostItemSlot: lostItemSlot,
|
||||||
|
lostItemRarity: lostItemRarity,
|
||||||
|
lostItem: lostEquipmentItem,
|
||||||
|
goldAtDeath: state.inventory.gold,
|
||||||
|
levelAtDeath: state.traits.level,
|
||||||
|
timestamp: state.skillSystem.elapsedMs,
|
||||||
|
lastCombatEvents: lastCombatEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 보스전 사망 시 5분 레벨링 모드 진입
|
||||||
|
final bossLevelingEndTime = isBossDeath
|
||||||
|
? DateTime.now().millisecondsSinceEpoch +
|
||||||
|
(5 * 60 * 1000) // 5분
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 전투 상태 초기화 및 사망 횟수 증가
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
currentCombat: null,
|
||||||
|
deathCount: state.progress.deathCount + 1,
|
||||||
|
bossLevelingEndTime: bossLevelingEndTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
equipment: newEquipment,
|
||||||
|
progress: progress,
|
||||||
|
deathInfo: deathInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 손실 계산
|
||||||
|
({
|
||||||
|
Equipment equipment,
|
||||||
|
int lostCount,
|
||||||
|
String? lostItemName,
|
||||||
|
EquipmentSlot? lostItemSlot,
|
||||||
|
ItemRarity? lostItemRarity,
|
||||||
|
EquipmentItem? lostItem,
|
||||||
|
})
|
||||||
|
_calculateEquipmentLoss(GameState state) {
|
||||||
|
var newEquipment = state.equipment;
|
||||||
|
|
||||||
|
// 레벨 기반 장비 손실 확률 계산
|
||||||
|
// Lv 1: 20%, Lv 5: ~56%, Lv 10+: 100%
|
||||||
|
// 공식: 20 + (level - 1) * 80 / 9
|
||||||
|
final level = state.traits.level;
|
||||||
|
final lossChancePercent = level >= 10
|
||||||
|
? 100
|
||||||
|
: (20 + ((level - 1) * 80 ~/ 9)).clamp(0, 100);
|
||||||
|
final roll = state.rng.nextInt(100); // 0~99
|
||||||
|
final shouldLoseEquipment = roll < lossChancePercent;
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
print(
|
||||||
|
'[Death] Lv$level lossChance=$lossChancePercent% roll=$roll '
|
||||||
|
'shouldLose=$shouldLoseEquipment',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldLoseEquipment) {
|
||||||
|
return (
|
||||||
|
equipment: newEquipment,
|
||||||
|
lostCount: 0,
|
||||||
|
lostItemName: null,
|
||||||
|
lostItemSlot: null,
|
||||||
|
lostItemRarity: null,
|
||||||
|
lostItem: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
|
||||||
|
final equippedNonWeaponSlots = <int>[];
|
||||||
|
for (var i = 1; i < Equipment.slotCount; i++) {
|
||||||
|
final item = state.equipment.getItemByIndex(i);
|
||||||
|
if (item.isNotEmpty) {
|
||||||
|
equippedNonWeaponSlots.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equippedNonWeaponSlots.isEmpty) {
|
||||||
|
return (
|
||||||
|
equipment: newEquipment,
|
||||||
|
lostCount: 0,
|
||||||
|
lostItemName: null,
|
||||||
|
lostItemSlot: null,
|
||||||
|
lostItemRarity: null,
|
||||||
|
lostItem: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 랜덤하게 1개 슬롯 선택
|
||||||
|
final sacrificeIndex =
|
||||||
|
equippedNonWeaponSlots[state.rng.nextInt(
|
||||||
|
equippedNonWeaponSlots.length,
|
||||||
|
)];
|
||||||
|
|
||||||
|
// 제물로 바칠 아이템 정보 저장
|
||||||
|
final lostItem = state.equipment.getItemByIndex(sacrificeIndex);
|
||||||
|
final lostItemSlot = EquipmentSlot.values[sacrificeIndex];
|
||||||
|
|
||||||
|
// 해당 슬롯을 빈 장비로 교체
|
||||||
|
newEquipment = newEquipment.setItemByIndex(
|
||||||
|
sacrificeIndex,
|
||||||
|
EquipmentItem.empty(lostItemSlot),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[Death] Lost item: ${lostItem.name} (slot: $lostItemSlot)');
|
||||||
|
|
||||||
|
return (
|
||||||
|
equipment: newEquipment,
|
||||||
|
lostCount: 1,
|
||||||
|
lostItemName: lostItem.name,
|
||||||
|
lostItemSlot: lostItemSlot,
|
||||||
|
lostItemRarity: lostItem.rarity,
|
||||||
|
lostItem: lostItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/src/core/engine/loot_handler.dart
Normal file
80
lib/src/core/engine/loot_handler.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/potion_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||||
|
|
||||||
|
/// 전리품 처리 서비스
|
||||||
|
///
|
||||||
|
/// ProgressService에서 분리된 전리품 획득 로직 담당:
|
||||||
|
/// - 몬스터 부위 아이템 인벤토리 추가
|
||||||
|
/// - 특수 아이템 획득 (WinItem)
|
||||||
|
/// - 물약 드랍
|
||||||
|
class LootHandler {
|
||||||
|
const LootHandler({required this.mutations});
|
||||||
|
|
||||||
|
final GameMutations mutations;
|
||||||
|
|
||||||
|
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||||
|
({GameState state, Potion? droppedPotion}) winLoot(GameState state) {
|
||||||
|
final taskInfo = state.progress.currentTask;
|
||||||
|
final monsterPart = taskInfo.monsterPart ?? '';
|
||||||
|
final monsterBaseName = taskInfo.monsterBaseName ?? '';
|
||||||
|
|
||||||
|
var resultState = state;
|
||||||
|
|
||||||
|
// 부위가 '*'이면 WinItem 호출 (특수 아이템)
|
||||||
|
if (monsterPart == '*') {
|
||||||
|
resultState = mutations.winItem(resultState);
|
||||||
|
} else if (monsterPart.isNotEmpty && monsterBaseName.isNotEmpty) {
|
||||||
|
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
||||||
|
// ProperCase(Split(fTask.Caption,3))), 1);
|
||||||
|
// 예: "goblin Claw" 형태로 인벤토리 추가
|
||||||
|
final itemName =
|
||||||
|
'${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}';
|
||||||
|
|
||||||
|
// 인벤토리에 추가
|
||||||
|
final items = [...resultState.inventory.items];
|
||||||
|
final existing = items.indexWhere((e) => e.name == itemName);
|
||||||
|
if (existing >= 0) {
|
||||||
|
items[existing] = items[existing].copyWith(
|
||||||
|
count: items[existing].count + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items.add(InventoryEntry(name: itemName, count: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
resultState = resultState.copyWith(
|
||||||
|
inventory: resultState.inventory.copyWith(items: items),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 물약 드랍 시도
|
||||||
|
final potionService = const PotionService();
|
||||||
|
final rng = resultState.rng;
|
||||||
|
final monsterLevel = taskInfo.monsterLevel ?? resultState.traits.level;
|
||||||
|
final monsterGrade = taskInfo.monsterGrade ?? MonsterGrade.normal;
|
||||||
|
final (updatedPotionInventory, droppedPotion) = potionService.tryPotionDrop(
|
||||||
|
playerLevel: resultState.traits.level,
|
||||||
|
monsterLevel: monsterLevel,
|
||||||
|
monsterGrade: monsterGrade,
|
||||||
|
inventory: resultState.potionInventory,
|
||||||
|
roll: rng.nextInt(100),
|
||||||
|
typeRoll: rng.nextInt(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
state: resultState.copyWith(
|
||||||
|
rng: rng,
|
||||||
|
potionInventory: updatedPotionInventory,
|
||||||
|
),
|
||||||
|
droppedPotion: droppedPotion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 첫 글자만 대문자로 변환 (원본 ProperCase)
|
||||||
|
String _properCase(String s) {
|
||||||
|
if (s.isEmpty) return s;
|
||||||
|
return s[0].toUpperCase() + s.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
411
lib/src/core/engine/player_attack_processor.dart
Normal file
411
lib/src/core/engine/player_attack_processor.dart
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import 'package:asciineverdie/data/skill_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/combat_calculator.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_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/skill.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 플레이어 공격 처리 결과
|
||||||
|
typedef PlayerAttackResult = ({
|
||||||
|
CombatStats playerStats,
|
||||||
|
MonsterCombatStats monsterStats,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
List<DotEffect> activeDoTs,
|
||||||
|
List<ActiveBuff> activeDebuffs,
|
||||||
|
int totalDamageDealt,
|
||||||
|
List<CombatEvent> events,
|
||||||
|
bool isFirstPlayerAttack,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 플레이어 공격 처리 서비스
|
||||||
|
///
|
||||||
|
/// CombatTickService에서 분리된 플레이어 공격 로직 담당:
|
||||||
|
/// - 스킬 자동 선택 및 사용
|
||||||
|
/// - 일반 공격 처리
|
||||||
|
/// - 첫 공격 보너스
|
||||||
|
/// - 연속 공격 (Multi-attack)
|
||||||
|
class PlayerAttackProcessor {
|
||||||
|
PlayerAttackProcessor({required this.rng});
|
||||||
|
|
||||||
|
final DeterministicRandom rng;
|
||||||
|
|
||||||
|
/// 플레이어 공격 처리
|
||||||
|
PlayerAttackResult processAttack({
|
||||||
|
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,
|
||||||
|
required bool isFirstPlayerAttack,
|
||||||
|
required double firstStrikeBonus,
|
||||||
|
required bool hasMultiAttack,
|
||||||
|
double healingMultiplier = 1.0,
|
||||||
|
}) {
|
||||||
|
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 result = _useAttackSkill(
|
||||||
|
state: state,
|
||||||
|
skill: selectedSkill,
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
monsterStats: newMonsterStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
skillService: skillService,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
newPlayerStats = result.playerStats;
|
||||||
|
newMonsterStats = result.monsterStats;
|
||||||
|
newTotalDamageDealt += result.damage;
|
||||||
|
newSkillSystem = result.skillSystem;
|
||||||
|
events.add(result.event);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||||
|
final result = _useDotSkill(
|
||||||
|
state: state,
|
||||||
|
skill: selectedSkill,
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
skillService: skillService,
|
||||||
|
monsterName: newMonsterStats.name,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
newPlayerStats = result.playerStats;
|
||||||
|
newSkillSystem = result.skillSystem;
|
||||||
|
if (result.dotEffect != null) newActiveDoTs.add(result.dotEffect!);
|
||||||
|
events.add(result.event);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
|
final result = _useHealSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
skillService: skillService,
|
||||||
|
healingMultiplier: healingMultiplier,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
newPlayerStats = result.playerStats;
|
||||||
|
newSkillSystem = result.skillSystem;
|
||||||
|
events.add(result.event);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
|
final result = skillService.useBuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
);
|
||||||
|
newPlayerStats = result.updatedPlayer;
|
||||||
|
newSkillSystem = result.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
events.add(
|
||||||
|
CombatEvent.playerBuff(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: selectedSkill.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
||||||
|
final result = _useDebuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
skillService: skillService,
|
||||||
|
activeDebuffs: newActiveBuffs,
|
||||||
|
monsterName: newMonsterStats.name,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
newPlayerStats = result.playerStats;
|
||||||
|
newSkillSystem = result.skillSystem;
|
||||||
|
newActiveBuffs = result.activeDebuffs;
|
||||||
|
events.add(result.event);
|
||||||
|
} else {
|
||||||
|
// 일반 공격
|
||||||
|
final result = _processNormalAttack(
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
monsterStats: newMonsterStats,
|
||||||
|
calculator: calculator,
|
||||||
|
isFirstPlayerAttack: isFirstPlayerAttack,
|
||||||
|
firstStrikeBonus: firstStrikeBonus,
|
||||||
|
hasMultiAttack: hasMultiAttack,
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
newMonsterStats = result.monsterStats;
|
||||||
|
newTotalDamageDealt += result.totalDamage;
|
||||||
|
events.addAll(result.events);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
playerStats: newPlayerStats,
|
||||||
|
monsterStats: newMonsterStats,
|
||||||
|
skillSystem: newSkillSystem,
|
||||||
|
activeDoTs: newActiveDoTs,
|
||||||
|
activeDebuffs: newActiveBuffs,
|
||||||
|
totalDamageDealt: newTotalDamageDealt,
|
||||||
|
events: events,
|
||||||
|
isFirstPlayerAttack: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스킬 사용 헬퍼
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
({
|
||||||
|
CombatStats playerStats,
|
||||||
|
MonsterCombatStats monsterStats,
|
||||||
|
int damage,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
CombatEvent event,
|
||||||
|
})
|
||||||
|
_useAttackSkill({
|
||||||
|
required GameState state,
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required MonsterCombatStats monsterStats,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required SkillService skillService,
|
||||||
|
required int timestamp,
|
||||||
|
}) {
|
||||||
|
final skillRank = skillService.getSkillRankFromSkillBook(
|
||||||
|
state.skillBook,
|
||||||
|
skill.id,
|
||||||
|
);
|
||||||
|
final skillResult = skillService.useAttackSkillWithRank(
|
||||||
|
skill: skill,
|
||||||
|
player: playerStats,
|
||||||
|
monster: monsterStats,
|
||||||
|
skillSystem: skillSystem,
|
||||||
|
rank: skillRank,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
playerStats: skillResult.updatedPlayer,
|
||||||
|
monsterStats: skillResult.updatedMonster,
|
||||||
|
damage: skillResult.result.damage,
|
||||||
|
skillSystem: skillResult.updatedSkillSystem.startGlobalCooldown(),
|
||||||
|
event: CombatEvent.playerSkill(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: skill.name,
|
||||||
|
damage: skillResult.result.damage,
|
||||||
|
targetName: monsterStats.name,
|
||||||
|
attackDelayMs: playerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
({
|
||||||
|
CombatStats playerStats,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
DotEffect? dotEffect,
|
||||||
|
CombatEvent event,
|
||||||
|
})
|
||||||
|
_useDotSkill({
|
||||||
|
required GameState state,
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required SkillService skillService,
|
||||||
|
required String monsterName,
|
||||||
|
required int timestamp,
|
||||||
|
}) {
|
||||||
|
final skillResult = skillService.useDotSkill(
|
||||||
|
skill: skill,
|
||||||
|
player: playerStats,
|
||||||
|
skillSystem: skillSystem,
|
||||||
|
playerInt: state.stats.intelligence,
|
||||||
|
playerWis: state.stats.wis,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
playerStats: skillResult.updatedPlayer,
|
||||||
|
skillSystem: skillResult.updatedSkillSystem.startGlobalCooldown(),
|
||||||
|
dotEffect: skillResult.dotEffect,
|
||||||
|
event: CombatEvent.playerSkill(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: skill.name,
|
||||||
|
damage: skillResult.result.damage,
|
||||||
|
targetName: monsterName,
|
||||||
|
attackDelayMs: playerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
({CombatStats playerStats, SkillSystemState skillSystem, CombatEvent event})
|
||||||
|
_useHealSkill({
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required SkillService skillService,
|
||||||
|
required double healingMultiplier,
|
||||||
|
required int timestamp,
|
||||||
|
}) {
|
||||||
|
final skillResult = skillService.useHealSkill(
|
||||||
|
skill: skill,
|
||||||
|
player: playerStats,
|
||||||
|
skillSystem: skillSystem,
|
||||||
|
healingMultiplier: healingMultiplier,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
playerStats: skillResult.updatedPlayer,
|
||||||
|
skillSystem: skillResult.updatedSkillSystem.startGlobalCooldown(),
|
||||||
|
event: CombatEvent.playerHeal(
|
||||||
|
timestamp: timestamp,
|
||||||
|
healAmount: skillResult.result.healedAmount,
|
||||||
|
skillName: skill.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
({
|
||||||
|
CombatStats playerStats,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
List<ActiveBuff> activeDebuffs,
|
||||||
|
CombatEvent event,
|
||||||
|
})
|
||||||
|
_useDebuffSkill({
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required SkillService skillService,
|
||||||
|
required List<ActiveBuff> activeDebuffs,
|
||||||
|
required String monsterName,
|
||||||
|
required int timestamp,
|
||||||
|
}) {
|
||||||
|
final skillResult = skillService.useDebuffSkill(
|
||||||
|
skill: skill,
|
||||||
|
player: playerStats,
|
||||||
|
skillSystem: skillSystem,
|
||||||
|
currentDebuffs: activeDebuffs,
|
||||||
|
);
|
||||||
|
var newDebuffs = activeDebuffs;
|
||||||
|
if (skillResult.debuffEffect != null) {
|
||||||
|
newDebuffs =
|
||||||
|
activeDebuffs
|
||||||
|
.where((d) => d.effect.id != skillResult.debuffEffect!.effect.id)
|
||||||
|
.toList()
|
||||||
|
..add(skillResult.debuffEffect!);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
playerStats: skillResult.updatedPlayer,
|
||||||
|
skillSystem: skillResult.updatedSkillSystem.startGlobalCooldown(),
|
||||||
|
activeDebuffs: newDebuffs,
|
||||||
|
event: CombatEvent.playerDebuff(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: skill.name,
|
||||||
|
targetName: monsterName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 일반 공격
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
({MonsterCombatStats monsterStats, int totalDamage, List<CombatEvent> events})
|
||||||
|
_processNormalAttack({
|
||||||
|
required CombatStats playerStats,
|
||||||
|
required MonsterCombatStats monsterStats,
|
||||||
|
required CombatCalculator calculator,
|
||||||
|
required bool isFirstPlayerAttack,
|
||||||
|
required double firstStrikeBonus,
|
||||||
|
required bool hasMultiAttack,
|
||||||
|
required int timestamp,
|
||||||
|
}) {
|
||||||
|
final events = <CombatEvent>[];
|
||||||
|
var newMonsterStats = monsterStats;
|
||||||
|
var totalDamage = 0;
|
||||||
|
|
||||||
|
final attackResult = calculator.playerAttackMonster(
|
||||||
|
attacker: playerStats,
|
||||||
|
defender: newMonsterStats,
|
||||||
|
);
|
||||||
|
newMonsterStats = attackResult.updatedDefender;
|
||||||
|
|
||||||
|
// 첫 공격 배율 적용
|
||||||
|
var damage = attackResult.result.damage;
|
||||||
|
if (isFirstPlayerAttack && firstStrikeBonus > 1.0) {
|
||||||
|
damage = (damage * firstStrikeBonus).round();
|
||||||
|
final extraDamage = damage - attackResult.result.damage;
|
||||||
|
if (extraDamage > 0) {
|
||||||
|
final newHp = (newMonsterStats.hpCurrent - extraDamage).clamp(
|
||||||
|
0,
|
||||||
|
newMonsterStats.hpMax,
|
||||||
|
);
|
||||||
|
newMonsterStats = newMonsterStats.copyWith(hpCurrent: newHp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalDamage += 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: damage,
|
||||||
|
targetName: newMonsterStats.name,
|
||||||
|
isCritical: result.isCritical,
|
||||||
|
attackDelayMs: playerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연속 공격 (Refactor Monk 패시브) - 30% 확률로 추가 공격
|
||||||
|
if (hasMultiAttack && newMonsterStats.isAlive && rng.nextDouble() < 0.3) {
|
||||||
|
final extraAttack = calculator.playerAttackMonster(
|
||||||
|
attacker: playerStats,
|
||||||
|
defender: newMonsterStats,
|
||||||
|
);
|
||||||
|
newMonsterStats = extraAttack.updatedDefender;
|
||||||
|
totalDamage += 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: playerStats.attackDelayMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
monsterStats: newMonsterStats,
|
||||||
|
totalDamage: totalDamage,
|
||||||
|
events: events,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,18 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:asciineverdie/data/class_data.dart';
|
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/data/race_data.dart';
|
||||||
import 'package:asciineverdie/src/core/model/class_traits.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/act_progression_service.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/combat_tick_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/death_handler.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/loot_handler.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/market_service.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/reward_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/engine/task_generator.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.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/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/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/pq_config.dart';
|
||||||
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
@@ -58,12 +48,17 @@ class ProgressService {
|
|||||||
required this.config,
|
required this.config,
|
||||||
required this.mutations,
|
required this.mutations,
|
||||||
required this.rewards,
|
required this.rewards,
|
||||||
});
|
}) : _taskGenerator = TaskGenerator(config: config),
|
||||||
|
_lootHandler = LootHandler(mutations: mutations);
|
||||||
|
|
||||||
final PqConfig config;
|
final PqConfig config;
|
||||||
final GameMutations mutations;
|
final GameMutations mutations;
|
||||||
final RewardService rewards;
|
final RewardService rewards;
|
||||||
|
|
||||||
|
final TaskGenerator _taskGenerator;
|
||||||
|
final LootHandler _lootHandler;
|
||||||
|
static const _deathHandler = DeathHandler();
|
||||||
|
|
||||||
/// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767)
|
/// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767)
|
||||||
/// Prologue 태스크들을 큐에 추가하고 첫 태스크 시작
|
/// Prologue 태스크들을 큐에 추가하고 첫 태스크 시작
|
||||||
GameState initializeNewGame(GameState state) {
|
GameState initializeNewGame(GameState state) {
|
||||||
@@ -318,7 +313,7 @@ class ProgressService {
|
|||||||
// 플레이어 사망 체크
|
// 플레이어 사망 체크
|
||||||
if (!updatedCombat.playerStats.isAlive) {
|
if (!updatedCombat.playerStats.isAlive) {
|
||||||
final monsterName = updatedCombat.monsterStats.name;
|
final monsterName = updatedCombat.monsterStats.name;
|
||||||
nextState = _processPlayerDeath(
|
nextState = _deathHandler.processPlayerDeath(
|
||||||
state,
|
state,
|
||||||
killerName: monsterName,
|
killerName: monsterName,
|
||||||
cause: DeathCause.monster,
|
cause: DeathCause.monster,
|
||||||
@@ -380,7 +375,7 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 전리품 획득
|
// 전리품 획득
|
||||||
final lootResult = _winLoot(nextState);
|
final lootResult = _lootHandler.winLoot(nextState);
|
||||||
nextState = lootResult.state;
|
nextState = lootResult.state;
|
||||||
|
|
||||||
// 물약 드랍 로그 추가
|
// 물약 드랍 로그 추가
|
||||||
@@ -636,7 +631,7 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nextState = nextState.copyWith(progress: progress, queue: queue);
|
nextState = nextState.copyWith(progress: progress, queue: queue);
|
||||||
final newTaskResult = _generateNextTask(nextState);
|
final newTaskResult = _taskGenerator.generateNextTask(nextState);
|
||||||
progress = newTaskResult.progress;
|
progress = newTaskResult.progress;
|
||||||
queue = newTaskResult.queue;
|
queue = newTaskResult.queue;
|
||||||
}
|
}
|
||||||
@@ -650,241 +645,6 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 큐가 비어있을 때 다음 태스크 생성 (원본 Dequeue 667-684줄)
|
|
||||||
({ProgressState progress, QueueState queue}) _generateNextTask(
|
|
||||||
GameState state,
|
|
||||||
) {
|
|
||||||
var progress = state.progress;
|
|
||||||
final queue = state.queue;
|
|
||||||
final oldTaskType = progress.currentTask.type;
|
|
||||||
|
|
||||||
// 1. Encumbrance 초과 시 시장 이동
|
|
||||||
if (_shouldGoToMarket(progress)) {
|
|
||||||
return _createMarketTask(progress, queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 전환 태스크 (buying/heading)
|
|
||||||
if (_needsTransitionTask(oldTaskType)) {
|
|
||||||
return _createTransitionTask(state, progress, queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Act Boss 리트라이
|
|
||||||
if (state.progress.pendingActCompletion) {
|
|
||||||
return _createActBossRetryTask(state, progress, queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 최종 보스 전투
|
|
||||||
if (state.progress.finalBossState == FinalBossState.fighting &&
|
|
||||||
!state.progress.isInBossLevelingMode) {
|
|
||||||
if (state.progress.bossLevelingEndTime != null) {
|
|
||||||
progress = progress.copyWith(clearBossLevelingEndTime: true);
|
|
||||||
}
|
|
||||||
final actProgressionService = ActProgressionService(config: config);
|
|
||||||
return actProgressionService.startFinalBossFight(state, progress, queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 일반 몬스터 전투
|
|
||||||
return _createMonsterTask(state, progress, queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 시장 이동 조건 확인
|
|
||||||
bool _shouldGoToMarket(ProgressState progress) {
|
|
||||||
return progress.encumbrance.position >= progress.encumbrance.max &&
|
|
||||||
progress.encumbrance.max > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전환 태스크 필요 여부 확인
|
|
||||||
bool _needsTransitionTask(TaskType oldTaskType) {
|
|
||||||
return oldTaskType != TaskType.kill &&
|
|
||||||
oldTaskType != TaskType.neutral &&
|
|
||||||
oldTaskType != TaskType.buying;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 시장 이동 태스크 생성
|
|
||||||
({ProgressState progress, QueueState queue}) _createMarketTask(
|
|
||||||
ProgressState progress,
|
|
||||||
QueueState queue,
|
|
||||||
) {
|
|
||||||
final taskResult = pq_logic.startTask(
|
|
||||||
progress,
|
|
||||||
l10n.taskHeadingToMarket(),
|
|
||||||
4 * 1000,
|
|
||||||
);
|
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
|
||||||
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.market),
|
|
||||||
currentCombat: null,
|
|
||||||
);
|
|
||||||
return (progress: updatedProgress, queue: queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전환 태스크 생성 (buying 또는 heading)
|
|
||||||
({ProgressState progress, QueueState queue}) _createTransitionTask(
|
|
||||||
GameState state,
|
|
||||||
ProgressState progress,
|
|
||||||
QueueState queue,
|
|
||||||
) {
|
|
||||||
final gold = state.inventory.gold;
|
|
||||||
final equipPrice = state.traits.level * 50;
|
|
||||||
|
|
||||||
// Gold 충분 시 장비 구매
|
|
||||||
if (gold > equipPrice) {
|
|
||||||
final taskResult = pq_logic.startTask(
|
|
||||||
progress,
|
|
||||||
l10n.taskUpgradingHardware(),
|
|
||||||
5 * 1000,
|
|
||||||
);
|
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
|
||||||
currentTask: TaskInfo(
|
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.buying,
|
|
||||||
),
|
|
||||||
currentCombat: null,
|
|
||||||
);
|
|
||||||
return (progress: updatedProgress, queue: queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gold 부족 시 전장 이동
|
|
||||||
final taskResult = pq_logic.startTask(
|
|
||||||
progress,
|
|
||||||
l10n.taskEnteringDebugZone(),
|
|
||||||
4 * 1000,
|
|
||||||
);
|
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
|
||||||
currentTask: TaskInfo(
|
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.neutral,
|
|
||||||
),
|
|
||||||
currentCombat: null,
|
|
||||||
);
|
|
||||||
return (progress: updatedProgress, queue: queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Act Boss 재도전 태스크 생성
|
|
||||||
({ProgressState progress, QueueState queue}) _createActBossRetryTask(
|
|
||||||
GameState state,
|
|
||||||
ProgressState progress,
|
|
||||||
QueueState queue,
|
|
||||||
) {
|
|
||||||
final actProgressionService = ActProgressionService(config: config);
|
|
||||||
final actBoss = actProgressionService.createActBoss(state);
|
|
||||||
final combatCalculator = CombatCalculator(rng: state.rng);
|
|
||||||
final durationMillis = combatCalculator.estimateCombatDurationMs(
|
|
||||||
player: actBoss.playerStats,
|
|
||||||
monster: actBoss.monsterStats,
|
|
||||||
);
|
|
||||||
|
|
||||||
final taskResult = pq_logic.startTask(
|
|
||||||
progress,
|
|
||||||
l10n.taskDebugging(actBoss.monsterStats.name),
|
|
||||||
durationMillis,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
|
||||||
currentTask: TaskInfo(
|
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.kill,
|
|
||||||
monsterBaseName: actBoss.monsterStats.name,
|
|
||||||
monsterPart: '*',
|
|
||||||
monsterLevel: actBoss.monsterStats.level,
|
|
||||||
monsterGrade: MonsterGrade.boss,
|
|
||||||
monsterSize: getBossSizeForAct(state.progress.plotStageCount),
|
|
||||||
),
|
|
||||||
currentCombat: actBoss,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (progress: updatedProgress, queue: queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 일반 몬스터 전투 태스크 생성
|
|
||||||
({ProgressState progress, QueueState queue}) _createMonsterTask(
|
|
||||||
GameState state,
|
|
||||||
ProgressState progress,
|
|
||||||
QueueState queue,
|
|
||||||
) {
|
|
||||||
final level = state.traits.level;
|
|
||||||
|
|
||||||
// 퀘스트 몬스터 데이터 확인
|
|
||||||
final questMonster = state.progress.currentQuestMonster;
|
|
||||||
final questMonsterData = questMonster?.monsterData;
|
|
||||||
final questLevel = questMonsterData != null
|
|
||||||
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ??
|
|
||||||
0
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// 몬스터 생성
|
|
||||||
final monsterResult = pq_logic.monsterTask(
|
|
||||||
config,
|
|
||||||
state.rng,
|
|
||||||
level,
|
|
||||||
questMonsterData,
|
|
||||||
questLevel,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 몬스터 레벨 조정 (밸런스)
|
|
||||||
final actMinLevel = ActMonsterLevel.forPlotStage(
|
|
||||||
state.progress.plotStageCount,
|
|
||||||
);
|
|
||||||
final baseLevel = math.max(level, actMinLevel);
|
|
||||||
final effectiveMonsterLevel = monsterResult.level
|
|
||||||
.clamp(math.max(1, baseLevel - 3), baseLevel + 3)
|
|
||||||
.toInt();
|
|
||||||
|
|
||||||
// 전투 스탯 생성
|
|
||||||
final playerCombatStats = CombatStats.fromStats(
|
|
||||||
stats: state.stats,
|
|
||||||
equipment: state.equipment,
|
|
||||||
level: level,
|
|
||||||
monsterLevel: effectiveMonsterLevel,
|
|
||||||
);
|
|
||||||
|
|
||||||
final monsterCombatStats = MonsterCombatStats.fromLevel(
|
|
||||||
name: monsterResult.displayName,
|
|
||||||
level: effectiveMonsterLevel,
|
|
||||||
speedType: MonsterCombatStats.inferSpeedType(monsterResult.baseName),
|
|
||||||
plotStageCount: state.progress.plotStageCount,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 전투 상태 및 지속시간
|
|
||||||
final combatState = CombatState.start(
|
|
||||||
playerStats: playerCombatStats,
|
|
||||||
monsterStats: monsterCombatStats,
|
|
||||||
);
|
|
||||||
|
|
||||||
final combatCalculator = CombatCalculator(rng: state.rng);
|
|
||||||
final durationMillis = combatCalculator.estimateCombatDurationMs(
|
|
||||||
player: playerCombatStats,
|
|
||||||
monster: monsterCombatStats,
|
|
||||||
);
|
|
||||||
|
|
||||||
final taskResult = pq_logic.startTask(
|
|
||||||
progress,
|
|
||||||
l10n.taskDebugging(monsterResult.displayName),
|
|
||||||
durationMillis,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 몬스터 사이즈 결정
|
|
||||||
final monsterSize = getMonsterSizeForAct(
|
|
||||||
plotStageCount: state.progress.plotStageCount,
|
|
||||||
grade: monsterResult.grade,
|
|
||||||
rng: state.rng,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
|
||||||
currentTask: TaskInfo(
|
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.kill,
|
|
||||||
monsterBaseName: monsterResult.baseName,
|
|
||||||
monsterPart: monsterResult.part,
|
|
||||||
monsterLevel: effectiveMonsterLevel,
|
|
||||||
monsterGrade: monsterResult.grade,
|
|
||||||
monsterSize: monsterSize,
|
|
||||||
),
|
|
||||||
currentCombat: combatState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (progress: updatedProgress, queue: queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advances quest completion, applies reward, and enqueues next quest task.
|
/// Advances quest completion, applies reward, and enqueues next quest task.
|
||||||
GameState completeQuest(GameState state) {
|
GameState completeQuest(GameState state) {
|
||||||
final result = pq_logic.completeQuest(
|
final result = pq_logic.completeQuest(
|
||||||
@@ -1069,184 +829,4 @@ class ProgressService {
|
|||||||
final progress = state.progress.copyWith(encumbrance: encumBar);
|
final progress = state.progress.copyWith(encumbrance: encumBar);
|
||||||
return state.copyWith(progress: progress);
|
return state.copyWith(progress: progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
|
||||||
/// 전리품 획득 결과
|
|
||||||
///
|
|
||||||
/// [state] 업데이트된 게임 상태
|
|
||||||
/// [droppedPotion] 드랍된 물약 (없으면 null)
|
|
||||||
({GameState state, Potion? droppedPotion}) _winLoot(GameState state) {
|
|
||||||
final taskInfo = state.progress.currentTask;
|
|
||||||
final monsterPart = taskInfo.monsterPart ?? '';
|
|
||||||
final monsterBaseName = taskInfo.monsterBaseName ?? '';
|
|
||||||
|
|
||||||
var resultState = state;
|
|
||||||
|
|
||||||
// 부위가 '*'이면 WinItem 호출 (특수 아이템)
|
|
||||||
if (monsterPart == '*') {
|
|
||||||
resultState = mutations.winItem(resultState);
|
|
||||||
} else if (monsterPart.isNotEmpty && monsterBaseName.isNotEmpty) {
|
|
||||||
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
|
||||||
// ProperCase(Split(fTask.Caption,3))), 1);
|
|
||||||
// 예: "goblin Claw" 형태로 인벤토리 추가
|
|
||||||
final itemName =
|
|
||||||
'${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}';
|
|
||||||
|
|
||||||
// 인벤토리에 추가
|
|
||||||
final items = [...resultState.inventory.items];
|
|
||||||
final existing = items.indexWhere((e) => e.name == itemName);
|
|
||||||
if (existing >= 0) {
|
|
||||||
items[existing] = items[existing].copyWith(
|
|
||||||
count: items[existing].count + 1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
items.add(InventoryEntry(name: itemName, count: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
resultState = resultState.copyWith(
|
|
||||||
inventory: resultState.inventory.copyWith(items: items),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 물약 드랍 시도
|
|
||||||
final potionService = const PotionService();
|
|
||||||
final rng = resultState.rng;
|
|
||||||
final monsterLevel = taskInfo.monsterLevel ?? resultState.traits.level;
|
|
||||||
final monsterGrade = taskInfo.monsterGrade ?? MonsterGrade.normal;
|
|
||||||
final (updatedPotionInventory, droppedPotion) = potionService.tryPotionDrop(
|
|
||||||
playerLevel: resultState.traits.level,
|
|
||||||
monsterLevel: monsterLevel,
|
|
||||||
monsterGrade: monsterGrade,
|
|
||||||
inventory: resultState.potionInventory,
|
|
||||||
roll: rng.nextInt(100),
|
|
||||||
typeRoll: rng.nextInt(100),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
state: resultState.copyWith(
|
|
||||||
rng: rng,
|
|
||||||
potionInventory: updatedPotionInventory,
|
|
||||||
),
|
|
||||||
droppedPotion: droppedPotion,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 첫 글자만 대문자로 변환 (원본 ProperCase)
|
|
||||||
String _properCase(String s) {
|
|
||||||
if (s.isEmpty) return s;
|
|
||||||
return s[0].toUpperCase() + s.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 플레이어 사망 처리 (Phase 4)
|
|
||||||
///
|
|
||||||
/// 모든 장비 상실 및 사망 정보 기록
|
|
||||||
/// 보스전 사망 시: 장비 보호 + 5분 레벨링 모드 진입
|
|
||||||
GameState _processPlayerDeath(
|
|
||||||
GameState state, {
|
|
||||||
required String killerName,
|
|
||||||
required DeathCause cause,
|
|
||||||
}) {
|
|
||||||
// 사망 직전 전투 이벤트 저장 (최대 10개)
|
|
||||||
final lastCombatEvents =
|
|
||||||
state.progress.currentCombat?.recentEvents ?? const [];
|
|
||||||
|
|
||||||
// 보스전 사망 여부 확인 (최종 보스 fighting 상태)
|
|
||||||
final isBossDeath =
|
|
||||||
state.progress.finalBossState == FinalBossState.fighting;
|
|
||||||
|
|
||||||
// 보스전 사망이 아닐 경우에만 장비 손실
|
|
||||||
var newEquipment = state.equipment;
|
|
||||||
var lostCount = 0;
|
|
||||||
String? lostItemName;
|
|
||||||
EquipmentSlot? lostItemSlot;
|
|
||||||
ItemRarity? lostItemRarity;
|
|
||||||
EquipmentItem? lostEquipmentItem; // 광고 부활 시 복구용
|
|
||||||
|
|
||||||
if (!isBossDeath) {
|
|
||||||
// 레벨 기반 장비 손실 확률 계산
|
|
||||||
// Lv 1: 20%, Lv 5: ~56%, Lv 10+: 100%
|
|
||||||
// 공식: 20 + (level - 1) * 80 / 9
|
|
||||||
final level = state.traits.level;
|
|
||||||
final lossChancePercent = level >= 10
|
|
||||||
? 100
|
|
||||||
: (20 + ((level - 1) * 80 ~/ 9)).clamp(0, 100);
|
|
||||||
final roll = state.rng.nextInt(100); // 0~99
|
|
||||||
final shouldLoseEquipment = roll < lossChancePercent;
|
|
||||||
|
|
||||||
// ignore: avoid_print
|
|
||||||
print(
|
|
||||||
'[Death] Lv$level lossChance=$lossChancePercent% roll=$roll '
|
|
||||||
'shouldLose=$shouldLoseEquipment',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldLoseEquipment) {
|
|
||||||
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
|
|
||||||
final equippedNonWeaponSlots = <int>[];
|
|
||||||
for (var i = 1; i < Equipment.slotCount; i++) {
|
|
||||||
final item = state.equipment.getItemByIndex(i);
|
|
||||||
if (item.isNotEmpty) {
|
|
||||||
equippedNonWeaponSlots.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (equippedNonWeaponSlots.isNotEmpty) {
|
|
||||||
lostCount = 1;
|
|
||||||
// 랜덤하게 1개 슬롯 선택
|
|
||||||
final sacrificeIndex =
|
|
||||||
equippedNonWeaponSlots[state.rng.nextInt(
|
|
||||||
equippedNonWeaponSlots.length,
|
|
||||||
)];
|
|
||||||
|
|
||||||
// 제물로 바칠 아이템 정보 저장
|
|
||||||
lostEquipmentItem = state.equipment.getItemByIndex(sacrificeIndex);
|
|
||||||
lostItemName = lostEquipmentItem.name;
|
|
||||||
lostItemSlot = EquipmentSlot.values[sacrificeIndex];
|
|
||||||
lostItemRarity = lostEquipmentItem.rarity;
|
|
||||||
|
|
||||||
// 해당 슬롯을 빈 장비로 교체
|
|
||||||
newEquipment = newEquipment.setItemByIndex(
|
|
||||||
sacrificeIndex,
|
|
||||||
EquipmentItem.empty(lostItemSlot),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ignore: avoid_print
|
|
||||||
print('[Death] Lost item: $lostItemName (slot: $lostItemSlot)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사망 정보 생성 (전투 로그 포함)
|
|
||||||
final deathInfo = DeathInfo(
|
|
||||||
cause: cause,
|
|
||||||
killerName: killerName,
|
|
||||||
lostEquipmentCount: lostCount,
|
|
||||||
lostItemName: lostItemName,
|
|
||||||
lostItemSlot: lostItemSlot,
|
|
||||||
lostItemRarity: lostItemRarity,
|
|
||||||
lostItem: lostEquipmentItem, // 광고 부활 시 복구용
|
|
||||||
goldAtDeath: state.inventory.gold,
|
|
||||||
levelAtDeath: state.traits.level,
|
|
||||||
timestamp: state.skillSystem.elapsedMs,
|
|
||||||
lastCombatEvents: lastCombatEvents,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 보스전 사망 시 5분 레벨링 모드 진입
|
|
||||||
final bossLevelingEndTime = isBossDeath
|
|
||||||
? DateTime.now().millisecondsSinceEpoch +
|
|
||||||
(5 * 60 * 1000) // 5분
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// 전투 상태 초기화 및 사망 횟수 증가
|
|
||||||
final progress = state.progress.copyWith(
|
|
||||||
currentCombat: null,
|
|
||||||
deathCount: state.progress.deathCount + 1,
|
|
||||||
bossLevelingEndTime: bossLevelingEndTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
return state.copyWith(
|
|
||||||
equipment: newEquipment,
|
|
||||||
progress: progress,
|
|
||||||
deathInfo: deathInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
200
lib/src/core/engine/skill_auto_selector.dart
Normal file
200
lib/src/core/engine/skill_auto_selector.dart
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import 'package:asciineverdie/data/skill_data.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/skill.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 스킬 자동 선택 AI
|
||||||
|
///
|
||||||
|
/// SkillService에서 분리된 전투 중 스킬 자동 선택 로직.
|
||||||
|
/// 상황별 우선순위에 따라 최적의 스킬을 선택한다.
|
||||||
|
class SkillAutoSelector {
|
||||||
|
const SkillAutoSelector({required this.rng});
|
||||||
|
|
||||||
|
final DeterministicRandom rng;
|
||||||
|
|
||||||
|
/// 전투 중 자동 스킬 선택
|
||||||
|
///
|
||||||
|
/// 우선순위:
|
||||||
|
/// 1. HP < 30% -> 회복 스킬 (최우선)
|
||||||
|
/// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투)
|
||||||
|
/// 3. 30% 확률로 스킬 사용:
|
||||||
|
/// - 버프: HP > 80% & MP > 60% & 활성 버프 없음
|
||||||
|
/// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음
|
||||||
|
/// - DOT: 몬스터 HP > 60% & 활성 DOT 없음
|
||||||
|
/// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬
|
||||||
|
/// 4. MP < 20% -> 일반 공격
|
||||||
|
Skill? selectAutoSkill({
|
||||||
|
required CombatStats player,
|
||||||
|
required MonsterCombatStats monster,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required List<String> availableSkillIds,
|
||||||
|
required bool Function(Skill) canUse,
|
||||||
|
List<DotEffect> activeDoTs = const [],
|
||||||
|
List<ActiveBuff> activeDebuffs = const [],
|
||||||
|
}) {
|
||||||
|
final mpRatio = player.mpRatio;
|
||||||
|
final hpRatio = player.hpRatio;
|
||||||
|
|
||||||
|
// MP 20% 미만이면 일반 공격
|
||||||
|
if (mpRatio < 0.2) return null;
|
||||||
|
|
||||||
|
// 사용 가능한 스킬 필터링
|
||||||
|
final availableSkills = availableSkillIds
|
||||||
|
.map((id) => SkillData.getSkillById(id))
|
||||||
|
.whereType<Skill>()
|
||||||
|
.where(canUse)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (availableSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
// HP < 30% -> 회복 스킬 최우선 (생존)
|
||||||
|
if (hpRatio < 0.3) {
|
||||||
|
final healSkill = _findBestHealSkill(availableSkills, player.mpCurrent);
|
||||||
|
if (healSkill != null) return healSkill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 70% 확률로 일반 공격 (스킬은 특별한 상황에서만)
|
||||||
|
final useNormalAttack = rng.nextInt(100) < 70;
|
||||||
|
if (useNormalAttack) return null;
|
||||||
|
|
||||||
|
// === 아래부터 30% 확률로 스킬 사용 ===
|
||||||
|
|
||||||
|
// 버프: HP > 80% & MP > 60% (매우 안전할 때만)
|
||||||
|
if (hpRatio > 0.8 && mpRatio > 0.6) {
|
||||||
|
final hasActiveBuff = skillSystem.activeBuffs.isNotEmpty;
|
||||||
|
if (!hasActiveBuff) {
|
||||||
|
final buffSkill = _findBestBuffSkill(availableSkills, player.mpCurrent);
|
||||||
|
if (buffSkill != null) return buffSkill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반)
|
||||||
|
if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) {
|
||||||
|
final debuffSkill = _findBestDebuffSkill(
|
||||||
|
availableSkills,
|
||||||
|
player.mpCurrent,
|
||||||
|
);
|
||||||
|
if (debuffSkill != null) return debuffSkill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리)
|
||||||
|
if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) {
|
||||||
|
final dotSkill = _findBestDotSkill(availableSkills, player.mpCurrent);
|
||||||
|
if (dotSkill != null) return dotSkill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상)
|
||||||
|
final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5;
|
||||||
|
|
||||||
|
if (isBossFight) {
|
||||||
|
return _findStrongestAttackSkill(availableSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 전투 -> MP 효율 좋은 공격 스킬
|
||||||
|
return _findEfficientAttackSkill(availableSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 좋은 DOT 스킬 찾기
|
||||||
|
Skill? _findBestDotSkill(List<Skill> skills, int currentMp) {
|
||||||
|
final dotSkills = skills
|
||||||
|
.where((s) => s.isDot && s.mpCost <= currentMp)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (dotSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
dotSkills.sort((a, b) {
|
||||||
|
final aTotal =
|
||||||
|
(a.baseDotDamage ?? 0) *
|
||||||
|
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
||||||
|
final bTotal =
|
||||||
|
(b.baseDotDamage ?? 0) *
|
||||||
|
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
||||||
|
return bTotal.compareTo(aTotal);
|
||||||
|
});
|
||||||
|
|
||||||
|
return dotSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 좋은 회복 스킬 찾기
|
||||||
|
Skill? _findBestHealSkill(List<Skill> skills, int currentMp) {
|
||||||
|
final healSkills = skills
|
||||||
|
.where((s) => s.isHeal && s.mpCost <= currentMp)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (healSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
healSkills.sort((a, b) {
|
||||||
|
final aValue = a.healPercent * 100 + a.healAmount;
|
||||||
|
final bValue = b.healPercent * 100 + b.healAmount;
|
||||||
|
return bValue.compareTo(aValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return healSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 강력한 공격 스킬 찾기
|
||||||
|
Skill? _findStrongestAttackSkill(List<Skill> skills) {
|
||||||
|
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||||
|
if (attackSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
attackSkills.sort(
|
||||||
|
(a, b) => b.damageMultiplier.compareTo(a.damageMultiplier),
|
||||||
|
);
|
||||||
|
return attackSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MP 효율 좋은 공격 스킬 찾기
|
||||||
|
Skill? _findEfficientAttackSkill(List<Skill> skills) {
|
||||||
|
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||||
|
if (attackSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
attackSkills.sort((a, b) => b.mpEfficiency.compareTo(a.mpEfficiency));
|
||||||
|
return attackSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 좋은 버프 스킬 찾기
|
||||||
|
Skill? _findBestBuffSkill(List<Skill> skills, int currentMp) {
|
||||||
|
final buffSkills = skills
|
||||||
|
.where((s) => s.isBuff && s.mpCost <= currentMp && s.buff != null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (buffSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
buffSkills.sort((a, b) {
|
||||||
|
final aValue =
|
||||||
|
(a.buff?.atkModifier ?? 0) +
|
||||||
|
(a.buff?.defModifier ?? 0) * 0.5 +
|
||||||
|
(a.buff?.criRateModifier ?? 0) * 0.3;
|
||||||
|
final bValue =
|
||||||
|
(b.buff?.atkModifier ?? 0) +
|
||||||
|
(b.buff?.defModifier ?? 0) * 0.5 +
|
||||||
|
(b.buff?.criRateModifier ?? 0) * 0.3;
|
||||||
|
return bValue.compareTo(aValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return buffSkills.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 가장 좋은 디버프 스킬 찾기
|
||||||
|
Skill? _findBestDebuffSkill(List<Skill> skills, int currentMp) {
|
||||||
|
final debuffSkills = skills
|
||||||
|
.where((s) => s.isDebuff && s.mpCost <= currentMp && s.buff != null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (debuffSkills.isEmpty) return null;
|
||||||
|
|
||||||
|
debuffSkills.sort((a, b) {
|
||||||
|
final aValue =
|
||||||
|
(a.buff?.atkModifier ?? 0).abs() +
|
||||||
|
(a.buff?.defModifier ?? 0).abs() * 0.5;
|
||||||
|
final bValue =
|
||||||
|
(b.buff?.atkModifier ?? 0).abs() +
|
||||||
|
(b.buff?.defModifier ?? 0).abs() * 0.5;
|
||||||
|
return bValue.compareTo(aValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return debuffSkills.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:asciineverdie/data/skill_data.dart';
|
import 'package:asciineverdie/data/skill_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/skill_auto_selector.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_stats.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/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
@@ -309,20 +310,12 @@ class SkillService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 자동 스킬 선택
|
// 자동 스킬 선택 (SkillAutoSelector에 위임)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 전투 중 자동 스킬 선택
|
/// 전투 중 자동 스킬 선택
|
||||||
///
|
///
|
||||||
/// 우선순위:
|
/// 세부 로직은 SkillAutoSelector에 위임.
|
||||||
/// 1. HP < 30% → 회복 스킬 (최우선)
|
|
||||||
/// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투)
|
|
||||||
/// 3. 30% 확률로 스킬 사용:
|
|
||||||
/// - 버프: HP > 80% & MP > 60% & 활성 버프 없음
|
|
||||||
/// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음
|
|
||||||
/// - DOT: 몬스터 HP > 60% & 활성 DOT 없음
|
|
||||||
/// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬
|
|
||||||
/// 4. MP < 20% → 일반 공격
|
|
||||||
Skill? selectAutoSkill({
|
Skill? selectAutoSkill({
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required MonsterCombatStats monster,
|
required MonsterCombatStats monster,
|
||||||
@@ -331,186 +324,22 @@ class SkillService {
|
|||||||
List<DotEffect> activeDoTs = const [],
|
List<DotEffect> activeDoTs = const [],
|
||||||
List<ActiveBuff> activeDebuffs = const [],
|
List<ActiveBuff> activeDebuffs = const [],
|
||||||
}) {
|
}) {
|
||||||
final currentMp = player.mpCurrent;
|
final selector = SkillAutoSelector(rng: rng);
|
||||||
final mpRatio = player.mpRatio;
|
return selector.selectAutoSkill(
|
||||||
final hpRatio = player.hpRatio;
|
player: player,
|
||||||
|
monster: monster,
|
||||||
// MP 20% 미만이면 일반 공격
|
skillSystem: skillSystem,
|
||||||
if (mpRatio < 0.2) return null;
|
availableSkillIds: availableSkillIds,
|
||||||
|
canUse: (skill) =>
|
||||||
// 사용 가능한 스킬 필터링
|
canUseSkill(
|
||||||
final availableSkills = availableSkillIds
|
skill: skill,
|
||||||
.map((id) => SkillData.getSkillById(id))
|
currentMp: player.mpCurrent,
|
||||||
.whereType<Skill>()
|
skillSystem: skillSystem,
|
||||||
.where(
|
) ==
|
||||||
(skill) =>
|
null,
|
||||||
canUseSkill(
|
activeDoTs: activeDoTs,
|
||||||
skill: skill,
|
activeDebuffs: activeDebuffs,
|
||||||
currentMp: currentMp,
|
|
||||||
skillSystem: skillSystem,
|
|
||||||
) ==
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (availableSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
// HP < 30% → 회복 스킬 최우선 (생존)
|
|
||||||
if (hpRatio < 0.3) {
|
|
||||||
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
|
||||||
if (healSkill != null) return healSkill;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 70% 확률로 일반 공격 (스킬은 특별한 상황에서만)
|
|
||||||
final useNormalAttack = rng.nextInt(100) < 70;
|
|
||||||
if (useNormalAttack) return null;
|
|
||||||
|
|
||||||
// === 아래부터 30% 확률로 스킬 사용 ===
|
|
||||||
|
|
||||||
// 버프: HP > 80% & MP > 60% (매우 안전할 때만)
|
|
||||||
// 활성 버프가 있으면 건너뜀 (중복 방지)
|
|
||||||
if (hpRatio > 0.8 && mpRatio > 0.6) {
|
|
||||||
final hasActiveBuff = skillSystem.activeBuffs.isNotEmpty;
|
|
||||||
if (!hasActiveBuff) {
|
|
||||||
final buffSkill = _findBestBuffSkill(availableSkills, currentMp);
|
|
||||||
if (buffSkill != null) return buffSkill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반)
|
|
||||||
if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) {
|
|
||||||
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
|
|
||||||
if (debuffSkill != null) return debuffSkill;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리)
|
|
||||||
if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) {
|
|
||||||
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
|
|
||||||
if (dotSkill != null) return dotSkill;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상)
|
|
||||||
final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5;
|
|
||||||
|
|
||||||
if (isBossFight) {
|
|
||||||
// 가장 강력한 공격 스킬
|
|
||||||
return _findStrongestAttackSkill(availableSkills);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 일반 전투 → MP 효율 좋은 공격 스킬
|
|
||||||
return _findEfficientAttackSkill(availableSkills);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 가장 좋은 DOT 스킬 찾기
|
|
||||||
///
|
|
||||||
/// 예상 총 데미지 (틱 × 데미지) 기준으로 선택
|
|
||||||
Skill? _findBestDotSkill(List<Skill> skills, int currentMp) {
|
|
||||||
final dotSkills = skills
|
|
||||||
.where((s) => s.isDot && s.mpCost <= currentMp)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (dotSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
// 예상 총 데미지 기준 정렬
|
|
||||||
dotSkills.sort((a, b) {
|
|
||||||
final aTotal =
|
|
||||||
(a.baseDotDamage ?? 0) *
|
|
||||||
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
|
||||||
final bTotal =
|
|
||||||
(b.baseDotDamage ?? 0) *
|
|
||||||
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
|
||||||
return bTotal.compareTo(aTotal);
|
|
||||||
});
|
|
||||||
|
|
||||||
return dotSkills.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 가장 좋은 회복 스킬 찾기
|
|
||||||
Skill? _findBestHealSkill(List<Skill> skills, int currentMp) {
|
|
||||||
final healSkills = skills
|
|
||||||
.where((s) => s.isHeal && s.mpCost <= currentMp)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (healSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
// 회복량 기준 정렬 (% 회복 > 고정 회복)
|
|
||||||
healSkills.sort((a, b) {
|
|
||||||
final aValue = a.healPercent * 100 + a.healAmount;
|
|
||||||
final bValue = b.healPercent * 100 + b.healAmount;
|
|
||||||
return bValue.compareTo(aValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
return healSkills.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 가장 강력한 공격 스킬 찾기
|
|
||||||
Skill? _findStrongestAttackSkill(List<Skill> skills) {
|
|
||||||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
|
||||||
if (attackSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
attackSkills.sort(
|
|
||||||
(a, b) => b.damageMultiplier.compareTo(a.damageMultiplier),
|
|
||||||
);
|
);
|
||||||
return attackSkills.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MP 효율 좋은 공격 스킬 찾기
|
|
||||||
Skill? _findEfficientAttackSkill(List<Skill> skills) {
|
|
||||||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
|
||||||
if (attackSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
attackSkills.sort((a, b) => b.mpEfficiency.compareTo(a.mpEfficiency));
|
|
||||||
return attackSkills.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 가장 좋은 버프 스킬 찾기
|
|
||||||
///
|
|
||||||
/// ATK 증가 버프 우선, 그 다음 복합 버프
|
|
||||||
Skill? _findBestBuffSkill(List<Skill> skills, int currentMp) {
|
|
||||||
final buffSkills = skills
|
|
||||||
.where((s) => s.isBuff && s.mpCost <= currentMp && s.buff != null)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (buffSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
// ATK 증가량 기준 정렬
|
|
||||||
buffSkills.sort((a, b) {
|
|
||||||
final aValue =
|
|
||||||
(a.buff?.atkModifier ?? 0) +
|
|
||||||
(a.buff?.defModifier ?? 0) * 0.5 +
|
|
||||||
(a.buff?.criRateModifier ?? 0) * 0.3;
|
|
||||||
final bValue =
|
|
||||||
(b.buff?.atkModifier ?? 0) +
|
|
||||||
(b.buff?.defModifier ?? 0) * 0.5 +
|
|
||||||
(b.buff?.criRateModifier ?? 0) * 0.3;
|
|
||||||
return bValue.compareTo(aValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
return buffSkills.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 가장 좋은 디버프 스킬 찾기
|
|
||||||
///
|
|
||||||
/// 적 ATK 감소 디버프 우선
|
|
||||||
Skill? _findBestDebuffSkill(List<Skill> skills, int currentMp) {
|
|
||||||
final debuffSkills = skills
|
|
||||||
.where((s) => s.isDebuff && s.mpCost <= currentMp && s.buff != null)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (debuffSkills.isEmpty) return null;
|
|
||||||
|
|
||||||
// 디버프 효과 크기 기준 정렬 (음수 값이므로 절대값으로 비교)
|
|
||||||
debuffSkills.sort((a, b) {
|
|
||||||
final aValue =
|
|
||||||
(a.buff?.atkModifier ?? 0).abs() +
|
|
||||||
(a.buff?.defModifier ?? 0).abs() * 0.5;
|
|
||||||
final bValue =
|
|
||||||
(b.buff?.atkModifier ?? 0).abs() +
|
|
||||||
(b.buff?.defModifier ?? 0).abs() * 0.5;
|
|
||||||
return bValue.compareTo(aValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
return debuffSkills.first;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
259
lib/src/core/engine/task_generator.dart
Normal file
259
lib/src/core/engine/task_generator.dart
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/shared/animation/monster_size.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/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/monster_grade.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
|
/// 태스크 생성 서비스
|
||||||
|
///
|
||||||
|
/// ProgressService에서 분리된 다음 태스크 생성 로직 담당:
|
||||||
|
/// - 시장 이동, 전환 태스크, 보스 리트라이, 몬스터 전투 생성
|
||||||
|
class TaskGenerator {
|
||||||
|
const TaskGenerator({required this.config});
|
||||||
|
|
||||||
|
final PqConfig config;
|
||||||
|
|
||||||
|
/// 큐가 비어있을 때 다음 태스크 생성 (원본 Dequeue 667-684줄)
|
||||||
|
({ProgressState progress, QueueState queue}) generateNextTask(
|
||||||
|
GameState state,
|
||||||
|
) {
|
||||||
|
var progress = state.progress;
|
||||||
|
final queue = state.queue;
|
||||||
|
final oldTaskType = progress.currentTask.type;
|
||||||
|
|
||||||
|
// 1. Encumbrance 초과 시 시장 이동
|
||||||
|
if (_shouldGoToMarket(progress)) {
|
||||||
|
return _createMarketTask(progress, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 전환 태스크 (buying/heading)
|
||||||
|
if (_needsTransitionTask(oldTaskType)) {
|
||||||
|
return _createTransitionTask(state, progress, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Act Boss 리트라이
|
||||||
|
if (state.progress.pendingActCompletion) {
|
||||||
|
return _createActBossRetryTask(state, progress, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 최종 보스 전투
|
||||||
|
if (state.progress.finalBossState == FinalBossState.fighting &&
|
||||||
|
!state.progress.isInBossLevelingMode) {
|
||||||
|
if (state.progress.bossLevelingEndTime != null) {
|
||||||
|
progress = progress.copyWith(clearBossLevelingEndTime: true);
|
||||||
|
}
|
||||||
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
return actProgressionService.startFinalBossFight(state, progress, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 일반 몬스터 전투
|
||||||
|
return _createMonsterTask(state, progress, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 시장 이동 조건 확인
|
||||||
|
bool _shouldGoToMarket(ProgressState progress) {
|
||||||
|
return progress.encumbrance.position >= progress.encumbrance.max &&
|
||||||
|
progress.encumbrance.max > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전환 태스크 필요 여부 확인
|
||||||
|
bool _needsTransitionTask(TaskType oldTaskType) {
|
||||||
|
return oldTaskType != TaskType.kill &&
|
||||||
|
oldTaskType != TaskType.neutral &&
|
||||||
|
oldTaskType != TaskType.buying;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 시장 이동 태스크 생성
|
||||||
|
({ProgressState progress, QueueState queue}) _createMarketTask(
|
||||||
|
ProgressState progress,
|
||||||
|
QueueState queue,
|
||||||
|
) {
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
l10n.taskHeadingToMarket(),
|
||||||
|
4 * 1000,
|
||||||
|
);
|
||||||
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.market),
|
||||||
|
currentCombat: null,
|
||||||
|
);
|
||||||
|
return (progress: updatedProgress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전환 태스크 생성 (buying 또는 heading)
|
||||||
|
({ProgressState progress, QueueState queue}) _createTransitionTask(
|
||||||
|
GameState state,
|
||||||
|
ProgressState progress,
|
||||||
|
QueueState queue,
|
||||||
|
) {
|
||||||
|
final gold = state.inventory.gold;
|
||||||
|
final equipPrice = state.traits.level * 50;
|
||||||
|
|
||||||
|
// Gold 충분 시 장비 구매
|
||||||
|
if (gold > equipPrice) {
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
l10n.taskUpgradingHardware(),
|
||||||
|
5 * 1000,
|
||||||
|
);
|
||||||
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.buying,
|
||||||
|
),
|
||||||
|
currentCombat: null,
|
||||||
|
);
|
||||||
|
return (progress: updatedProgress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gold 부족 시 전장 이동
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
l10n.taskEnteringDebugZone(),
|
||||||
|
4 * 1000,
|
||||||
|
);
|
||||||
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.neutral,
|
||||||
|
),
|
||||||
|
currentCombat: null,
|
||||||
|
);
|
||||||
|
return (progress: updatedProgress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Act Boss 재도전 태스크 생성
|
||||||
|
({ProgressState progress, QueueState queue}) _createActBossRetryTask(
|
||||||
|
GameState state,
|
||||||
|
ProgressState progress,
|
||||||
|
QueueState queue,
|
||||||
|
) {
|
||||||
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
final actBoss = actProgressionService.createActBoss(state);
|
||||||
|
final combatCalculator = CombatCalculator(rng: state.rng);
|
||||||
|
final durationMillis = combatCalculator.estimateCombatDurationMs(
|
||||||
|
player: actBoss.playerStats,
|
||||||
|
monster: actBoss.monsterStats,
|
||||||
|
);
|
||||||
|
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
l10n.taskDebugging(actBoss.monsterStats.name),
|
||||||
|
durationMillis,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.kill,
|
||||||
|
monsterBaseName: actBoss.monsterStats.name,
|
||||||
|
monsterPart: '*',
|
||||||
|
monsterLevel: actBoss.monsterStats.level,
|
||||||
|
monsterGrade: MonsterGrade.boss,
|
||||||
|
monsterSize: getBossSizeForAct(state.progress.plotStageCount),
|
||||||
|
),
|
||||||
|
currentCombat: actBoss,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (progress: updatedProgress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일반 몬스터 전투 태스크 생성
|
||||||
|
({ProgressState progress, QueueState queue}) _createMonsterTask(
|
||||||
|
GameState state,
|
||||||
|
ProgressState progress,
|
||||||
|
QueueState queue,
|
||||||
|
) {
|
||||||
|
final level = state.traits.level;
|
||||||
|
|
||||||
|
// 퀘스트 몬스터 데이터 확인
|
||||||
|
final questMonster = state.progress.currentQuestMonster;
|
||||||
|
final questMonsterData = questMonster?.monsterData;
|
||||||
|
final questLevel = questMonsterData != null
|
||||||
|
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ??
|
||||||
|
0
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 몬스터 생성
|
||||||
|
final monsterResult = pq_logic.monsterTask(
|
||||||
|
config,
|
||||||
|
state.rng,
|
||||||
|
level,
|
||||||
|
questMonsterData,
|
||||||
|
questLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 몬스터 레벨 조정 (밸런스)
|
||||||
|
final actMinLevel = ActMonsterLevel.forPlotStage(
|
||||||
|
state.progress.plotStageCount,
|
||||||
|
);
|
||||||
|
final baseLevel = math.max(level, actMinLevel);
|
||||||
|
final effectiveMonsterLevel = monsterResult.level
|
||||||
|
.clamp(math.max(1, baseLevel - 3), baseLevel + 3)
|
||||||
|
.toInt();
|
||||||
|
|
||||||
|
// 전투 스탯 생성
|
||||||
|
final playerCombatStats = CombatStats.fromStats(
|
||||||
|
stats: state.stats,
|
||||||
|
equipment: state.equipment,
|
||||||
|
level: level,
|
||||||
|
monsterLevel: effectiveMonsterLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
final monsterCombatStats = MonsterCombatStats.fromLevel(
|
||||||
|
name: monsterResult.displayName,
|
||||||
|
level: effectiveMonsterLevel,
|
||||||
|
speedType: MonsterCombatStats.inferSpeedType(monsterResult.baseName),
|
||||||
|
plotStageCount: state.progress.plotStageCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 상태 및 지속시간
|
||||||
|
final combatState = CombatState.start(
|
||||||
|
playerStats: playerCombatStats,
|
||||||
|
monsterStats: monsterCombatStats,
|
||||||
|
);
|
||||||
|
|
||||||
|
final combatCalculator = CombatCalculator(rng: state.rng);
|
||||||
|
final durationMillis = combatCalculator.estimateCombatDurationMs(
|
||||||
|
player: playerCombatStats,
|
||||||
|
monster: monsterCombatStats,
|
||||||
|
);
|
||||||
|
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
l10n.taskDebugging(monsterResult.displayName),
|
||||||
|
durationMillis,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 몬스터 사이즈 결정
|
||||||
|
final monsterSize = getMonsterSizeForAct(
|
||||||
|
plotStageCount: state.progress.plotStageCount,
|
||||||
|
grade: monsterResult.grade,
|
||||||
|
rng: state.rng,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.kill,
|
||||||
|
monsterBaseName: monsterResult.baseName,
|
||||||
|
monsterPart: monsterResult.part,
|
||||||
|
monsterLevel: effectiveMonsterLevel,
|
||||||
|
monsterGrade: monsterResult.grade,
|
||||||
|
monsterSize: monsterSize,
|
||||||
|
),
|
||||||
|
currentCombat: combatState,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (progress: updatedProgress, queue: queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user