- Progress Quest 6.4 Flutter 포팅 프로젝트 - 게임 루프, 상태 관리, UI 구현 - 캐릭터 생성, 인벤토리, 장비, 주문 시스템 - 시장/판매/구매 메커니즘
315 lines
9.7 KiB
Dart
315 lines
9.7 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
class FrontScreen extends StatelessWidget {
|
||
const FrontScreen({super.key, this.onNewCharacter, this.onLoadSave});
|
||
|
||
/// "New character" 버튼 클릭 시 호출
|
||
final void Function(BuildContext context)? onNewCharacter;
|
||
|
||
/// "Load save" 버튼 클릭 시 호출
|
||
final Future<void> Function(BuildContext context)? onLoadSave;
|
||
|
||
@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: 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: 24),
|
||
_ActionRow(
|
||
onNewCharacter: onNewCharacter != null
|
||
? () => onNewCharacter!(context)
|
||
: () => _showPlaceholder(context),
|
||
onLoadSave: onLoadSave != null
|
||
? () => onLoadSave!(context)
|
||
: () => _showPlaceholder(context),
|
||
),
|
||
const SizedBox(height: 24),
|
||
const _StatusCards(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _showPlaceholder(BuildContext context) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text(
|
||
'Core gameplay loop is coming next. See doc/progress-quest-flutter-plan.md for milestones.',
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
class _HeroHeader extends StatelessWidget {
|
||
const _HeroHeader({required this.theme, required this.colorScheme});
|
||
|
||
final ThemeData theme;
|
||
final ColorScheme colorScheme;
|
||
|
||
@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.start,
|
||
children: [
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(Icons.auto_awesome, color: colorScheme.onPrimary),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Ascii Never Die',
|
||
style: theme.textTheme.headlineSmall?.copyWith(
|
||
color: colorScheme.onPrimary,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
color: colorScheme.onPrimary.withValues(alpha: 0.9),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: const [
|
||
_Tag(icon: Icons.cloud_off_outlined, label: 'No network'),
|
||
_Tag(icon: Icons.timer_outlined, label: 'Idle RPG loop'),
|
||
_Tag(icon: Icons.storage_rounded, label: 'Local saves'),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ActionRow extends StatelessWidget {
|
||
const _ActionRow({required this.onNewCharacter, required this.onLoadSave});
|
||
|
||
final VoidCallback onNewCharacter;
|
||
final VoidCallback onLoadSave;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
return Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: [
|
||
FilledButton.icon(
|
||
onPressed: onNewCharacter,
|
||
icon: const Icon(Icons.casino_outlined),
|
||
label: const Text('New character'),
|
||
style: FilledButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||
textStyle: theme.textTheme.titleMedium,
|
||
),
|
||
),
|
||
OutlinedButton.icon(
|
||
onPressed: onLoadSave,
|
||
icon: const Icon(Icons.folder_open),
|
||
label: const Text('Load save'),
|
||
style: OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||
textStyle: theme.textTheme.titleMedium,
|
||
),
|
||
),
|
||
TextButton.icon(
|
||
onPressed: () => _showPlaceholder(context),
|
||
icon: const Icon(Icons.menu_book_outlined),
|
||
label: const Text('View build plan'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _StatusCards extends StatelessWidget {
|
||
const _StatusCards();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: const [
|
||
_InfoCard(
|
||
icon: Icons.route_outlined,
|
||
title: 'Build roadmap',
|
||
points: [
|
||
'Port PQ 6.4 data set (Config.dfm) into Dart constants.',
|
||
'Recreate quest/task loop with deterministic RNG + saves.',
|
||
'Deliver offline-first storage (GZip JSON) across platforms.',
|
||
],
|
||
),
|
||
SizedBox(height: 16),
|
||
_InfoCard(
|
||
icon: Icons.auto_fix_high_outlined,
|
||
title: 'Tech stack',
|
||
points: [
|
||
'Flutter (Material 3) with multiplatform targets enabled.',
|
||
'path_provider + shared_preferences for local storage hooks.',
|
||
'Strict lints with package imports enforced from day one.',
|
||
],
|
||
),
|
||
SizedBox(height: 16),
|
||
_InfoCard(
|
||
icon: Icons.checklist_rtl,
|
||
title: 'Today’s focus',
|
||
points: [
|
||
'Set up scaffold + lints.',
|
||
'Wire seed theme and initial navigation shell.',
|
||
'Keep reference assets under example/pq for parity.',
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _InfoCard extends StatelessWidget {
|
||
const _InfoCard({required this.title, required this.points, this.icon});
|
||
|
||
final String title;
|
||
final List<String> points;
|
||
final IconData? icon;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final colorScheme = theme.colorScheme;
|
||
|
||
return Card(
|
||
elevation: 3,
|
||
shadowColor: colorScheme.shadow.withValues(alpha: 0.2),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(18),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
if (icon != null) ...[
|
||
Icon(icon, color: colorScheme.primary),
|
||
const SizedBox(width: 10),
|
||
],
|
||
Text(
|
||
title,
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
...points.map(
|
||
(point) => Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Padding(
|
||
padding: EdgeInsets.only(top: 3),
|
||
child: Icon(Icons.check_circle_outline, size: 18),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(point, style: theme.textTheme.bodyMedium),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _Tag extends StatelessWidget {
|
||
const _Tag({required this.icon, required this.label});
|
||
|
||
final IconData icon;
|
||
final String label;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
return Chip(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
backgroundColor: colorScheme.onPrimary.withValues(alpha: 0.14),
|
||
avatar: Icon(icon, color: colorScheme.onPrimary, size: 16),
|
||
label: Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: colorScheme.onPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
side: BorderSide.none,
|
||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
visualDensity: VisualDensity.compact,
|
||
);
|
||
}
|
||
}
|