Files
asciinevrdie/lib/src/features/arena/widgets/arena_hp_bar.dart
JiWoong Sul 864a866039 refactor(ui): 위젯 분리 및 화면 개선
- game_play_screen에서 desktop 패널 위젯 분리
- death_overlay에서 death_buttons, death_combat_log 분리
- mobile_carousel_layout에서 mobile_options_menu 분리
- 아레나 위젯 개선 (arena_hp_bar, result_panel 등)
- settings_screen에서 retro_settings_widgets 분리
- 기타 위젯 리팩토링 및 import 경로 업데이트
2026-02-23 15:49:38 +09:00

255 lines
8.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:asciineverdie/l10n/app_localizations.dart';
import 'package:asciineverdie/src/shared/retro_colors.dart';
/// 아레나 전투 HP 바 (좌우 대칭 레이아웃)
class ArenaHpBars extends StatelessWidget {
const ArenaHpBars({
super.key,
required this.challengerName,
required this.challengerHp,
required this.challengerHpMax,
required this.challengerFlashAnimation,
required this.challengerHpChange,
required this.opponentName,
required this.opponentHp,
required this.opponentHpMax,
required this.opponentFlashAnimation,
required this.opponentHpChange,
});
final String challengerName;
final int challengerHp;
final int challengerHpMax;
final Animation<double> challengerFlashAnimation;
final int challengerHpChange;
final String opponentName;
final int opponentHp;
final int opponentHpMax;
final Animation<double> opponentFlashAnimation;
final int opponentHpChange;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: RetroColors.panelBgOf(context),
border: Border(
bottom: BorderSide(color: RetroColors.borderOf(context), width: 2),
),
),
child: Row(
children: [
Expanded(
child: _ArenaHpBar(
name: challengerName,
hp: challengerHp,
hpMax: challengerHpMax,
fillColor: RetroColors.mpBlue,
accentColor: Colors.blue,
flashAnimation: challengerFlashAnimation,
hpChange: challengerHpChange,
isReversed: false,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
'VS',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 13,
color: RetroColors.goldOf(context),
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: _ArenaHpBar(
name: opponentName,
hp: opponentHp,
hpMax: opponentHpMax,
fillColor: RetroColors.hpRed,
accentColor: Colors.red,
flashAnimation: opponentFlashAnimation,
hpChange: opponentHpChange,
isReversed: true,
),
),
],
),
);
}
}
/// 레트로 세그먼트 HP 바 (개별)
class _ArenaHpBar extends StatelessWidget {
const _ArenaHpBar({
required this.name,
required this.hp,
required this.hpMax,
required this.fillColor,
required this.accentColor,
required this.flashAnimation,
required this.hpChange,
required this.isReversed,
});
final String name;
final int hp;
final int hpMax;
final Color fillColor;
final Color accentColor;
final Animation<double> flashAnimation;
final int hpChange;
final bool isReversed;
@override
Widget build(BuildContext context) {
final hpRatio = hpMax > 0 ? hp / hpMax : 0.0;
final isLow = hpRatio < 0.2 && hpRatio > 0;
return AnimatedBuilder(
animation: flashAnimation,
builder: (context, child) {
final isDamage = hpChange < 0;
final flashColor = isDamage
? RetroColors.hpRed.withValues(alpha: flashAnimation.value * 0.4)
: RetroColors.expGreen.withValues(
alpha: flashAnimation.value * 0.4,
);
return Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: flashAnimation.value > 0.1
? flashColor
: accentColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: accentColor, width: 2),
),
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
crossAxisAlignment: isReversed
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.textPrimaryOf(context),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
_buildSegmentBar(context, hpRatio, isLow),
const SizedBox(height: 2),
Row(
mainAxisAlignment: isReversed
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Text(
L10n.of(context).hpLabel,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: accentColor.withValues(alpha: 0.8),
),
),
const SizedBox(width: 4),
Text(
'$hp/$hpMax',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: isLow ? RetroColors.hpRed : fillColor,
),
),
],
),
],
),
if (hpChange != 0 && flashAnimation.value > 0.05)
Positioned(
left: isReversed ? null : 0,
right: isReversed ? 0 : null,
top: -12,
child: Transform.translate(
offset: Offset(0, -12 * (1 - flashAnimation.value)),
child: Opacity(
opacity: flashAnimation.value,
child: Text(
hpChange > 0 ? '+$hpChange' : '$hpChange',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 13,
fontWeight: FontWeight.bold,
color: isDamage
? RetroColors.hpRed
: RetroColors.expGreen,
shadows: const [
Shadow(color: Colors.black, blurRadius: 3),
Shadow(color: Colors.black, blurRadius: 6),
],
),
),
),
),
),
],
),
);
},
);
}
/// 세그먼트 바 (8-bit 스타일)
Widget _buildSegmentBar(BuildContext context, double ratio, bool isLow) {
const segmentCount = 10;
final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round();
final segments = List.generate(segmentCount, (index) {
final isFilled = isReversed
? index >= segmentCount - filledSegments
: index < filledSegments;
return Expanded(
child: Container(
height: 8,
decoration: BoxDecoration(
color: isFilled
? (isLow ? RetroColors.hpRed : fillColor)
: fillColor.withValues(alpha: 0.2),
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.borderOf(
context,
).withValues(alpha: 0.3),
width: 1,
)
: BorderSide.none,
),
),
),
);
});
return Container(
decoration: BoxDecoration(
border: Border.all(color: RetroColors.borderOf(context), width: 1),
),
child: Row(children: isReversed ? segments.reversed.toList() : segments),
);
}
}