From 78267b7e47003e0b4204b1a491684c6c42c3320c Mon Sep 17 00:00:00 2001 From: Horoli Date: Sun, 7 Dec 2025 19:18:40 +0900 Subject: [PATCH] Feat: Implement Enemy Attack Animation --- lib/screens/battle_screen.dart | 71 ++++++++++++++++++++++++++--- prompt/62_enemy_attack_animation.md | 17 +++++++ 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 prompt/62_enemy_attack_animation.md diff --git a/lib/screens/battle_screen.dart b/lib/screens/battle_screen.dart index c0c1cc3..82412a1 100644 --- a/lib/screens/battle_screen.dart +++ b/lib/screens/battle_screen.dart @@ -42,10 +42,13 @@ class _BattleScreenState extends State { final GlobalKey _shakeKey = GlobalKey(); final GlobalKey _playerAnimKey = GlobalKey(); + final GlobalKey _enemyAnimKey = + GlobalKey(); // Added Enemy Anim Key final GlobalKey _explosionKey = GlobalKey(); bool _showLogs = false; bool _isPlayerAttacking = false; // Player Attack Animation State + bool _isEnemyAttacking = false; // Enemy Attack Animation State @override void initState() { @@ -287,8 +290,60 @@ class _BattleScreenState extends State { } }); } + } else if (event.type == ActionType.attack && + event.target == EffectTarget.player && + event.feedbackType == null) { + // 2. Enemy Attack Animation Trigger + final RenderBox? playerBox = + _playerKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? enemyBox = + _enemyKey.currentContext?.findRenderObject() as RenderBox?; + + if (playerBox != null && enemyBox != null) { + final playerPos = playerBox.localToGlobal(Offset.zero); + final enemyPos = enemyBox.localToGlobal(Offset.zero); + + // Enemy moves TO Player + final offset = playerPos - enemyPos; + + // Start Animation: Hide Stats + setState(() { + _isEnemyAttacking = true; + }); + + _enemyAnimKey.currentState + ?.animateAttack(offset, () { + showEffect(); // Show Effect at Impact! + + // Shake and Explosion ONLY for Risky (Enemy can also do risky attacks) + if (event.risk == RiskLevel.risky) { + _shakeKey.currentState?.shake(); + + // Explosion on Player + RenderBox? stackBox = + _stackKey.currentContext?.findRenderObject() + as RenderBox?; + if (stackBox != null) { + Offset localPlayerPos = stackBox.globalToLocal(playerPos); + localPlayerPos += Offset( + playerBox.size.width / 2, + playerBox.size.height / 2, + ); + _explosionKey.currentState?.explode(localPlayerPos); + } + } + }, event.risk) + .then((_) { + // End Animation: Show Stats + if (mounted) { + setState(() { + _isEnemyAttacking = false; + }); + } + }); + } } else { - // Not a player attack, show immediately + // Not a player/enemy attack movement, show immediately showEffect(); } }); @@ -444,11 +499,15 @@ class _BattleScreenState extends State { Positioned( top: 0, right: 0, - child: CharacterStatusCard( - character: battleProvider.enemy, - isPlayer: false, - isTurn: !battleProvider.isPlayerTurn, - key: _enemyKey, + child: BattleAnimationWidget( + key: _enemyAnimKey, + child: CharacterStatusCard( + character: battleProvider.enemy, + isPlayer: false, + isTurn: !battleProvider.isPlayerTurn, + key: _enemyKey, + hideStats: _isEnemyAttacking, + ), ), ), // Player (Bottom Left) diff --git a/prompt/62_enemy_attack_animation.md b/prompt/62_enemy_attack_animation.md new file mode 100644 index 0000000..76f47ad --- /dev/null +++ b/prompt/62_enemy_attack_animation.md @@ -0,0 +1,17 @@ +# 62. Implement Enemy Attack Animation + +## 1. 목표 (Goal) +- 플레이어뿐만 아니라 적(Enemy)이 공격할 때도 플레이어 쪽으로 돌진하는 애니메이션을 추가하여 전투의 역동성을 높입니다. + +## 2. 구현 계획 (Implementation Plan) +1. **`BattleScreen` 수정:** + - `GlobalKey _enemyAnimKey`를 추가합니다. + - 적 캐릭터 UI(`CharacterStatusCard`)를 `BattleAnimationWidget`으로 감쌉니다. +2. **애니메이션 트리거 로직 (`_addFloatingEffect`):** + - 기존 플레이어 공격 감지 로직과 유사하게, `event.type == ActionType.attack`이고 `event.target == EffectTarget.player`인 경우를 감지합니다. + - 적 위치에서 플레이어 위치로의 오프셋(`playerPos - enemyPos`)을 계산하여 `_enemyAnimKey`로 애니메이션을 실행합니다. + - 공격 모션 중에는 적의 스탯 정보(HP바 등)를 일시적으로 숨기는 로직(`_isEnemyAttacking`)도 추가합니다. + +## 3. 기대 효과 (Expected Outcome) +- 적의 턴에도 시각적인 움직임이 발생하여 전투가 더 생동감 있게 느껴짐. +- 플레이어와 적의 상호작용이 명확해짐.