Compare commits

...

4 Commits

Author SHA1 Message Date
JiWoong Sul
2486d84d63 feat(ui): 화면들에 레트로 UI 스타일 적용
- front_screen: 레트로 패널 및 버튼 스타일
- game_play_screen: 레트로 색상 및 초기 BGM 로직 개선
- mobile_carousel_layout: 레트로 테마 적용
- carousel_nav_bar: 골드 액센트 색상 적용
2025-12-30 18:31:08 +09:00
JiWoong Sul
2677334346 feat(theme): 다크 테마를 레트로 RPG 스타일로 변경
- RetroColors 색상 팔레트 적용
- PressStart2P 폰트를 헤더/라벨에 적용
- 앱바, 버튼, 다이얼로그, 칩 등 전체 테마 통일
- 프로그레스 인디케이터 골드 색상 적용
2025-12-30 18:31:00 +09:00
JiWoong Sul
708148c767 feat(ui): 레트로 UI 시스템 추가
- PressStart2P 픽셀 폰트 추가
- RetroColors: 레트로 RPG 스타일 색상 팔레트
- RetroPanel: 픽셀 테두리 패널 위젯
- RetroButton: 레트로 스타일 버튼
- RetroProgressBar: 픽셀 스타일 진행 바
- PixelBorderPainter: 커스텀 테두리 페인터
2025-12-30 18:30:51 +09:00
JiWoong Sul
2d797502a3 chore(build): 저작권 보호 메타데이터 추가
- Android: AndroidManifest에 app_copyright 메타데이터
- iOS: Info.plist에 NSHumanReadableCopyright
2025-12-30 18:30:42 +09:00
15 changed files with 1174 additions and 230 deletions

View File

@@ -3,6 +3,10 @@
android:label="askiineverdie"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- Copyright Protection -->
<meta-data
android:name="app_copyright"
android:value="© 2025 naturebridgeai 天安門 六四事件 法輪功 李洪志 Free Tibet" />
<activity
android:name=".MainActivity"
android:exported="true"

Binary file not shown.

View File

