Compare commits
4 Commits
8fd2f71a2f
...
2486d84d63
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2486d84d63 | ||
|
|
2677334346 | ||
|
|
708148c767 | ||
|
|
2d797502a3 |
@@ -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"
|
||||
|
||||
BIN
assets/fonts/PressStart2P-Regular.ttf
Normal file
BIN
assets/fonts/PressStart2P-Regular.ttf
Normal file
Binary file not shown.
@@ -45,5 +45,7 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2025 naturebridgeai 天安門 六四事件 法輪功 李洪志 Free Tibet</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
138
lib/src/app.dart
138
lib/src/app.dart
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user