feat(ui): 레트로 UI 시스템 추가
- PressStart2P 픽셀 폰트 추가 - RetroColors: 레트로 RPG 스타일 색상 팔레트 - RetroPanel: 픽셀 테두리 패널 위젯 - RetroButton: 레트로 스타일 버튼 - RetroProgressBar: 픽셀 스타일 진행 바 - PixelBorderPainter: 커스텀 테두리 페인터
This commit is contained in:
258
lib/src/shared/widgets/retro_progress_bar.dart
Normal file
258
lib/src/shared/widgets/retro_progress_bar.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user