feat(ui): 레트로 UI 시스템 추가
- PressStart2P 픽셀 폰트 추가 - RetroColors: 레트로 RPG 스타일 색상 팔레트 - RetroPanel: 픽셀 테두리 패널 위젯 - RetroButton: 레트로 스타일 버튼 - RetroProgressBar: 픽셀 스타일 진행 바 - PixelBorderPainter: 커스텀 테두리 페인터
This commit is contained in:
BIN
assets/fonts/PressStart2P-Regular.ttf
Normal file
BIN
assets/fonts/PressStart2P-Regular.ttf
Normal file
Binary file not shown.
147
lib/src/shared/retro_colors.dart
Normal file
147
lib/src/shared/retro_colors.dart
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 레트로 RPG 스타일 색상 팔레트 (8-bit/16-bit 클래식 RPG 느낌)
|
||||||
|
class RetroColors {
|
||||||
|
RetroColors._();
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 메인 UI 컬러 (Main UI Colors)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 골드 (테두리, 강조, 타이틀)
|
||||||
|
static const Color gold = Color(0xFFD4A84B);
|
||||||
|
|
||||||
|
/// 밝은 골드 (호버, 하이라이트)
|
||||||
|
static const Color goldLight = Color(0xFFE8C97A);
|
||||||
|
|
||||||
|
/// 어두운 골드 (눌림 상태)
|
||||||
|
static const Color goldDark = Color(0xFFB08A3A);
|
||||||
|
|
||||||
|
/// 갈색 (프레임, 테두리)
|
||||||
|
static const Color brown = Color(0xFF8B4513);
|
||||||
|
|
||||||
|
/// 크림색 (텍스트 배경, 밝은 패널)
|
||||||
|
static const Color cream = Color(0xFFF5E6C8);
|
||||||
|
|
||||||
|
/// 다크 브라운 (패널 배경)
|
||||||
|
static const Color darkBrown = Color(0xFF3D2817);
|
||||||
|
|
||||||
|
/// 매우 어두운 브라운 (딥 배경)
|
||||||
|
static const Color deepBrown = Color(0xFF2A1A0F);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 상태 바 컬러 (Status Bar Colors)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// HP 바 빨간색
|
||||||
|
static const Color hpRed = Color(0xFFCC3333);
|
||||||
|
|
||||||
|
/// HP 바 빨간색 (어두운)
|
||||||
|
static const Color hpRedDark = Color(0xFF8B2222);
|
||||||
|
|
||||||
|
/// MP 바 파란색
|
||||||
|
static const Color mpBlue = Color(0xFF3366CC);
|
||||||
|
|
||||||
|
/// MP 바 파란색 (어두운)
|
||||||
|
static const Color mpBlueDark = Color(0xFF224488);
|
||||||
|
|
||||||
|
/// EXP/성공 초록색
|
||||||
|
static const Color expGreen = Color(0xFF33CC66);
|
||||||
|
|
||||||
|
/// EXP/성공 초록색 (어두운)
|
||||||
|
static const Color expGreenDark = Color(0xFF228844);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// ASCII 애니메이션 컬러 (기존 유지)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// ASCII 흰색 (캐릭터/몬스터)
|
||||||
|
static const Color asciiWhite = Color(0xFFFFFFFF);
|
||||||
|
|
||||||
|
/// ASCII 시안 (긍정 효과)
|
||||||
|
static const Color asciiCyan = Color(0xFF00FFFF);
|
||||||
|
|
||||||
|
/// ASCII 마젠타 (부정 효과)
|
||||||
|
static const Color asciiMagenta = Color(0xFFFF00FF);
|
||||||
|
|
||||||
|
/// ASCII 노란색 (경고, 중요)
|
||||||
|
static const Color asciiYellow = Color(0xFFFFFF00);
|
||||||
|
|
||||||
|
/// ASCII 초록색 (성공, 회복)
|
||||||
|
static const Color asciiGreen = Color(0xFF00FF00);
|
||||||
|
|
||||||
|
/// ASCII 빨간색 (데미지, 위험)
|
||||||
|
static const Color asciiRed = Color(0xFFFF0000);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 텍스트 컬러 (Text Colors)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 기본 텍스트 (밝은 배경용)
|
||||||
|
static const Color textDark = Color(0xFF2A1A0F);
|
||||||
|
|
||||||
|
/// 기본 텍스트 (어두운 배경용)
|
||||||
|
static const Color textLight = Color(0xFFF5E6C8);
|
||||||
|
|
||||||
|
/// 비활성 텍스트
|
||||||
|
static const Color textDisabled = Color(0xFF7A6A5A);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 버튼 컬러 (Button Colors)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 주요 버튼 배경
|
||||||
|
static const Color buttonPrimary = Color(0xFF5A4A3A);
|
||||||
|
|
||||||
|
/// 주요 버튼 배경 (눌림)
|
||||||
|
static const Color buttonPrimaryPressed = Color(0xFF3A2A1A);
|
||||||
|
|
||||||
|
/// 보조 버튼 배경
|
||||||
|
static const Color buttonSecondary = Color(0xFF4A3A2A);
|
||||||
|
|
||||||
|
/// 보조 버튼 배경 (눌림)
|
||||||
|
static const Color buttonSecondaryPressed = Color(0xFF2A1A0A);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 패널 컬러 (Panel Colors)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 패널 배경 (기본)
|
||||||
|
static const Color panelBg = Color(0xFF3D2817);
|
||||||
|
|
||||||
|
/// 패널 배경 (밝은)
|
||||||
|
static const Color panelBgLight = Color(0xFF5D4837);
|
||||||
|
|
||||||
|
/// 패널 테두리 (외곽, 어두운)
|
||||||
|
static const Color panelBorderOuter = Color(0xFF1A0F08);
|
||||||
|
|
||||||
|
/// 패널 테두리 (내곽, 밝은)
|
||||||
|
static const Color panelBorderInner = Color(0xFF8B7355);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 유틸리티 메서드 (Utility Methods)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// 레트로 테마용 ColorScheme 생성
|
||||||
|
static ColorScheme get colorScheme => ColorScheme.dark(
|
||||||
|
primary: gold,
|
||||||
|
onPrimary: textDark,
|
||||||
|
primaryContainer: goldDark,
|
||||||
|
onPrimaryContainer: textLight,
|
||||||
|
secondary: brown,
|
||||||
|
onSecondary: textLight,
|
||||||
|
secondaryContainer: darkBrown,
|
||||||
|
onSecondaryContainer: textLight,
|
||||||
|
tertiary: cream,
|
||||||
|
onTertiary: textDark,
|
||||||
|
tertiaryContainer: panelBgLight,
|
||||||
|
onTertiaryContainer: textLight,
|
||||||
|
error: hpRed,
|
||||||
|
onError: textLight,
|
||||||
|
surface: deepBrown,
|
||||||
|
onSurface: textLight,
|
||||||
|
surfaceContainerHighest: panelBg,
|
||||||
|
outline: panelBorderOuter,
|
||||||
|
outlineVariant: panelBorderInner,
|
||||||
|
);
|
||||||
|
}
|
||||||
106
lib/src/shared/widgets/pixel_border_painter.dart
Normal file
106
lib/src/shared/widgets/pixel_border_painter.dart
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 픽셀 스타일 테두리를 그리는 CustomPainter
|
||||||
|
/// 8-bit 게임의 UI 프레임 느낌을 재현
|
||||||
|
class PixelBorderPainter extends CustomPainter {
|
||||||
|
const PixelBorderPainter({
|
||||||
|
this.borderWidth = 3.0,
|
||||||
|
this.outerColor = RetroColors.panelBorderOuter,
|
||||||
|
this.innerColor = RetroColors.panelBorderInner,
|
||||||
|
this.fillColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 테두리 두께 (픽셀 단위로 표현)
|
||||||
|
final double borderWidth;
|
||||||
|
|
||||||
|
/// 외곽 테두리 색상 (어두운 색)
|
||||||
|
final Color outerColor;
|
||||||
|
|
||||||
|
/// 내곽 테두리 색상 (밝은 색, 입체감 표현)
|
||||||
|
final Color innerColor;
|
||||||
|
|
||||||
|
/// 배경 채우기 색상 (null이면 투명)
|
||||||
|
final Color? fillColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final outerPaint = Paint()
|
||||||
|
..color = outerColor
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final innerPaint = Paint()
|
||||||
|
..color = innerColor
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
// 배경 채우기
|
||||||
|
if (fillColor != null) {
|
||||||
|
final bgPaint = Paint()
|
||||||
|
..color = fillColor!
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(borderWidth, borderWidth,
|
||||||
|
size.width - borderWidth * 2, size.height - borderWidth * 2),
|
||||||
|
bgPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 외곽 테두리 (상단, 좌측 - 어두운 색으로 깊이감)
|
||||||
|
// 상단
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(0, 0, size.width, borderWidth),
|
||||||
|
outerPaint,
|
||||||
|
);
|
||||||
|
// 좌측
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(0, 0, borderWidth, size.height),
|
||||||
|
outerPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 외곽 테두리 (하단, 우측 - 어두운 색)
|
||||||
|
// 하단
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(0, size.height - borderWidth, size.width, borderWidth),
|
||||||
|
outerPaint,
|
||||||
|
);
|
||||||
|
// 우측
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(size.width - borderWidth, 0, borderWidth, size.height),
|
||||||
|
outerPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 내곽 하이라이트 (상단, 좌측 내부 - 밝은 색으로 입체감)
|
||||||
|
// 상단 내부
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(borderWidth, borderWidth,
|
||||||
|
size.width - borderWidth * 2, borderWidth * 0.5),
|
||||||
|
innerPaint,
|
||||||
|
);
|
||||||
|
// 좌측 내부
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(borderWidth, borderWidth,
|
||||||
|
borderWidth * 0.5, size.height - borderWidth * 2),
|
||||||
|
innerPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant PixelBorderPainter oldDelegate) {
|
||||||
|
return borderWidth != oldDelegate.borderWidth ||
|
||||||
|
outerColor != oldDelegate.outerColor ||
|
||||||
|
innerColor != oldDelegate.innerColor ||
|
||||||
|
fillColor != oldDelegate.fillColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 골드 테두리 스타일 (타이틀, 중요 패널용)
|
||||||
|
class GoldBorderPainter extends PixelBorderPainter {
|
||||||
|
const GoldBorderPainter({
|
||||||
|
super.borderWidth = 3.0,
|
||||||
|
super.fillColor,
|
||||||
|
}) : super(
|
||||||
|
outerColor: RetroColors.goldDark,
|
||||||
|
innerColor: RetroColors.goldLight,
|
||||||
|
);
|
||||||
|
}
|
||||||
150
lib/src/shared/widgets/retro_button.dart
Normal file
150
lib/src/shared/widgets/retro_button.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 레트로 RPG 스타일 버튼
|
||||||
|
/// 8-bit 게임의 눌림 효과를 재현
|
||||||
|
class RetroButton extends StatefulWidget {
|
||||||
|
const RetroButton({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.onPressed,
|
||||||
|
this.isPrimary = true,
|
||||||
|
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 버튼 내부 컨텐츠
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// 클릭 콜백 (null이면 비활성화)
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
/// Primary 버튼 여부 (색상 차이)
|
||||||
|
final bool isPrimary;
|
||||||
|
|
||||||
|
/// 내부 패딩
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RetroButton> createState() => _RetroButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RetroButtonState extends State<RetroButton> {
|
||||||
|
bool _isPressed = false;
|
||||||
|
|
||||||
|
bool get _isEnabled => widget.onPressed != null;
|
||||||
|
|
||||||
|
Color get _backgroundColor {
|
||||||
|
if (!_isEnabled) return RetroColors.buttonSecondary.withValues(alpha: 0.5);
|
||||||
|
if (_isPressed) {
|
||||||
|
return widget.isPrimary
|
||||||
|
? RetroColors.buttonPrimaryPressed
|
||||||
|
: RetroColors.buttonSecondaryPressed;
|
||||||
|
}
|
||||||
|
return widget.isPrimary
|
||||||
|
? RetroColors.buttonPrimary
|
||||||
|
: RetroColors.buttonSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get _borderTopLeft {
|
||||||
|
if (_isPressed) return RetroColors.panelBorderOuter;
|
||||||
|
return RetroColors.panelBorderInner;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get _borderBottomRight {
|
||||||
|
if (_isPressed) return RetroColors.panelBorderInner;
|
||||||
|
return RetroColors.panelBorderOuter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTapDown: _isEnabled ? (_) => setState(() => _isPressed = true) : null,
|
||||||
|
onTapUp: _isEnabled ? (_) => setState(() => _isPressed = false) : null,
|
||||||
|
onTapCancel: _isEnabled ? () => setState(() => _isPressed = false) : null,
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 50),
|
||||||
|
padding: widget.padding,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _backgroundColor,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: _borderTopLeft, width: 2),
|
||||||
|
left: BorderSide(color: _borderTopLeft, width: 2),
|
||||||
|
bottom: BorderSide(color: _borderBottomRight, width: 2),
|
||||||
|
right: BorderSide(color: _borderBottomRight, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
transform: _isPressed
|
||||||
|
? Matrix4.translationValues(1, 1, 0)
|
||||||
|
: Matrix4.identity(),
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: _isEnabled ? RetroColors.textLight : RetroColors.textDisabled,
|
||||||
|
),
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레트로 텍스트 버튼 (간편 생성용)
|
||||||
|
class RetroTextButton extends StatelessWidget {
|
||||||
|
const RetroTextButton({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
this.onPressed,
|
||||||
|
this.isPrimary = true,
|
||||||
|
this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool isPrimary;
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RetroButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
isPrimary: isPrimary,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, size: 14, color: RetroColors.textLight),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Text(text.toUpperCase()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레트로 아이콘 버튼
|
||||||
|
class RetroIconButton extends StatelessWidget {
|
||||||
|
const RetroIconButton({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
this.onPressed,
|
||||||
|
this.size = 32,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RetroButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
padding: EdgeInsets.all(size * 0.25),
|
||||||
|
child: Icon(icon, size: size * 0.5, color: RetroColors.textLight),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
lib/src/shared/widgets/retro_panel.dart
Normal file
120
lib/src/shared/widgets/retro_panel.dart
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/shared/retro_colors.dart';
|
||||||
|
import 'package:askiineverdie/src/shared/widgets/pixel_border_painter.dart';
|
||||||
|
|
||||||
|
/// 레트로 RPG 스타일 패널
|
||||||
|
/// 8-bit 게임의 UI 프레임 느낌을 재현
|
||||||
|
class RetroPanel extends StatelessWidget {
|
||||||
|
const RetroPanel({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.padding = const EdgeInsets.all(12),
|
||||||
|
this.backgroundColor = RetroColors.panelBg,
|
||||||
|
this.borderWidth = 3.0,
|
||||||
|
this.useGoldBorder = false,
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 패널 내부 컨텐츠
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// 내부 패딩
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
/// 배경 색상
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// 테두리 두께
|
||||||
|
final double borderWidth;
|
||||||
|
|
||||||
|
/// 골드 테두리 사용 여부 (중요한 패널에 사용)
|
||||||
|
final bool useGoldBorder;
|
||||||
|
|
||||||
|
/// 패널 타이틀 (상단에 표시)
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final painter = useGoldBorder
|
||||||
|
? GoldBorderPainter(borderWidth: borderWidth, fillColor: backgroundColor)
|
||||||
|
: PixelBorderPainter(borderWidth: borderWidth, fillColor: backgroundColor);
|
||||||
|
|
||||||
|
return CustomPaint(
|
||||||
|
painter: painter,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(borderWidth).add(padding),
|
||||||
|
child: title != null
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_PanelTitle(title: title!, useGoldBorder: useGoldBorder),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Flexible(child: child),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 패널 타이틀 위젯
|
||||||
|
class _PanelTitle extends StatelessWidget {
|
||||||
|
const _PanelTitle({required this.title, required this.useGoldBorder});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final bool useGoldBorder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: useGoldBorder
|
||||||
|
? RetroColors.goldDark.withValues(alpha: 0.3)
|
||||||
|
: RetroColors.panelBorderOuter.withValues(alpha: 0.5),
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: useGoldBorder ? RetroColors.gold : RetroColors.panelBorderInner,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: useGoldBorder ? RetroColors.gold : RetroColors.textLight,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 골드 테두리 레트로 패널 (중요한 컨텐츠용)
|
||||||
|
class RetroGoldPanel extends StatelessWidget {
|
||||||
|
const RetroGoldPanel({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.padding = const EdgeInsets.all(12),
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RetroPanel(
|
||||||
|
useGoldBorder: true,
|
||||||
|
padding: padding,
|
||||||
|
title: title,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
lib/src/shared/widgets/retro_widgets.dart
Normal file
8
lib/src/shared/widgets/retro_widgets.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/// 레트로 RPG 스타일 UI 위젯 모음
|
||||||
|
/// 8-bit/16-bit 클래식 RPG 느낌의 UI 컴포넌트
|
||||||
|
library;
|
||||||
|
|
||||||
|
export 'pixel_border_painter.dart';
|
||||||
|
export 'retro_button.dart';
|
||||||
|
export 'retro_panel.dart';
|
||||||
|
export 'retro_progress_bar.dart';
|
||||||
@@ -92,8 +92,13 @@ flutter:
|
|||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
# weight: 700
|
# weight: 700
|
||||||
#
|
#
|
||||||
# 커스텀 모노스페이스 폰트 (Canvas ASCII 렌더링용)
|
# 커스텀 폰트
|
||||||
fonts:
|
fonts:
|
||||||
|
# 모노스페이스 폰트 (ASCII 렌더링용)
|
||||||
- family: JetBrainsMono
|
- family: JetBrainsMono
|
||||||
fonts:
|
fonts:
|
||||||
- asset: assets/fonts/JetBrainsMono-Regular.ttf
|
- asset: assets/fonts/JetBrainsMono-Regular.ttf
|
||||||
|
# 픽셀 폰트 (레트로 UI용)
|
||||||
|
- family: PressStart2P
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/PressStart2P-Regular.ttf
|
||||||
|
|||||||
Reference in New Issue
Block a user