diff --git a/assets/fonts/PressStart2P-Regular.ttf b/assets/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/assets/fonts/PressStart2P-Regular.ttf differ diff --git a/lib/src/shared/retro_colors.dart b/lib/src/shared/retro_colors.dart new file mode 100644 index 0000000..66c758c --- /dev/null +++ b/lib/src/shared/retro_colors.dart @@ -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, + ); +} diff --git a/lib/src/shared/widgets/pixel_border_painter.dart b/lib/src/shared/widgets/pixel_border_painter.dart new file mode 100644 index 0000000..ccaeedf --- /dev/null +++ b/lib/src/shared/widgets/pixel_border_painter.dart @@ -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, + ); +} diff --git a/lib/src/shared/widgets/retro_button.dart b/lib/src/shared/widgets/retro_button.dart new file mode 100644 index 0000000..0bc6fcd --- /dev/null +++ b/lib/src/shared/widgets/retro_button.dart @@ -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 createState() => _RetroButtonState(); +} + +class _RetroButtonState extends State { + 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), + ); + } +} diff --git a/lib/src/shared/widgets/retro_panel.dart b/lib/src/shared/widgets/retro_panel.dart new file mode 100644 index 0000000..3196190 --- /dev/null +++ b/lib/src/shared/widgets/retro_panel.dart @@ -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, + ); + } +} diff --git a/lib/src/shared/widgets/retro_progress_bar.dart b/lib/src/shared/widgets/retro_progress_bar.dart new file mode 100644 index 0000000..023e817 --- /dev/null +++ b/lib/src/shared/widgets/retro_progress_bar.dart @@ -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, + ); + } +} diff --git a/lib/src/shared/widgets/retro_widgets.dart b/lib/src/shared/widgets/retro_widgets.dart new file mode 100644 index 0000000..4778b42 --- /dev/null +++ b/lib/src/shared/widgets/retro_widgets.dart @@ -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'; diff --git a/pubspec.yaml b/pubspec.yaml index 2953737..c4d38b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,8 +92,13 @@ flutter: # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # - # 커스텀 모노스페이스 폰트 (Canvas ASCII 렌더링용) + # 커스텀 폰트 fonts: + # 모노스페이스 폰트 (ASCII 렌더링용) - family: JetBrainsMono fonts: - asset: assets/fonts/JetBrainsMono-Regular.ttf + # 픽셀 폰트 (레트로 UI용) + - family: PressStart2P + fonts: + - asset: assets/fonts/PressStart2P-Regular.ttf