feat(animation): 공격 속도 기반 동적 애니메이션 페이즈

- CombatEvent에 attackDelayMs 필드 추가
- ProgressService에서 전투 이벤트에 공격 속도 전달
- AsciiAnimationCard에서 공격 속도 기반 페이즈 프레임 수 계산
- 200ms tick 기준으로 동적 프레임 수 (최소 2, 최대 10)
This commit is contained in:
JiWoong Sul
2025-12-26 18:10:43 +09:00
parent c55530d3be
commit 6e56420a07
3 changed files with 37 additions and 1 deletions

View File

@@ -1118,6 +1118,7 @@ class ProgressService {
skillName: selectedSkill.name, skillName: selectedSkill.name,
damage: skillResult.result.damage, damage: skillResult.result.damage,
targetName: monsterStats.name, targetName: monsterStats.name,
attackDelayMs: playerStats.attackDelayMs,
), ),
); );
} else if (selectedSkill != null && selectedSkill.isDot) { } else if (selectedSkill != null && selectedSkill.isDot) {
@@ -1144,6 +1145,7 @@ class ProgressService {
skillName: selectedSkill.name, skillName: selectedSkill.name,
damage: skillResult.result.damage, damage: skillResult.result.damage,
targetName: monsterStats.name, targetName: monsterStats.name,
attackDelayMs: playerStats.attackDelayMs,
), ),
); );
} else if (selectedSkill != null && selectedSkill.isHeal) { } else if (selectedSkill != null && selectedSkill.isHeal) {
@@ -1206,6 +1208,7 @@ class ProgressService {
damage: result.damage, damage: result.damage,
targetName: monsterStats.name, targetName: monsterStats.name,
isCritical: result.isCritical, isCritical: result.isCritical,
attackDelayMs: playerStats.attackDelayMs,
), ),
); );
} }
@@ -1257,6 +1260,7 @@ class ProgressService {
timestamp: timestamp, timestamp: timestamp,
damage: result.damage, damage: result.damage,
attackerName: monsterStats.name, attackerName: monsterStats.name,
attackDelayMs: monsterStats.attackDelayMs,
), ),
); );
} }

View File

@@ -49,6 +49,7 @@ class CombatEvent {
this.isCritical = false, this.isCritical = false,
this.skillName, this.skillName,
this.targetName, this.targetName,
this.attackDelayMs,
}); });
/// 이벤트 타입 /// 이벤트 타입
@@ -72,12 +73,17 @@ class CombatEvent {
/// 대상 이름 (몬스터 또는 플레이어) /// 대상 이름 (몬스터 또는 플레이어)
final String? targetName; final String? targetName;
/// 공격자의 공격 속도 (ms)
/// 애니메이션 페이즈 지속 시간 계산에 사용
final int? attackDelayMs;
/// 플레이어 공격 이벤트 생성 /// 플레이어 공격 이벤트 생성
factory CombatEvent.playerAttack({ factory CombatEvent.playerAttack({
required int timestamp, required int timestamp,
required int damage, required int damage,
required String targetName, required String targetName,
bool isCritical = false, bool isCritical = false,
int? attackDelayMs,
}) { }) {
return CombatEvent( return CombatEvent(
type: CombatEventType.playerAttack, type: CombatEventType.playerAttack,
@@ -85,6 +91,7 @@ class CombatEvent {
damage: damage, damage: damage,
targetName: targetName, targetName: targetName,
isCritical: isCritical, isCritical: isCritical,
attackDelayMs: attackDelayMs,
); );
} }
@@ -93,12 +100,14 @@ class CombatEvent {
required int timestamp, required int timestamp,
required int damage, required int damage,
required String attackerName, required String attackerName,
int? attackDelayMs,
}) { }) {
return CombatEvent( return CombatEvent(
type: CombatEventType.monsterAttack, type: CombatEventType.monsterAttack,
timestamp: timestamp, timestamp: timestamp,
damage: damage, damage: damage,
targetName: attackerName, targetName: attackerName,
attackDelayMs: attackDelayMs,
); );
} }
@@ -161,6 +170,7 @@ class CombatEvent {
required int damage, required int damage,
required String targetName, required String targetName,
bool isCritical = false, bool isCritical = false,
int? attackDelayMs,
}) { }) {
return CombatEvent( return CombatEvent(
type: CombatEventType.playerSkill, type: CombatEventType.playerSkill,
@@ -169,6 +179,7 @@ class CombatEvent {
damage: damage, damage: damage,
targetName: targetName, targetName: targetName,
isCritical: isCritical, isCritical: isCritical,
attackDelayMs: attackDelayMs,
); );
} }

View File

@@ -128,6 +128,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
bool _showParryEffect = false; bool _showParryEffect = false;
bool _showSkillEffect = false; bool _showSkillEffect = false;
// 공격 속도 기반 동적 페이즈 프레임 수 (Phase 6)
int _eventDrivenPhaseFrames = 0;
bool _isEventDrivenPhase = false;
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의 // 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
// specialAnimationFrameCounts 상수 사용 // specialAnimationFrameCounts 상수 사용
@@ -318,6 +322,15 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
// 페이즈 인덱스 동기화 // 페이즈 인덱스 동기화
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase); _phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
if (_phaseIndex < 0) _phaseIndex = 0; if (_phaseIndex < 0) _phaseIndex = 0;
// 공격 속도에 따른 동적 페이즈 프레임 수 계산 (Phase 6)
// 200ms tick 기준으로 프레임 수 계산 (최소 2, 최대 10)
if (event.attackDelayMs != null && event.attackDelayMs! > 0) {
_eventDrivenPhaseFrames = (event.attackDelayMs! ~/ 200).clamp(2, 10);
_isEventDrivenPhase = true;
} else {
_isEventDrivenPhase = false;
}
}); });
} }
@@ -434,8 +447,14 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_phaseFrameCount++; _phaseFrameCount++;
final currentPhase = _battlePhaseSequence[_phaseIndex]; final currentPhase = _battlePhaseSequence[_phaseIndex];
// 현재 페이즈의 프레임 수 결정 (Phase 6)
// 이벤트 기반 페이즈일 경우 공격 속도에 따른 동적 프레임 수 사용
final targetFrames = _isEventDrivenPhase
? _eventDrivenPhaseFrames
: currentPhase.$2;
// 현재 페이즈의 프레임 수를 초과하면 다음 페이즈로 // 현재 페이즈의 프레임 수를 초과하면 다음 페이즈로
if (_phaseFrameCount >= currentPhase.$2) { if (_phaseFrameCount >= targetFrames) {
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length; _phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
_phaseFrameCount = 0; _phaseFrameCount = 0;
_battleSubFrame = 0; _battleSubFrame = 0;
@@ -444,6 +463,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_showBlockEffect = false; _showBlockEffect = false;
_showParryEffect = false; _showParryEffect = false;
_showSkillEffect = false; _showSkillEffect = false;
// 이벤트 기반 페이즈 종료
_isEventDrivenPhase = false;
} else { } else {
_battleSubFrame++; _battleSubFrame++;
} }