feat(ui): HP/MP 바 개선 및 전투 시스템 UI 업데이트

- HP/MP 변화 시 플래시 효과 및 변화량 표시 추가
- 전투 중 몬스터 HP 바 표시 기능 추가
- 몬스터 HP 바 Row 오버플로우 버그 수정 (Flexible 적용)
- 전투 상태 및 이벤트 모델 개선
- 캐릭터 애니메이션 및 전투 컴포저 업데이트
This commit is contained in:
JiWoong Sul
2025-12-18 18:10:22 +09:00
parent 45147da5ec
commit cf8fdaecde
14 changed files with 1220 additions and 153 deletions

View File

@@ -10,6 +10,7 @@ import 'package:askiineverdie/src/core/animation/character_frames.dart';
import 'package:askiineverdie/src/core/animation/monster_size.dart';
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
import 'package:askiineverdie/src/core/constants/ascii_colors.dart';
import 'package:askiineverdie/src/core/model/combat_event.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
/// ASCII 애니메이션 카드 위젯
@@ -30,6 +31,7 @@ class AsciiAnimationCard extends StatefulWidget {
this.characterLevel,
this.monsterLevel,
this.isPaused = false,
this.latestCombatEvent,
});
final TaskType taskType;
@@ -57,6 +59,9 @@ class AsciiAnimationCard extends StatefulWidget {
/// 몬스터 레벨 (몬스터 크기 결정용)
final int? monsterLevel;
/// 최근 전투 이벤트 (애니메이션 동기화용)
final CombatEvent? latestCombatEvent;
@override
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
}
@@ -90,6 +95,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
int _phaseIndex = 0;
int _phaseFrameCount = 0;
// 전투 이벤트 동기화용 (Phase 5)
int? _lastEventTimestamp;
bool _showCriticalEffect = false;
@override
void initState() {
super.initState();
@@ -124,6 +133,12 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
return;
}
// 전투 이벤트 동기화 (Phase 5)
if (widget.latestCombatEvent != null &&
widget.latestCombatEvent!.timestamp != _lastEventTimestamp) {
_handleCombatEvent(widget.latestCombatEvent!);
}
if (oldWidget.taskType != widget.taskType ||
oldWidget.monsterBaseName != widget.monsterBaseName ||
oldWidget.weaponName != widget.weaponName ||
@@ -133,6 +148,45 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
}
}
/// 전투 이벤트에 따라 애니메이션 페이즈 강제 전환 (Phase 5)
void _handleCombatEvent(CombatEvent event) {
_lastEventTimestamp = event.timestamp;
// 전투 모드가 아니면 무시
if (!_isBattleMode) return;
// 이벤트 타입에 따라 페이즈 강제 전환
final (targetPhase, isCritical) = switch (event.type) {
// 플레이어 공격 → attack 페이즈
CombatEventType.playerAttack => (BattlePhase.attack, event.isCritical),
CombatEventType.playerSkill => (BattlePhase.attack, event.isCritical),
// 몬스터 공격/플레이어 피격 → hit 페이즈
CombatEventType.monsterAttack => (BattlePhase.hit, false),
CombatEventType.playerBlock => (BattlePhase.hit, false),
CombatEventType.playerParry => (BattlePhase.hit, false),
// 회피 → recover 페이즈 (빠른 회피 동작)
CombatEventType.playerEvade => (BattlePhase.recover, false),
CombatEventType.monsterEvade => (BattlePhase.idle, false),
// 회복/버프 → idle 페이즈 유지
CombatEventType.playerHeal => (BattlePhase.idle, false),
CombatEventType.playerBuff => (BattlePhase.idle, false),
};
setState(() {
_battlePhase = targetPhase;
_battleSubFrame = 0;
_phaseFrameCount = 0;
_showCriticalEffect = isCritical;
// 페이즈 인덱스 동기화
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
if (_phaseIndex < 0) _phaseIndex = 0;
});
}
/// 현재 상태를 유지하면서 타이머만 재시작
void _restartTimer() {
_timer?.cancel();
@@ -286,6 +340,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
_phaseFrameCount = 0;
_battleSubFrame = 0;
// 크리티컬 이펙트 리셋 (페이즈 전환 시)
_showCriticalEffect = false;
} else {
_battleSubFrame++;
}
@@ -376,17 +432,23 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
frameText = _animationData.frames[frameIndex];
}
// 특수 애니메이션 중이면 테두리 표시
// 테두리 효과 결정 (특수 애니메이션 또는 크리티컬 히트)
final isSpecial = _currentSpecialAnimation != null;
Border? borderEffect;
if (_showCriticalEffect) {
// 크리티컬 히트: 노란색 테두리 (Phase 5)
borderEffect = Border.all(color: Colors.yellow.withValues(alpha: 0.8), width: 2);
} else if (isSpecial) {
// 특수 애니메이션: 시안 테두리
borderEffect = Border.all(color: AsciiColors.positive.withValues(alpha: 0.5));
}
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4),
border: isSpecial
? Border.all(color: AsciiColors.positive.withValues(alpha: 0.5))
: null,
border: borderEffect,
),
child: _isBattleMode
? LayoutBuilder(