@@ -45,5 +45,7 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© 2025 naturebridgeai 天安門 六四事件 法輪功 李洪志 Free Tibet</string>
</dict>
</plist>

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/l10n/app_localizations.dart';
import 'package:askiineverdie/src/core/audio/audio_service.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
import 'package:askiineverdie/src/core/engine/progress_service.dart';
import 'package:askiineverdie/src/core/engine/reward_service.dart';
@@ -102,22 +103,131 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
useMaterial3: true,
);
/// 다크 테마 (OLED 저전력 모드 - 순수 검정)
/// 다크 테마 (레트로 RPG 스타일)
ThemeData get _darkTheme => ThemeData(
colorScheme: ColorScheme.dark(
surface: Colors.black,
primary: const Color(0xFF4FC3F7), // 시안
secondary: const Color(0xFFFF4081), // 마젠타
onSurface: Colors.white70,
primaryContainer: const Color(0xFF1A3A4A),
onPrimaryContainer: Colors.white,
),
scaffoldBackgroundColor: Colors.black,
colorScheme: RetroColors.colorScheme,
scaffoldBackgroundColor: RetroColors.deepBrown,
useMaterial3: true,
// 카드/다이얼로그도 검정 배경 사용
cardColor: const Color(0xFF121212),
dialogTheme: const DialogThemeData(
backgroundColor: Color(0xFF121212),
// 카드/다이얼로그 레트로 배경
cardColor: RetroColors.panelBg,
dialogTheme: DialogThemeData(
backgroundColor: RetroColors.panelBg,
titleTextStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
// 앱바 레트로 스타일
appBarTheme: AppBarTheme(
backgroundColor: RetroColors.darkBrown,
foregroundColor: RetroColors.textLight,
titleTextStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
// 버튼 테마
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: RetroColors.buttonPrimary,
foregroundColor: RetroColors.textLight,
textStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: RetroColors.gold,
side: const BorderSide(color: RetroColors.gold, width: 2),
textStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: RetroColors.cream,
textStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
),
),
),
// 텍스트 테마
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 18,
color: RetroColors.gold,
),
headlineMedium: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.gold,
),
headlineSmall: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
titleLarge: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.textLight,
),
titleMedium: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: RetroColors.textLight,
),
titleSmall: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.textLight,
),
bodyLarge: TextStyle(fontSize: 14, color: RetroColors.textLight),
bodyMedium: TextStyle(fontSize: 12, color: RetroColors.textLight),
bodySmall: TextStyle(fontSize: 10, color: RetroColors.textLight),
labelLarge: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: RetroColors.textLight,
),
labelMedium: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.textLight,
),
labelSmall: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textLight,
),
),
// 칩 테마
chipTheme: ChipThemeData(
backgroundColor: RetroColors.panelBgLight,
labelStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.textLight,
),
side: const BorderSide(color: RetroColors.panelBorderInner),
),
// 리스트 타일 테마
listTileTheme: const ListTileThemeData(
textColor: RetroColors.textLight,
iconColor: RetroColors.gold,
),
// 프로그레스 인디케이터
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: RetroColors.gold,
linearTrackColor: RetroColors.panelBorderOuter,
),
);

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/l10n/app_localizations.dart';
import 'package:askiineverdie/src/features/front/widgets/hero_vs_boss_animation.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
import 'package:askiineverdie/src/shared/widgets/retro_widgets.dart';
class FrontScreen extends StatelessWidget {
const FrontScreen({
@@ -60,142 +62,120 @@ class FrontScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [colorScheme.surfaceContainerHighest, colorScheme.surface],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Column(
children: [
// 스크롤 영역 (헤더, 애니메이션, 버튼)
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 960),
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_HeroHeader(theme: theme, colorScheme: colorScheme),
const SizedBox(height: 20),
const HeroVsBossAnimation(),
const SizedBox(height: 24),
_ActionButtons(
onNewCharacter: onNewCharacter != null
? () => _handleNewCharacter(context)
: null,
onLoadSave: onLoadSave != null
? () => onLoadSave!(context)
: null,
onHallOfFame: onHallOfFame != null
? () => onHallOfFame!(context)
: null,
),
],
),
backgroundColor: RetroColors.deepBrown,
body: SafeArea(
child: Column(
children: [
// 스크롤 영역 (헤더, 애니메이션, 버튼)
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _RetroHeader(),
const SizedBox(height: 16),
const _AnimationPanel(),
const SizedBox(height: 16),
_ActionButtons(
onNewCharacter: onNewCharacter != null
? () => _handleNewCharacter(context)
: null,
onLoadSave: onLoadSave != null
? () => onLoadSave!(context)
: null,
onHallOfFame: onHallOfFame != null
? () => onHallOfFame!(context)
: null,
),
],
),
),
),
),
// 카피라이트 푸터 (하단 고정)
const _CopyrightFooter(),
],
),
),
// 카피라이트 푸터 (하단 고정)
const _CopyrightFooter(),
],
),
),
);
}
}
/// 헤더 (타이틀 + 태그) - 중앙 정렬
class _HeroHeader extends StatelessWidget {
const _HeroHeader({required this.theme, required this.colorScheme});
final ThemeData theme;
final ColorScheme colorScheme;
/// 레트로 스타일 헤더 (타이틀 + 태그)
class _RetroHeader extends StatelessWidget {
const _RetroHeader();
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
gradient: LinearGradient(
colors: [
colorScheme.primary.withValues(alpha: 0.9),
colorScheme.primaryContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.18),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 타이틀 (중앙 정렬)
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.auto_awesome, color: colorScheme.onPrimary),
const SizedBox(width: 10),
Text(
L10n.of(context).appTitle,
style: theme.textTheme.headlineSmall?.copyWith(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 14),
// 태그 (중앙 정렬)
Builder(
builder: (context) {
final l10n = L10n.of(context);
return Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
_Tag(
icon: Icons.cloud_off_outlined,
label: l10n.tagNoNetwork,
),
_Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
_Tag(
icon: Icons.storage_rounded,
label: l10n.tagLocalSaves,
final l10n = L10n.of(context);
return RetroGoldPanel(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: Column(
children: [
// 타이틀 (픽셀 폰트)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 20),
const SizedBox(width: 12),
Text(
l10n.appTitle,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.gold,
shadows: [
Shadow(
color: RetroColors.goldDark,
offset: Offset(2, 2),
),
],
);
},
),
],
),
),
),
],
),
const SizedBox(height: 16),
// 태그 (레트로 스타일)
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
_RetroTag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork),
_RetroTag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
_RetroTag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves),
],
),
],
),
);
}
}
/// 액션 버튼 (세로 배치)
/// 애니메이션 패널
class _AnimationPanel extends StatelessWidget {
const _AnimationPanel();
@override
Widget build(BuildContext context) {
return RetroPanel(
title: 'BATTLE',
padding: const EdgeInsets.all(8),
child: const AspectRatio(
aspectRatio: 16 / 9,
child: HeroVsBossAnimation(),
),
);
}
}
/// 액션 버튼 (레트로 스타일)
class _ActionButtons extends StatelessWidget {
const _ActionButtons({
this.onNewCharacter,
@@ -209,48 +189,39 @@ class _ActionButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = L10n.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 새 캐릭터 (Primary)
FilledButton.icon(
onPressed: onNewCharacter,
icon: const Icon(Icons.casino_outlined),
label: Text(l10n.newCharacter),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
textStyle: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
return RetroPanel(
title: 'MENU',
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 새 캐릭터 (Primary)
RetroTextButton(
text: l10n.newCharacter,
icon: Icons.casino_outlined,
onPressed: onNewCharacter,
),
const SizedBox(height: 12),
// 불러오기 (Secondary)
RetroTextButton(
text: l10n.loadSave,
icon: Icons.folder_open,
onPressed: onLoadSave,
isPrimary: false,
),
const SizedBox(height: 12),
// 명예의 전당
if (onHallOfFame != null)
RetroTextButton(
text: game_l10n.uiHallOfFame,
icon: Icons.emoji_events_outlined,
onPressed: onHallOfFame,
isPrimary: false,
),
),
),
const SizedBox(height: 12),
// 불러오기 (Secondary)
OutlinedButton.icon(
onPressed: onLoadSave,
icon: const Icon(Icons.folder_open),
label: Text(l10n.loadSave),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
textStyle: theme.textTheme.titleMedium,
),
),
const SizedBox(height: 12),
// 명예의 전당 (Tertiary)
if (onHallOfFame != null)
TextButton.icon(
onPressed: onHallOfFame,
icon: const Icon(Icons.emoji_events_outlined),
label: Text(game_l10n.uiHallOfFame),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
textStyle: theme.textTheme.titleMedium,
),
),
],
],
),
);
}
}
@@ -261,43 +232,51 @@ class _CopyrightFooter extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
game_l10n.copyrightText,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
),
),
);
}
}
/// 태그 칩
class _Tag extends StatelessWidget {
const _Tag({required this.icon, required this.label});
/// 레트로 태그 칩
class _RetroTag extends StatelessWidget {
const _RetroTag({required this.icon, required this.label});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
// 어두운 배경에 잘 보이도록 대비되는 색상 사용
final tagColor = colorScheme.onPrimaryContainer;
return Chip(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
backgroundColor: colorScheme.primaryContainer.withValues(alpha: 0.8),
avatar: Icon(icon, color: tagColor, size: 16),
label: Text(
label,
style: TextStyle(color: tagColor, fontWeight: FontWeight.w600),
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: RetroColors.panelBgLight,
border: Border.all(color: RetroColors.panelBorderInner, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: RetroColors.gold, size: 12),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textLight,
),
),
],
),
side: BorderSide.none,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
);
}
}

