feat(core): 엔진, 모델, 애니메이션 개선
- ProgressService 로직 개선 - CombatCalculator 업데이트 - GameState, MonsterCombatStats 확장 - CanvasBattleComposer 개선
This commit is contained in:
@@ -68,15 +68,17 @@ class CanvasBattleComposer {
|
||||
bool isDot = false,
|
||||
bool isBlock = false,
|
||||
bool isParry = false,
|
||||
bool hideMonster = false,
|
||||
}) {
|
||||
final layers = <AsciiLayer>[
|
||||
_createBackgroundLayer(environment, globalTick),
|
||||
_createCharacterLayer(phase, subFrame, attacker),
|
||||
// PvP 모드: 몬스터 대신 상대 캐릭터(좌우 반전) 표시
|
||||
if (isPvP)
|
||||
_createOpponentCharacterLayer(phase, subFrame, attacker)
|
||||
else
|
||||
_createMonsterLayer(phase, subFrame, attacker),
|
||||
// hideMonster: 몬스터 사망 애니메이션 중에는 렌더링 안함
|
||||
if (!hideMonster)
|
||||
isPvP
|
||||
? _createOpponentCharacterLayer(phase, subFrame, attacker)
|
||||
: _createMonsterLayer(phase, subFrame, attacker),
|
||||
];
|
||||
|
||||
// 이펙트 레이어 (준비/공격/히트 페이즈에서, 공격자 있을 때)
|
||||
@@ -1460,3 +1462,13 @@ const _parryTextFrames = <List<String>>[
|
||||
[r'*PARRY!*', r'========'],
|
||||
[r'=PARRY!=', r'********'],
|
||||
];
|
||||
|
||||
/// 몬스터 Idle 프레임 가져오기 (외부에서 접근 가능)
|
||||
///
|
||||
/// 몬스터 사망 애니메이션에서 분해할 프레임을 가져올 때 사용
|
||||
List<List<String>> getMonsterIdleFrames(
|
||||
MonsterCategory category,
|
||||
MonsterSize size,
|
||||
) {
|
||||
return _getMonsterIdleFrames(category, size);
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ class CombatCalculator {
|
||||
final isParried = parryRoll < defenderParryRate;
|
||||
|
||||
// 3. 기본 데미지 계산 (0.8 ~ 1.2 변동)
|
||||
// DEF 감산 비율: 0.5 (방어력 효과 상향, 몬스터 ATK 하향과 연동)
|
||||
// DEF 감산 비율: 0.4 (전체 데미지 상승 조정)
|
||||
final damageVariation = 0.8 + rng.nextDouble() * 0.4;
|
||||
var baseDamage = (attackerAtk * damageVariation - defenderDef * 0.5);
|
||||
var baseDamage = (attackerAtk * damageVariation - defenderDef * 0.4);
|
||||
|
||||
// 4. 크리티컬 판정
|
||||
final criRoll = rng.nextDouble();
|
||||
@@ -207,7 +207,7 @@ class CombatCalculator {
|
||||
required MonsterCombatStats monster,
|
||||
}) {
|
||||
// 플레이어 DPS (초당 데미지)
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.4);
|
||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||
final playerDps =
|
||||
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
@@ -227,14 +227,14 @@ class CombatCalculator {
|
||||
required MonsterCombatStats monster,
|
||||
}) {
|
||||
// 플레이어 예상 생존 시간
|
||||
final monsterDamagePerHit = math.max(1, monster.atk - player.def * 0.5);
|
||||
final monsterDamagePerHit = math.max(1, monster.atk - player.def * 0.4);
|
||||
final monsterHitsPerSecond = 1000 / monster.attackDelayMs;
|
||||
final monsterDps =
|
||||
monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
||||
final playerSurvivalTime = player.hpCurrent / monsterDps;
|
||||
|
||||
// 몬스터 예상 생존 시간
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.4);
|
||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||
final playerDps =
|
||||
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
|
||||
@@ -588,9 +588,19 @@ class ProgressService {
|
||||
|
||||
// 4. 최종 보스 전투 체크
|
||||
// finalBossState == fighting이면 Glitch God 스폰
|
||||
// 단, 레벨링 모드 중이면 일반 몬스터로 레벨업 후 재도전
|
||||
if (state.progress.finalBossState == FinalBossState.fighting) {
|
||||
if (state.progress.isInBossLevelingMode) {
|
||||
// 레벨링 모드: 일반 몬스터 전투로 대체 (아래 MonsterTask로 진행)
|
||||
} else {
|
||||
// 레벨링 모드 종료 또는 첫 도전: 보스전 시작
|
||||
// 레벨링 모드가 끝났으면 타이머 초기화
|
||||
if (state.progress.bossLevelingEndTime != null) {
|
||||
progress = progress.copyWith(clearBossLevelingEndTime: true);
|
||||
}
|
||||
return _startFinalBossFight(state, progress, queue);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. MonsterTask 실행 (원본 678-684줄)
|
||||
final level = state.traits.level;
|
||||
@@ -634,6 +644,7 @@ class ProgressService {
|
||||
name: monsterResult.displayName,
|
||||
level: effectiveMonsterLevel,
|
||||
speedType: MonsterCombatStats.inferSpeedType(monsterResult.baseName),
|
||||
plotStageCount: state.progress.plotStageCount,
|
||||
);
|
||||
|
||||
// 전투 상태 초기화
|
||||
@@ -1667,6 +1678,7 @@ class ProgressService {
|
||||
/// 플레이어 사망 처리 (Phase 4)
|
||||
///
|
||||
/// 모든 장비 상실 및 사망 정보 기록
|
||||
/// 보스전 사망 시: 장비 보호 + 5분 레벨링 모드 진입
|
||||
GameState _processPlayerDeath(
|
||||
GameState state, {
|
||||
required String killerName,
|
||||
@@ -1676,8 +1688,16 @@ class ProgressService {
|
||||
final lastCombatEvents =
|
||||
state.progress.currentCombat?.recentEvents ?? const [];
|
||||
|
||||
// 보스전 사망 여부 확인 (최종 보스 fighting 상태)
|
||||
final isBossDeath =
|
||||
state.progress.finalBossState == FinalBossState.fighting;
|
||||
|
||||
// 보스전 사망이 아닐 경우에만 장비 손실
|
||||
var newEquipment = state.equipment;
|
||||
var lostCount = 0;
|
||||
|
||||
if (!isBossDeath) {
|
||||
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
|
||||
// 장착된 비무기 슬롯 인덱스 수집 (슬롯 1~10 중 장비가 있는 것)
|
||||
final equippedNonWeaponSlots = <int>[];
|
||||
for (var i = 1; i < Equipment.slotCount; i++) {
|
||||
if (state.equipment.getItemByIndex(i).isNotEmpty) {
|
||||
@@ -1685,14 +1705,11 @@ class ProgressService {
|
||||
}
|
||||
}
|
||||
|
||||
// 제물로 바칠 장비 선택 및 삭제
|
||||
var newEquipment = state.equipment;
|
||||
final lostCount = equippedNonWeaponSlots.isNotEmpty ? 1 : 0;
|
||||
|
||||
if (equippedNonWeaponSlots.isNotEmpty) {
|
||||
lostCount = 1;
|
||||
// 랜덤하게 1개 슬롯 선택
|
||||
final sacrificeIndex =
|
||||
equippedNonWeaponSlots[state.rng.nextInt(equippedNonWeaponSlots.length)];
|
||||
final sacrificeIndex = equippedNonWeaponSlots[
|
||||
state.rng.nextInt(equippedNonWeaponSlots.length)];
|
||||
final slot = EquipmentSlot.values[sacrificeIndex];
|
||||
|
||||
// 해당 슬롯을 빈 장비로 교체
|
||||
@@ -1701,6 +1718,7 @@ class ProgressService {
|
||||
EquipmentItem.empty(slot),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 사망 정보 생성 (전투 로그 포함)
|
||||
final deathInfo = DeathInfo(
|
||||
@@ -1713,12 +1731,16 @@ class ProgressService {
|
||||
lastCombatEvents: lastCombatEvents,
|
||||
);
|
||||
|
||||
// 보스전 사망 시 5분 레벨링 모드 진입
|
||||
final bossLevelingEndTime = isBossDeath
|
||||
? DateTime.now().millisecondsSinceEpoch + (5 * 60 * 1000) // 5분
|
||||
: null;
|
||||
|
||||
// 전투 상태 초기화 및 사망 횟수 증가
|
||||
// pendingActCompletion 플래그는 유지 (Boss 리트라이를 위해)
|
||||
final progress = state.progress.copyWith(
|
||||
currentCombat: null,
|
||||
deathCount: state.progress.deathCount + 1,
|
||||
// pendingActCompletion은 copyWith에서 명시하지 않으면 기존 값 유지
|
||||
bossLevelingEndTime: bossLevelingEndTime,
|
||||
);
|
||||
|
||||
return state.copyWith(
|
||||
|
||||
@@ -475,7 +475,8 @@ class Inventory {
|
||||
final int gold;
|
||||
final List<InventoryEntry> items;
|
||||
|
||||
factory Inventory.empty() => const Inventory(gold: 0, items: []);
|
||||
/// 초기 골드 1000 지급 (캐릭터 생성 시)
|
||||
factory Inventory.empty() => const Inventory(gold: 1000, items: []);
|
||||
|
||||
Inventory copyWith({int? gold, List<InventoryEntry>? items}) {
|
||||
return Inventory(gold: gold ?? this.gold, items: items ?? this.items);
|
||||
@@ -800,6 +801,7 @@ class ProgressState {
|
||||
this.deathCount = 0,
|
||||
this.finalBossState = FinalBossState.notSpawned,
|
||||
this.pendingActCompletion = false,
|
||||
this.bossLevelingEndTime,
|
||||
});
|
||||
|
||||
final ProgressBarState task;
|
||||
@@ -835,6 +837,10 @@ class ProgressState {
|
||||
/// Act Boss 처치 대기 중 여부 (처치 후 시네마틱 재생 트리거)
|
||||
final bool pendingActCompletion;
|
||||
|
||||
/// 보스 사망 후 레벨링 모드 종료 시간 (milliseconds since epoch)
|
||||
/// null이면 레벨링 모드 아님, 값이 있으면 해당 시간까지 레벨링
|
||||
final int? bossLevelingEndTime;
|
||||
|
||||
factory ProgressState.empty() => ProgressState(
|
||||
task: ProgressBarState.empty(),
|
||||
quest: ProgressBarState.empty(),
|
||||
@@ -867,6 +873,8 @@ class ProgressState {
|
||||
int? deathCount,
|
||||
FinalBossState? finalBossState,
|
||||
bool? pendingActCompletion,
|
||||
int? bossLevelingEndTime,
|
||||
bool clearBossLevelingEndTime = false,
|
||||
}) {
|
||||
return ProgressState(
|
||||
task: task ?? this.task,
|
||||
@@ -885,8 +893,17 @@ class ProgressState {
|
||||
deathCount: deathCount ?? this.deathCount,
|
||||
finalBossState: finalBossState ?? this.finalBossState,
|
||||
pendingActCompletion: pendingActCompletion ?? this.pendingActCompletion,
|
||||
bossLevelingEndTime: clearBossLevelingEndTime
|
||||
? null
|
||||
: (bossLevelingEndTime ?? this.bossLevelingEndTime),
|
||||
);
|
||||
}
|
||||
|
||||
/// 현재 레벨링 모드인지 확인
|
||||
bool get isInBossLevelingMode {
|
||||
if (bossLevelingEndTime == null) return false;
|
||||
return DateTime.now().millisecondsSinceEpoch < bossLevelingEndTime!;
|
||||
}
|
||||
}
|
||||
|
||||
class QueueEntry {
|
||||
|
||||
@@ -131,15 +131,29 @@ class MonsterCombatStats {
|
||||
/// [level] 몬스터 레벨 (원본 데이터 기준)
|
||||
/// [speedType] 공격 속도 타입 (기본: normal)
|
||||
/// [monsterType] 몬스터 타입 (기본: normal)
|
||||
/// [plotStageCount] 현재 Act (1=Prologue, 2=Act I, 3=Act II, ...)
|
||||
factory MonsterCombatStats.fromLevel({
|
||||
required String name,
|
||||
required int level,
|
||||
MonsterSpeedType speedType = MonsterSpeedType.normal,
|
||||
MonsterType monsterType = MonsterType.normal,
|
||||
int plotStageCount = 1,
|
||||
}) {
|
||||
// balance_constants.dart의 MonsterBaseStats 사용
|
||||
final baseStats = MonsterBaseStats.generate(level, monsterType);
|
||||
|
||||
// Act II 이후 (plotStageCount >= 3) HP 10% 상승
|
||||
final hpMultiplier = plotStageCount >= 3 ? 1.1 : 1.0;
|
||||
final adjustedHp = (baseStats.hp * hpMultiplier).round();
|
||||
|
||||
// Act별 경험치 배율 (후반부 레벨업 가속)
|
||||
final expMultiplier = switch (plotStageCount) {
|
||||
5 => 1.3, // Act IV: 30% 보너스
|
||||
6 => 1.8, // Act V: 80% 보너스 (보스전 대비)
|
||||
_ => 1.0, // 기본
|
||||
};
|
||||
final adjustedExp = (baseStats.exp * expMultiplier).round();
|
||||
|
||||
// 크리티컬 확률: 레벨에 따라 천천히 증가 (0.02 ~ 0.3)
|
||||
final criRate = (0.02 + level * 0.003).clamp(0.02, 0.3);
|
||||
|
||||
@@ -164,14 +178,14 @@ class MonsterCombatStats {
|
||||
level: level,
|
||||
atk: baseStats.atk,
|
||||
def: baseStats.def,
|
||||
hpMax: baseStats.hp,
|
||||
hpCurrent: baseStats.hp,
|
||||
hpMax: adjustedHp,
|
||||
hpCurrent: adjustedHp,
|
||||
criRate: criRate,
|
||||
criDamage: criDamage,
|
||||
evasion: evasion,
|
||||
accuracy: accuracy,
|
||||
attackDelayMs: attackDelayMs,
|
||||
expReward: baseStats.exp,
|
||||
expReward: adjustedExp,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user