feat(ui): 레트로 UI 시스템 추가

- PressStart2P 픽셀 폰트 추가
- RetroColors: 레트로 RPG 스타일 색상 팔레트
- RetroPanel: 픽셀 테두리 패널 위젯
- RetroButton: 레트로 스타일 버튼
- RetroProgressBar: 픽셀 스타일 진행 바
- PixelBorderPainter: 커스텀 테두리 페인터
This commit is contained in:
JiWoong Sul
2025-12-30 18:30:51 +09:00
parent 2d797502a3
commit 708148c767
8 changed files with 795 additions and 1 deletions

View File

@@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 레트로 RPG 스타일 프로그레스 바
/// 세그먼트 스타일로 8-bit 게임 느낌 재현
class RetroProgressBar extends StatelessWidget {
const RetroProgressBar({
super.key,
required this.value,
this.maxValue = 1.0,
this.height = 16,
this.segmentCount = 20,
this.fillColor = RetroColors.expGreen,
this.emptyColor = RetroColors.panelBorderOuter,
this.showSegments = true,
this.label,
this.showPercentage = false,
});
/// 현재 값 (0.0 ~ maxValue)
final double value;
/// 최대 값
final double maxValue;
/// 바 높이
final double height;
/// 세그먼트 개수 (showSegments가 true일 때)
final int segmentCount;
/// 채워진 부분 색상
final Color fillColor;
/// 빈 부분 색상
final Color emptyColor;
/// 세그먼트 표시 여부
final bool showSegments;
/// 레이블 (좌측에 표시)
final String? label;
/// 퍼센트 표시 여부
final bool showPercentage;
double get _percentage => maxValue > 0 ? (value / maxValue).clamp(0.0, 1.0) : 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
if (label != null) ...[
Text(
label!,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: height * 0.5,
color: RetroColors.textLight,
),
),
const SizedBox(width: 8),
],
Expanded(
child: Container(
height: height,
decoration: BoxDecoration(
color: emptyColor,
border: Border.all(color: RetroColors.panelBorderOuter, width: 2),
),
child: showSegments
? _buildSegmentedBar()
: _buildSolidBar(),
),
),
if (showPercentage) ...[
const SizedBox(width: 8),
SizedBox(
width: 48,
child: Text(
'${(_percentage * 100).toInt()}%',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: height * 0.45,
color: RetroColors.textLight,
),
textAlign: TextAlign.right,
),
),
],
],
);
}
Widget _buildSegmentedBar() {
final filledSegments = (_percentage * segmentCount).round();
return LayoutBuilder(
builder: (context, constraints) {
final segmentWidth = constraints.maxWidth / segmentCount;
return Row(
children: List.generate(segmentCount, (index) {
final isFilled = index < filledSegments;
return Container(
width: segmentWidth,
decoration: BoxDecoration(
color: isFilled ? fillColor : emptyColor,
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.panelBorderOuter.withValues(alpha: 0.5),
width: 1,
)
: BorderSide.none,
),
),
);
}),
);
},
);
}
Widget _buildSolidBar() {
return FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: _percentage,
child: Container(color: fillColor),
);
}
}
/// HP 바 (빨간색)
class RetroHpBar extends StatelessWidget {
const RetroHpBar({
super.key,
required this.current,
required this.max,
this.height = 16,
this.showLabel = true,
this.showValue = false,
});
final int current;
final int max;
final double height;
final bool showLabel;
final bool showValue;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
RetroProgressBar(
value: current.toDouble(),
maxValue: max.toDouble(),
height: height,
fillColor: RetroColors.hpRed,
emptyColor: RetroColors.hpRedDark.withValues(alpha: 0.3),
label: showLabel ? 'HP' : null,
),
if (showValue) ...[
const SizedBox(height: 2),
Text(
'$current / $max',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: height * 0.4,
color: RetroColors.textLight.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
],
],
);
}
}
/// MP 바 (파란색)
class RetroMpBar extends StatelessWidget {
const RetroMpBar({
super.key,
required this.current,
required this.max,
this.height = 16,
this.showLabel = true,
this.showValue = false,
});
final int current;
final int max;
final double height;
final bool showLabel;
final bool showValue;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
RetroProgressBar(
value: current.toDouble(),
maxValue: max.toDouble(),
height: height,
fillColor: RetroColors.mpBlue,
emptyColor: RetroColors.mpBlueDark.withValues(alpha: 0.3),
label: showLabel ? 'MP' : null,
),
if (showValue) ...[
const SizedBox(height: 2),
Text(
'$current / $max',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: height * 0.4,
color: RetroColors.textLight.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
],
],
);
}
}
/// EXP 바 (초록색)
class RetroExpBar extends StatelessWidget {
const RetroExpBar({
super.key,
required this.current,
required this.max,
this.height = 12,
this.showLabel = true,
this.showPercentage = true,
});
final int current;
final int max;
final double height;
final bool showLabel;
final bool showPercentage;
@override
Widget build(BuildContext context) {
return RetroProgressBar(
value: current.toDouble(),
maxValue: max.toDouble(),
height: height,
fillColor: RetroColors.expGreen,
emptyColor: RetroColors.expGreenDark.withValues(alpha: 0.3),
label: showLabel ? 'EXP' : null,
showPercentage: showPercentage,
);
}
}