View File

@@ -36,6 +36,7 @@ import 'package:askiineverdie/src/features/game/widgets/statistics_dialog.dart';
import 'package:askiineverdie/src/features/game/widgets/help_dialog.dart';
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
import 'package:askiineverdie/src/core/audio/audio_service.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
///
@@ -829,7 +830,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
);
}
// 기존 데스크톱 레이아웃
// 기존 데스크톱 레이아웃 (레트로 스타일)
return NotificationOverlay(
key: localeKey,
notificationService: _notificationService,
@@ -846,8 +847,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
}
},
child: Scaffold(
backgroundColor: RetroColors.deepBrown,
appBar: AppBar(
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
backgroundColor: RetroColors.darkBrown,
title: Text(
L10n.of(context).progressQuestTitle(state.traits.name),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
actions: [
// 치트 버튼 (디버그용)
if (widget.controller.cheatsEnabled) ...[
@@ -955,6 +965,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final l10n = L10n.of(context);
return Card(
margin: const EdgeInsets.all(4),
color: RetroColors.panelBg,
shape: RoundedRectangleBorder(
side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2),
borderRadius: BorderRadius.circular(0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -1023,6 +1038,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final l10n = L10n.of(context);
return Card(
margin: const EdgeInsets.all(4),
color: RetroColors.panelBg,
shape: RoundedRectangleBorder(
side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2),
borderRadius: BorderRadius.circular(0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -1069,6 +1089,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final l10n = L10n.of(context);
return Card(
margin: const EdgeInsets.all(4),
color: RetroColors.panelBg,
shape: RoundedRectangleBorder(
side: const BorderSide(color: RetroColors.panelBorderOuter, width: 2),
borderRadius: BorderRadius.circular(0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -1112,13 +1137,20 @@ class _GamePlayScreenState extends State<GamePlayScreen>
Widget _buildPanelHeader(String title) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: const BoxDecoration(
color: RetroColors.darkBrown,
border: Border(
bottom: BorderSide(color: RetroColors.gold, width: 2),
),
),
child: Text(
title,
style: TextStyle(
title.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: RetroColors.gold,
),
),
);
@@ -1126,8 +1158,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Text(title, style: Theme.of(context).textTheme.labelSmall),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
title.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
),
),
);
}

View File

@@ -15,6 +15,7 @@ import 'package:askiineverdie/src/features/game/pages/story_page.dart';
import 'package:askiineverdie/src/features/game/widgets/carousel_nav_bar.dart';
import 'package:askiineverdie/src/features/game/widgets/combat_log.dart';
import 'package:askiineverdie/src/features/game/widgets/enhanced_animation_panel.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 모바일 캐로셀 레이아웃
///
@@ -358,6 +359,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: RetroColors.panelBg,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
@@ -366,19 +368,22 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 헤더
// 헤더 (레트로 스타일)
Container(
padding: const EdgeInsets.all(16),
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
decoration: const BoxDecoration(
color: RetroColors.darkBrown,
border: Border(
bottom: BorderSide(color: RetroColors.gold, width: 2),
),
),
child: Text(
l10n.menuOptions,
child: const Text(
'OPTIONS',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
),
@@ -555,12 +560,21 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
final state = widget.state;
return Scaffold(
backgroundColor: RetroColors.deepBrown,
appBar: AppBar(
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
backgroundColor: RetroColors.darkBrown,
title: Text(
L10n.of(context).progressQuestTitle(state.traits.name),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: RetroColors.gold,
),
),
actions: [
// 옵션 버튼
IconButton(
icon: const Icon(Icons.settings),
icon: const Icon(Icons.settings, color: RetroColors.gold),
onPressed: () => _showOptionsMenu(context),
tooltip: l10n.menuOptions,
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 캐로셀 페이지 인덱스
enum CarouselPage {
@@ -13,7 +14,7 @@ enum CarouselPage {
story, // 6: 스토리
}
/// 캐로셀 네비게이션 바
/// 캐로셀 네비게이션 바 (레트로 스타일)
///
/// 7개의 페이지 버튼을 표시하고 현재 페이지를 하이라이트.
/// 버튼 탭 시 해당 페이지로 이동.
@@ -31,10 +32,12 @@ class CarouselNavBar extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
decoration: const BoxDecoration(
color: RetroColors.darkBrown,
border: Border(
top: BorderSide(color: RetroColors.gold, width: 2),
),
),
child: Row(
children: CarouselPage.values.map((page) {
@@ -52,7 +55,7 @@ class CarouselNavBar extends StatelessWidget {
}
}
/// 개별 네비게이션 버튼
/// 개별 네비게이션 버튼 (레트로 스타일)
class _NavButton extends StatelessWidget {
const _NavButton({
required this.page,
@@ -67,33 +70,32 @@ class _NavButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final (icon, label) = _getIconAndLabel(page);
final theme = Theme.of(context);
final color = isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant;
final color = isSelected ? RetroColors.gold : RetroColors.textDisabled;
final bgColor = isSelected
? RetroColors.panelBgLight
: Colors.transparent;
return InkWell(
return GestureDetector(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1),
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: isSelected
? BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.circular(8),
)
: null,
decoration: BoxDecoration(
color: bgColor,
border: isSelected
? Border.all(color: RetroColors.gold, width: 1)
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 20, color: color),
Icon(icon, size: 18, color: color),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontFamily: 'PressStart2P',
fontSize: 5,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: color,
),

View 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,
);
}

View 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,
);
}

View 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),
);
}
}

View 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,
);
}
}

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,
);
}
}

View 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';

View File

@@ -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