feat(ui): Phase 8 UI/UX 개선 완료

- CombatLog 위젯 게임 화면에 통합
- HP/MP 바 추가 (HP < 20% 깜빡임 효과)
- SkillPanel 추가 (쿨타임 완료 시 glow 효과)
- combatLog 로컬라이제이션 (4개 언어)
- 테스트 수정 (skipOffstage 처리)
This commit is contained in:
JiWoong Sul
2025-12-17 18:52:24 +09:00
parent abcb89d334
commit 7c7f3b0d9e
13 changed files with 480 additions and 4 deletions

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
/// HP/MP 바 위젯 (Phase 8: 사망 위험 시 깜빡임)
///
/// HP가 20% 미만일 때 빨간색 깜빡임 효과 표시
class HpMpBar extends StatefulWidget {
const HpMpBar({
super.key,
required this.hpCurrent,
required this.hpMax,
required this.mpCurrent,
required this.mpMax,
});
final int hpCurrent;
final int hpMax;
final int mpCurrent;
final int mpMax;
@override
State<HpMpBar> createState() => _HpMpBarState();
}
class _HpMpBarState extends State<HpMpBar> with SingleTickerProviderStateMixin {
late AnimationController _blinkController;
late Animation<double> _blinkAnimation;
@override
void initState() {
super.initState();
_blinkController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_blinkAnimation = Tween<double>(begin: 1.0, end: 0.3).animate(
CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut),
);
_updateBlinkState();
}
@override
void didUpdateWidget(HpMpBar oldWidget) {
super.didUpdateWidget(oldWidget);
_updateBlinkState();
}
void _updateBlinkState() {
final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 1.0;
// HP < 20% 시 깜박임 시작
if (hpRatio < 0.2 && hpRatio > 0) {
if (!_blinkController.isAnimating) {
_blinkController.repeat(reverse: true);
}
} else {
_blinkController.stop();
_blinkController.reset();
}
}
@override
void dispose() {
_blinkController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 0.0;
final mpRatio = widget.mpMax > 0 ? widget.mpCurrent / widget.mpMax : 0.0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// HP 바
_buildBar(
label: 'HP',
current: widget.hpCurrent,
max: widget.hpMax,
ratio: hpRatio,
color: Colors.red,
isLow: hpRatio < 0.2 && hpRatio > 0,
),
const SizedBox(height: 4),
// MP 바
_buildBar(
label: 'MP',
current: widget.mpCurrent,
max: widget.mpMax,
ratio: mpRatio,
color: Colors.blue,
isLow: false,
),
],
),
);
}
Widget _buildBar({
required String label,
required int current,
required int max,
required double ratio,
required Color color,
required bool isLow,
}) {
final bar = Row(
children: [
SizedBox(
width: 24,
child: Text(
label,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: ratio.clamp(0.0, 1.0),
backgroundColor: color.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 10,
),
),
),
const SizedBox(width: 4),
SizedBox(
width: 60,
child: Text(
'$current/$max',
style: const TextStyle(fontSize: 9),
textAlign: TextAlign.right,
),
),
],
);
// HP < 20% 시 깜박임 효과 적용
if (isLow) {
return AnimatedBuilder(
animation: _blinkAnimation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: (1 - _blinkAnimation.value) * 0.3),
borderRadius: BorderRadius.circular(4),
),
child: child,
);
},
child: bar,
);
}
return bar;
}
}