Compare commits

..

5 Commits

Author SHA1 Message Date
JiWoong Sul
06f76e1364 feat(ui): 캐릭터 생성 화면 레트로 UI 전면 개편
- RetroPanel, RetroButton 사용으로 통일
- 스탯 표시 레트로 스타일 적용
- 종족/직업 선택 UI 개선
- 전체 레이아웃 레트로 RPG 느낌으로 변경
2025-12-30 19:04:09 +09:00
JiWoong Sul
9e96b94465 feat(ui): 게임 플레이 화면 레트로 UI 및 로직 개선
- 레트로 색상/스타일 전체 적용
- 다이얼로그들 RetroDialog로 통일
- 설정 화면 레트로 테마 적용
2025-12-30 19:04:00 +09:00
JiWoong Sul
27e05fb3c1 feat(ui): 게임 위젯들 레트로 UI 적용
- death_overlay: 사망 화면 레트로 스타일로 재디자인
- help_dialog: RetroDialog 사용으로 통일
- hp_mp_bar: 레트로 프로그레스 바 스타일 적용
- notification_overlay: 레트로 패널 스타일 적용
- statistics_dialog: RetroDialog로 변경
2025-12-30 19:03:52 +09:00
JiWoong Sul
af837fde8a feat(theme): 앱 테마 레트로 스타일 확장
- 라이트/다크 테마 모두 레트로 색상 적용
- 다이얼로그, 바텀시트 테마 추가
- 슬라이더, 스위치 테마 커스터마이징
- 입력 필드 테마 추가
2025-12-30 19:03:43 +09:00
JiWoong Sul
4d9042451c feat(ui): 레트로 위젯 확장
- RetroDialog: 레트로 스타일 다이얼로그 위젯 추가
- RetroButton: 다양한 크기/스타일 옵션 추가
- retro_widgets.dart에 export 추가
2025-12-30 19:03:34 +09:00
11 changed files with 2008 additions and 969 deletions

View File

@@ -381,23 +381,73 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
} }
} }
/// 스플래시 화면 (세이브 파일 확인 중) /// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일
class _SplashScreen extends StatelessWidget { class _SplashScreen extends StatelessWidget {
const _SplashScreen(); const _SplashScreen();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return Scaffold(
backgroundColor: RetroColors.deepBrown,
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( // 타이틀 로고
'ASCII NEVER DIE', Container(
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: RetroColors.panelBg,
border: Border.all(color: RetroColors.gold, width: 3),
),
child: Column(
children: [
// 아이콘
const Icon(
Icons.auto_awesome,
size: 32,
color: RetroColors.gold,
),
const SizedBox(height: 12),
// 타이틀
const Text(
'ASCII',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 20,
color: RetroColors.gold,
shadows: [
Shadow(
color: RetroColors.goldDark,
offset: Offset(2, 2),
),
],
),
),
const SizedBox(height: 4),
const Text(
'NEVER DIE',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.cream,
shadows: [
Shadow(
color: RetroColors.brown,
offset: Offset(1, 1),
),
],
),
),
],
),
),
const SizedBox(height: 32),
// 레트로 로딩 바
SizedBox(
width: 160,
child: _RetroLoadingBar(),
), ),
SizedBox(height: 16),
CircularProgressIndicator(),
], ],
), ),
), ),
@@ -405,7 +455,7 @@ class _SplashScreen extends StatelessWidget {
} }
} }
/// 자동 로드 화면 (세이브 파일 자동 로드) /// 자동 로드 화면 (세이브 파일 자동 로드) - 레트로 스타일
class _AutoLoadScreen extends StatefulWidget { class _AutoLoadScreen extends StatefulWidget {
const _AutoLoadScreen({ const _AutoLoadScreen({
required this.controller, required this.controller,
@@ -460,21 +510,142 @@ class _AutoLoadScreenState extends State<_AutoLoadScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: RetroColors.deepBrown,
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text( // 타이틀 로고
'ASCII NEVER DIE', Container(
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: RetroColors.panelBg,
border: Border.all(color: RetroColors.gold, width: 3),
),
child: Column(
children: [
// 아이콘
const Icon(
Icons.auto_awesome,
size: 32,
color: RetroColors.gold,
),
const SizedBox(height: 12),
// 타이틀
const Text(
'ASCII',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 20,
color: RetroColors.gold,
shadows: [
Shadow(
color: RetroColors.goldDark,
offset: Offset(2, 2),
),
],
),
),
const SizedBox(height: 4),
const Text(
'NEVER DIE',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.cream,
shadows: [
Shadow(
color: RetroColors.brown,
offset: Offset(1, 1),
),
],
),
),
],
),
),
const SizedBox(height: 32),
// 레트로 로딩 바
SizedBox(
width: 160,
child: _RetroLoadingBar(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const CircularProgressIndicator(), // 로딩 텍스트
const SizedBox(height: 16), Text(
Text(game_l10n.uiLoading), game_l10n.uiLoading,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.textDisabled,
),
),
], ],
), ),
), ),
); );
} }
} }
/// 레트로 스타일 로딩 바 (애니메이션)
class _RetroLoadingBar extends StatefulWidget {
@override
State<_RetroLoadingBar> createState() => _RetroLoadingBarState();
}
class _RetroLoadingBarState extends State<_RetroLoadingBar>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const segmentCount = 10;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// 웨이브 효과: 각 세그먼트가 순차적으로 켜지고 꺼짐
return Container(
height: 16,
decoration: BoxDecoration(
color: RetroColors.panelBg,
border: Border.all(color: RetroColors.panelBorderOuter, width: 2),
),
child: Row(
children: List.generate(segmentCount, (index) {
// 웨이브 패턴 계산
final progress = _controller.value * segmentCount;
final distance = (index - progress).abs();
final isLit = distance < 2 || (segmentCount - distance) < 2;
final opacity = isLit ? 1.0 : 0.2;
return Expanded(
child: Container(
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: RetroColors.gold.withValues(alpha: opacity),
),
),
);
}),
),
);
},
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'
show kIsWeb, defaultTargetPlatform, TargetPlatform; show kIsWeb, defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase; import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
import 'package:flutter/services.dart' show KeyDownEvent, LogicalKeyboardKey;
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/data/skill_data.dart'; import 'package:askiineverdie/data/skill_data.dart';
@@ -846,7 +847,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
} }
} }
}, },
child: Scaffold( // 웹/데스크톱 키보드 단축키 지원
child: Focus(
autofocus: true,
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
child: Scaffold(
backgroundColor: RetroColors.deepBrown, backgroundColor: RetroColors.deepBrown,
appBar: AppBar( appBar: AppBar(
backgroundColor: RetroColors.darkBrown, backgroundColor: RetroColors.darkBrown,
@@ -956,10 +961,54 @@ class _GamePlayScreenState extends State<GamePlayScreen>
], ],
), ),
), ),
),
), ),
); );
} }
/// 키보드 단축키 핸들러 (웹/데스크톱)
/// Space: 일시정지/재개, S: 속도 변경, H: 도움말, Esc: 설정
KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) {
// KeyDown 이벤트만 처리
if (event is! KeyDownEvent) return KeyEventResult.ignored;
final key = event.logicalKey;
// Space: 일시정지/재개
if (key == LogicalKeyboardKey.space) {
widget.controller.togglePause();
setState(() {});
return KeyEventResult.handled;
}
// S: 속도 변경
if (key == LogicalKeyboardKey.keyS) {
widget.controller.loop?.cycleSpeed();
setState(() {});
return KeyEventResult.handled;
}
// H 또는 F1: 도움말
if (key == LogicalKeyboardKey.keyH || key == LogicalKeyboardKey.f1) {
HelpDialog.show(context);
return KeyEventResult.handled;
}
// Escape: 설정
if (key == LogicalKeyboardKey.escape) {
_showSettingsScreen(context);
return KeyEventResult.handled;
}
// I: 통계
if (key == LogicalKeyboardKey.keyI) {
_showStatisticsDialog(context);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
/// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells) /// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells)
Widget _buildCharacterPanel(GameState state) { Widget _buildCharacterPanel(GameState state) {
final l10n = L10n.of(context); final l10n = L10n.of(context);
@@ -1170,6 +1219,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
); );
} }
/// 레트로 스타일 세그먼트 프로그레스 바
Widget _buildProgressBar( Widget _buildProgressBar(
int position, int position,
int max, int max,
@@ -1177,13 +1227,37 @@ class _GamePlayScreenState extends State<GamePlayScreen>
String? tooltip, String? tooltip,
}) { }) {
final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0; final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0;
const segmentCount = 20;
final filledSegments = (progress * segmentCount).round();
final bar = Padding( final bar = Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: LinearProgressIndicator( child: Container(
value: progress, height: 12,
backgroundColor: color.withValues(alpha: 0.2), decoration: BoxDecoration(
valueColor: AlwaysStoppedAnimation<Color>(color), color: color.withValues(alpha: 0.15),
minHeight: 12, border: Border.all(color: RetroColors.panelBorderOuter, width: 1),
),
child: Row(
children: List.generate(segmentCount, (index) {
final isFilled = index < filledSegments;
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isFilled ? color : color.withValues(alpha: 0.1),
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.panelBorderOuter.withValues(alpha: 0.3),
width: 1,
)
: BorderSide.none,
),
),
),
);
}),
),
), ),
); );
@@ -1203,26 +1277,37 @@ class _GamePlayScreenState extends State<GamePlayScreen>
]; ];
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Column( child: Column(
children: traits.map((t) { children: traits.map((t) {
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 1),
SizedBox( child: Row(
width: 50, children: [
child: Text(t.$1, style: const TextStyle(fontSize: 11)), SizedBox(
), width: 50,
Expanded( child: Text(
child: Text( t.$1.toUpperCase(),
t.$2, style: const TextStyle(
style: const TextStyle( fontFamily: 'PressStart2P',
fontSize: 11, fontSize: 6,
fontWeight: FontWeight.bold, color: RetroColors.textDisabled,
),
), ),
overflow: TextOverflow.ellipsis,
), ),
), Expanded(
], child: Text(
t.$2,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textLight,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
); );
}).toList(), }).toList(),
), ),
@@ -1237,7 +1322,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return Center( return Center(
child: Text( child: Text(
L10n.of(context).noSpellsYet, L10n.of(context).noSpellsYet,
style: const TextStyle(fontSize: 11), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
), ),
); );
} }
@@ -1274,7 +1363,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return Center( return Center(
child: Text( child: Text(
l10n.goldAmount(state.inventory.gold), l10n.goldAmount(state.inventory.gold),
style: const TextStyle(fontSize: 11), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.gold,
),
), ),
); );
} }
@@ -1284,19 +1377,32 @@ class _GamePlayScreenState extends State<GamePlayScreen>
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 2),
Expanded( child: Row(
child: Text(l10n.gold, style: const TextStyle(fontSize: 11)), children: [
), const Icon(Icons.monetization_on, size: 10, color: RetroColors.gold),
Text( const SizedBox(width: 4),
'${state.inventory.gold}', Expanded(
style: const TextStyle( child: Text(
fontSize: 11, l10n.gold.toUpperCase(),
fontWeight: FontWeight.bold, style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.gold,
),
),
), ),
), Text(
], '${state.inventory.gold}',
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.gold,
),
),
],
),
); );
} }
final item = state.inventory.items[index - 1]; final item = state.inventory.items[index - 1];
@@ -1305,20 +1411,31 @@ class _GamePlayScreenState extends State<GamePlayScreen>
context, context,
item.name, item.name,
); );
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 1),
Expanded( child: Row(
child: Text( children: [
translatedName, Expanded(
style: const TextStyle(fontSize: 11), child: Text(
overflow: TextOverflow.ellipsis, translatedName,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textLight,
),
overflow: TextOverflow.ellipsis,
),
), ),
), Text(
Text( '${item.count}',
'${item.count}', style: const TextStyle(
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), fontFamily: 'PressStart2P',
), fontSize: 6,
], color: RetroColors.cream,
),
),
],
),
); );
}, },
); );
@@ -1330,7 +1447,14 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final plotCount = state.progress.plotStageCount; final plotCount = state.progress.plotStageCount;
if (plotCount == 0) { if (plotCount == 0) {
return Center( return Center(
child: Text(l10n.prologue, style: const TextStyle(fontSize: 11)), child: Text(
l10n.prologue.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
),
); );
} }
@@ -1339,24 +1463,38 @@ class _GamePlayScreenState extends State<GamePlayScreen>
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isCompleted = index < plotCount - 1; final isCompleted = index < plotCount - 1;
return Row( final isCurrent = index == plotCount - 1;
children: [ return Padding(
Icon( padding: const EdgeInsets.symmetric(vertical: 1),
isCompleted ? Icons.check_box : Icons.check_box_outline_blank, child: Row(
size: 14, children: [
color: isCompleted ? Colors.green : Colors.grey, Icon(
), isCompleted
const SizedBox(width: 4), ? Icons.check_box
Text( : (isCurrent ? Icons.arrow_right : Icons.check_box_outline_blank),
index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)), size: 12,
style: TextStyle( color: isCompleted
fontSize: 11, ? RetroColors.expGreen
decoration: isCompleted : (isCurrent ? RetroColors.gold : RetroColors.textDisabled),
? TextDecoration.lineThrough
: TextDecoration.none,
), ),
), const SizedBox(width: 4),
], Expanded(
child: Text(
index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: isCompleted
? RetroColors.textDisabled
: (isCurrent ? RetroColors.gold : RetroColors.textLight),
decoration: isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
),
],
),
); );
}, },
); );
@@ -1368,7 +1506,14 @@ class _GamePlayScreenState extends State<GamePlayScreen>
if (questHistory.isEmpty) { if (questHistory.isEmpty) {
return Center( return Center(
child: Text(l10n.noActiveQuests, style: const TextStyle(fontSize: 11)), child: Text(
l10n.noActiveQuests.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
),
); );
} }
@@ -1382,32 +1527,44 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final isCurrentQuest = final isCurrentQuest =
index == questHistory.length - 1 && !quest.isComplete; index == questHistory.length - 1 && !quest.isComplete;
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 1),
if (isCurrentQuest) child: Row(
const Icon(Icons.arrow_right, size: 14) children: [
else
Icon( Icon(
quest.isComplete isCurrentQuest
? Icons.check_box ? Icons.arrow_right
: Icons.check_box_outline_blank, : (quest.isComplete
size: 14, ? Icons.check_box
color: quest.isComplete ? Colors.green : Colors.grey, : Icons.check_box_outline_blank),
size: 12,
color: isCurrentQuest
? RetroColors.gold
: (quest.isComplete
? RetroColors.expGreen
: RetroColors.textDisabled),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
quest.caption, quest.caption,
style: TextStyle( style: TextStyle(
fontSize: 11, fontFamily: 'PressStart2P',
decoration: quest.isComplete fontSize: 6,
? TextDecoration.lineThrough color: isCurrentQuest
: TextDecoration.none, ? RetroColors.gold
: (quest.isComplete
? RetroColors.textDisabled
: RetroColors.textLight),
decoration: quest.isComplete
? TextDecoration.lineThrough
: TextDecoration.none,
),
overflow: TextOverflow.ellipsis,
), ),
overflow: TextOverflow.ellipsis,
), ),
), ],
], ),
); );
}, },
); );

View File

@@ -4,6 +4,7 @@ import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:askiineverdie/src/core/model/combat_event.dart'; import 'package:askiineverdie/src/core/model/combat_event.dart';
import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 사망 오버레이 위젯 (Phase 4) /// 사망 오버레이 위젯 (Phase 4)
/// ///
@@ -27,96 +28,161 @@ class DeathOverlay extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Material( return Material(
color: Colors.black87, color: Colors.black.withValues(alpha: 0.9),
child: Center( child: Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 420),
margin: const EdgeInsets.all(24), margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.surface, color: RetroColors.panelBg,
borderRadius: BorderRadius.circular(16), border: const Border(
border: Border.all( top: BorderSide(color: RetroColors.hpRed, width: 4),
color: colorScheme.error.withValues(alpha: 0.5), left: BorderSide(color: RetroColors.hpRed, width: 4),
width: 2, bottom: BorderSide(color: RetroColors.panelBorderOuter, width: 4),
right: BorderSide(color: RetroColors.panelBorderOuter, width: 4),
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: colorScheme.error.withValues(alpha: 0.3), color: RetroColors.hpRed.withValues(alpha: 0.5),
blurRadius: 20, blurRadius: 30,
spreadRadius: 5, spreadRadius: 5,
), ),
], ],
), ),
child: SingleChildScrollView( child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ // 헤더 바
// 사망 타이틀 Container(
_buildDeathTitle(context), width: double.infinity,
const SizedBox(height: 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: RetroColors.hpRed.withValues(alpha: 0.3),
border: const Border(
bottom: BorderSide(color: RetroColors.hpRed, width: 2),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'',
style: TextStyle(fontSize: 16, color: RetroColors.hpRed),
),
SizedBox(width: 8),
Text(
'GAME OVER',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.hpRed,
letterSpacing: 2,
),
),
SizedBox(width: 8),
Text(
'',
style: TextStyle(fontSize: 16, color: RetroColors.hpRed),
),
],
),
),
// 본문
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 사망 타이틀
_buildDeathTitle(context),
const SizedBox(height: 16),
// 캐릭터 정보 // 캐릭터 정보
_buildCharacterInfo(context), _buildCharacterInfo(context),
const SizedBox(height: 16), const SizedBox(height: 16),
// 사망 원인 // 사망 원인
_buildDeathCause(context), _buildDeathCause(context),
const SizedBox(height: 24), const SizedBox(height: 20),
// 구분선 // 구분선
Divider(color: colorScheme.outlineVariant), _buildRetroDivider(),
const SizedBox(height: 16), const SizedBox(height: 16),
// 상실 정보 // 상실 정보
_buildLossInfo(context), _buildLossInfo(context),
// 전투 로그 (있는 경우만 표시) // 전투 로그 (있는 경우만 표시)
if (deathInfo.lastCombatEvents.isNotEmpty) ...[ if (deathInfo.lastCombatEvents.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Divider(color: colorScheme.outlineVariant), _buildRetroDivider(),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildCombatLog(context), _buildCombatLog(context),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
// 부활 버튼 // 부활 버튼
_buildResurrectButton(context), _buildResurrectButton(context),
], ],
), ),
),
],
), ),
), ),
), ),
); );
} }
/// 레트로 스타일 구분선
Widget _buildRetroDivider() {
return Container(
height: 2,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
RetroColors.hpRedDark,
RetroColors.hpRed,
RetroColors.hpRedDark,
Colors.transparent,
],
),
),
);
}
Widget _buildDeathTitle(BuildContext context) { Widget _buildDeathTitle(BuildContext context) {
return Column( return Column(
children: [ children: [
// ASCII 스컬 // ASCII 스컬 (더 큰 버전)
Text( const Text(
' _____\n / \\\n| () () |\n \\ ^ /\n |||||', ' _____ \n'
' / \\\n'
' | () () |\n'
' \\ ^ /\n'
' ||||| ',
style: TextStyle( style: TextStyle(
fontFamily: 'JetBrainsMono', fontFamily: 'JetBrainsMono',
fontSize: 14, fontSize: 12,
color: Theme.of(context).colorScheme.error, color: RetroColors.hpRed,
height: 1.0, height: 1.0,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
Text( Text(
l10n.deathYouDied, l10n.deathYouDied.toUpperCase(),
style: TextStyle( style: const TextStyle(
fontSize: 32, fontFamily: 'PressStart2P',
fontWeight: FontWeight.bold, fontSize: 14,
color: Theme.of(context).colorScheme.error, color: RetroColors.hpRed,
letterSpacing: 4, letterSpacing: 2,
shadows: [
Shadow(color: Colors.black, blurRadius: 4),
Shadow(color: RetroColors.hpRedDark, blurRadius: 8),
],
), ),
), ),
], ],
@@ -124,49 +190,62 @@ class DeathOverlay extends StatelessWidget {
} }
Widget _buildCharacterInfo(BuildContext context) { Widget _buildCharacterInfo(BuildContext context) {
final theme = Theme.of(context); return Container(
return Column( padding: const EdgeInsets.all(12),
children: [ decoration: BoxDecoration(
Text( color: RetroColors.panelBgLight.withValues(alpha: 0.5),
traits.name, border: Border.all(color: RetroColors.panelBorderInner, width: 1),
style: theme.textTheme.titleLarge?.copyWith( ),
fontWeight: FontWeight.bold, child: Column(
children: [
Text(
traits.name,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.gold,
),
), ),
), const SizedBox(height: 6),
const SizedBox(height: 4), Text(
Text( 'Lv.${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}',
'Level ${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}', style: const TextStyle(
style: theme.textTheme.bodyLarge?.copyWith( fontFamily: 'PressStart2P',
color: theme.colorScheme.onSurfaceVariant, fontSize: 8,
color: RetroColors.textLight,
),
), ),
), ],
], ),
); );
} }
Widget _buildDeathCause(BuildContext context) { Widget _buildDeathCause(BuildContext context) {
final theme = Theme.of(context);
final causeText = _getDeathCauseText(); final causeText = _getDeathCauseText();
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.errorContainer.withValues(alpha: 0.3), color: RetroColors.hpRedDark.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8), border: Border.all(color: RetroColors.hpRed.withValues(alpha: 0.5)),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( const Text(
Icons.dangerous_outlined, '',
size: 20, style: TextStyle(fontSize: 14, color: RetroColors.hpRed),
color: theme.colorScheme.error,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
causeText, child: Text(
style: theme.textTheme.bodyMedium?.copyWith( causeText,
color: theme.colorScheme.error, style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.hpRed,
),
textAlign: TextAlign.center,
), ),
), ),
], ],
@@ -183,7 +262,6 @@ class DeathOverlay extends StatelessWidget {
} }
Widget _buildLossInfo(BuildContext context) { Widget _buildLossInfo(BuildContext context) {
final theme = Theme.of(context);
final hasLostItem = deathInfo.lostItemName != null; final hasLostItem = deathInfo.lostItemName != null;
return Column( return Column(
@@ -191,20 +269,18 @@ class DeathOverlay extends StatelessWidget {
// 제물로 바친 아이템 표시 // 제물로 바친 아이템 표시
if (hasLostItem) ...[ if (hasLostItem) ...[
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.errorContainer.withValues(alpha: 0.2), color: RetroColors.hpRedDark.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: theme.colorScheme.error.withValues(alpha: 0.3), color: RetroColors.hpRed.withValues(alpha: 0.4),
), ),
), ),
child: Row( child: Row(
children: [ children: [
Icon( const Text(
Icons.local_fire_department, '🔥',
size: 20, style: TextStyle(fontSize: 16),
color: theme.colorScheme.error,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
@@ -212,17 +288,20 @@ class DeathOverlay extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
l10n.deathSacrificedToResurrect, l10n.deathSacrificedToResurrect.toUpperCase(),
style: theme.textTheme.labelSmall?.copyWith( style: const TextStyle(
color: theme.colorScheme.onSurfaceVariant, fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 4),
Text( Text(
deathInfo.lostItemName!, deathInfo.lostItemName!,
style: theme.textTheme.bodyMedium?.copyWith( style: const TextStyle(
fontWeight: FontWeight.bold, fontFamily: 'PressStart2P',
color: theme.colorScheme.error, fontSize: 7,
color: RetroColors.hpRed,
), ),
), ),
], ],
@@ -235,19 +314,19 @@ class DeathOverlay extends StatelessWidget {
] else ...[ ] else ...[
_buildInfoRow( _buildInfoRow(
context, context,
icon: Icons.check_circle_outline, asciiIcon: '',
label: l10n.deathEquipment, label: l10n.deathEquipment,
value: l10n.deathNoSacrificeNeeded, value: l10n.deathNoSacrificeNeeded,
isNegative: false, valueColor: RetroColors.expGreen,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
_buildInfoRow( _buildInfoRow(
context, context,
icon: Icons.monetization_on_outlined, asciiIcon: '💰',
label: l10n.deathCoinRemaining, label: l10n.deathCoinRemaining,
value: _formatGold(deathInfo.goldAtDeath), value: _formatGold(deathInfo.goldAtDeath),
isNegative: false, valueColor: RetroColors.gold,
), ),
], ],
); );
@@ -255,35 +334,36 @@ class DeathOverlay extends StatelessWidget {
Widget _buildInfoRow( Widget _buildInfoRow(
BuildContext context, { BuildContext context, {
required IconData icon, required String asciiIcon,
required String label, required String label,
required String value, required String value,
required bool isNegative, required Color valueColor,
}) { }) {
final theme = Theme.of(context);
final valueColor = isNegative
? theme.colorScheme.error
: theme.colorScheme.primary;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
Icon(icon, size: 18, color: theme.colorScheme.onSurfaceVariant), Text(
asciiIcon,
style: TextStyle(fontSize: 14, color: valueColor),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
label, label,
style: theme.textTheme.bodyMedium?.copyWith( style: const TextStyle(
color: theme.colorScheme.onSurfaceVariant, fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
), ),
), ),
], ],
), ),
Text( Text(
value, value,
style: theme.textTheme.bodyMedium?.copyWith( style: TextStyle(
fontWeight: FontWeight.bold, fontFamily: 'PressStart2P',
fontSize: 7,
color: valueColor, color: valueColor,
), ),
), ),
@@ -301,17 +381,48 @@ class DeathOverlay extends StatelessWidget {
} }
Widget _buildResurrectButton(BuildContext context) { Widget _buildResurrectButton(BuildContext context) {
final theme = Theme.of(context); return GestureDetector(
onTap: onResurrect,
return SizedBox( child: Container(
width: double.infinity, width: double.infinity,
child: FilledButton.icon( padding: const EdgeInsets.symmetric(vertical: 12),
onPressed: onResurrect, decoration: BoxDecoration(
icon: const Icon(Icons.replay), color: RetroColors.expGreen.withValues(alpha: 0.2),
label: Text(l10n.deathResurrect), border: Border(
style: FilledButton.styleFrom( top: const BorderSide(color: RetroColors.expGreen, width: 3),
backgroundColor: theme.colorScheme.primary, left: const BorderSide(color: RetroColors.expGreen, width: 3),
padding: const EdgeInsets.symmetric(vertical: 16), bottom: BorderSide(
color: RetroColors.expGreenDark.withValues(alpha: 0.8),
width: 3,
),
right: BorderSide(
color: RetroColors.expGreenDark.withValues(alpha: 0.8),
width: 3,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'',
style: TextStyle(
fontSize: 16,
color: RetroColors.expGreen,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
l10n.deathResurrect.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: RetroColors.expGreen,
letterSpacing: 1,
),
),
],
), ),
), ),
); );
@@ -319,29 +430,38 @@ class DeathOverlay extends StatelessWidget {
/// 사망 직전 전투 로그 표시 /// 사망 직전 전투 로그 표시
Widget _buildCombatLog(BuildContext context) { Widget _buildCombatLog(BuildContext context) {
final theme = Theme.of(context);
final events = deathInfo.lastCombatEvents; final events = deathInfo.lastCombatEvents;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
l10n.deathCombatLog, children: [
style: theme.textTheme.labelMedium?.copyWith( const Text(
color: theme.colorScheme.onSurfaceVariant, '📜',
fontWeight: FontWeight.bold, style: TextStyle(fontSize: 12),
), ),
const SizedBox(width: 6),
Text(
l10n.deathCombatLog.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.gold,
),
),
],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
constraints: const BoxConstraints(maxHeight: 120), constraints: const BoxConstraints(maxHeight: 100),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest, color: RetroColors.deepBrown,
borderRadius: BorderRadius.circular(8), border: Border.all(color: RetroColors.panelBorderOuter, width: 2),
), ),
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(6),
itemCount: events.length, itemCount: events.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final event = events[index]; final event = events[index];
@@ -355,22 +475,26 @@ class DeathOverlay extends StatelessWidget {
/// 개별 전투 이벤트 타일 /// 개별 전투 이벤트 타일
Widget _buildCombatEventTile(BuildContext context, CombatEvent event) { Widget _buildCombatEventTile(BuildContext context, CombatEvent event) {
final (icon, color, message) = _formatCombatEvent(event); final (asciiIcon, color, message) = _formatCombatEvent(event);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 2), padding: const EdgeInsets.symmetric(vertical: 1),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 12, color: color), Text(
const SizedBox(width: 6), asciiIcon,
style: TextStyle(fontSize: 10, color: color),
),
const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
message, message,
style: TextStyle( style: TextStyle(
fontSize: 11,
color: color,
fontFamily: 'JetBrainsMono', fontFamily: 'JetBrainsMono',
fontSize: 8,
color: color,
), ),
overflow: TextOverflow.ellipsis,
), ),
), ),
], ],
@@ -378,75 +502,75 @@ class DeathOverlay extends StatelessWidget {
); );
} }
/// 전투 이벤트를 아이콘, 색상, 메시지로 포맷 /// 전투 이벤트를 ASCII 아이콘, 색상, 메시지로 포맷
(IconData, Color, String) _formatCombatEvent(CombatEvent event) { (String, Color, String) _formatCombatEvent(CombatEvent event) {
final target = event.targetName ?? ''; final target = event.targetName ?? '';
return switch (event.type) { return switch (event.type) {
CombatEventType.playerAttack => ( CombatEventType.playerAttack => (
event.isCritical ? Icons.flash_on : Icons.local_fire_department, event.isCritical ? '' : '',
event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300, event.isCritical ? RetroColors.gold : RetroColors.expGreen,
event.isCritical event.isCritical
? l10n.combatCritical(event.damage, target) ? l10n.combatCritical(event.damage, target)
: l10n.combatYouHit(target, event.damage), : l10n.combatYouHit(target, event.damage),
), ),
CombatEventType.monsterAttack => ( CombatEventType.monsterAttack => (
Icons.dangerous, '💀',
Colors.red.shade300, RetroColors.hpRed,
l10n.combatMonsterHitsYou(target, event.damage), l10n.combatMonsterHitsYou(target, event.damage),
), ),
CombatEventType.playerEvade => ( CombatEventType.playerEvade => (
Icons.directions_run, '',
Colors.cyan.shade300, RetroColors.asciiCyan,
l10n.combatEvadedAttackFrom(target), l10n.combatEvadedAttackFrom(target),
), ),
CombatEventType.monsterEvade => ( CombatEventType.monsterEvade => (
Icons.directions_run, '',
Colors.orange.shade300, const Color(0xFFFF9933),
l10n.combatMonsterEvaded(target), l10n.combatMonsterEvaded(target),
), ),
CombatEventType.playerBlock => ( CombatEventType.playerBlock => (
Icons.shield, '🛡',
Colors.blueGrey.shade300, RetroColors.mpBlue,
l10n.combatBlockedAttack(target, event.damage), l10n.combatBlockedAttack(target, event.damage),
), ),
CombatEventType.playerParry => ( CombatEventType.playerParry => (
Icons.sports_kabaddi, '',
Colors.teal.shade300, const Color(0xFF00CCCC),
l10n.combatParriedAttack(target, event.damage), l10n.combatParriedAttack(target, event.damage),
), ),
CombatEventType.playerSkill => ( CombatEventType.playerSkill => (
Icons.auto_fix_high, '',
Colors.purple.shade300, const Color(0xFF9966FF),
l10n.combatSkillDamage(event.skillName ?? '', event.damage), l10n.combatSkillDamage(event.skillName ?? '', event.damage),
), ),
CombatEventType.playerHeal => ( CombatEventType.playerHeal => (
Icons.healing, '',
Colors.green.shade300, RetroColors.expGreen,
l10n.combatHealedFor(event.healAmount), l10n.combatHealedFor(event.healAmount),
), ),
CombatEventType.playerBuff => ( CombatEventType.playerBuff => (
Icons.trending_up, '',
Colors.lightBlue.shade300, RetroColors.mpBlue,
l10n.combatBuffActivated(event.skillName ?? ''), l10n.combatBuffActivated(event.skillName ?? ''),
), ),
CombatEventType.playerDebuff => ( CombatEventType.playerDebuff => (
Icons.trending_down, '',
Colors.deepOrange.shade300, const Color(0xFFFF6633),
l10n.combatDebuffApplied(event.skillName ?? '', target), l10n.combatDebuffApplied(event.skillName ?? '', target),
), ),
CombatEventType.dotTick => ( CombatEventType.dotTick => (
Icons.whatshot, '🔥',
Colors.deepOrange.shade300, const Color(0xFFFF6633),
l10n.combatDotTick(event.skillName ?? '', event.damage), l10n.combatDotTick(event.skillName ?? '', event.damage),
), ),
CombatEventType.playerPotion => ( CombatEventType.playerPotion => (
Icons.local_drink, '🧪',
Colors.lightGreen.shade300, RetroColors.expGreen,
l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target), l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target),
), ),
CombatEventType.potionDrop => ( CombatEventType.potionDrop => (
Icons.card_giftcard, '🎁',
Colors.lime.shade300, RetroColors.gold,
l10n.combatPotionDrop(event.skillName ?? ''), l10n.combatPotionDrop(event.skillName ?? ''),
), ),
}; };

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
import 'package:askiineverdie/src/shared/widgets/retro_dialog.dart';
/// 도움말 다이얼로그 (Help Dialog) /// 도움말 다이얼로그 (Help Dialog)
/// ///
/// 게임 메카닉과 UI 설명을 제공 /// 게임 메카닉과 UI 설명을 제공
@@ -10,6 +13,7 @@ class HelpDialog extends StatefulWidget {
static Future<void> show(BuildContext context) { static Future<void> show(BuildContext context) {
return showDialog( return showDialog(
context: context, context: context,
barrierColor: Colors.black87,
builder: (_) => const HelpDialog(), builder: (_) => const HelpDialog(),
); );
} }
@@ -36,113 +40,58 @@ class _HelpDialogState extends State<HelpDialog>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final isKorean = Localizations.localeOf(context).languageCode == 'ko'; final isKorean = Localizations.localeOf(context).languageCode == 'ko';
final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
return Dialog( final title = isKorean
child: Container( ? '도움말'
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 600), : isJapanese
child: Column( ? 'ヘルプ'
mainAxisSize: MainAxisSize.min, : 'Help';
children: [
// 헤더 final tabs = isKorean
Container( ? ['기본', '전투', '스킬', 'UI']
padding: const EdgeInsets.all(16), : isJapanese
decoration: BoxDecoration( ? ['基本', '戦闘', 'スキル', 'UI']
color: theme.colorScheme.primaryContainer, : ['Basics', 'Combat', 'Skills', 'UI'];
borderRadius: const BorderRadius.vertical(
top: Radius.circular(28), return RetroDialog(
), title: title,
), titleIcon: '',
child: Row( accentColor: RetroColors.mpBlue,
children: [ child: Column(
Icon( children: [
Icons.help_outline, // 탭 바
color: theme.colorScheme.onPrimaryContainer, RetroTabBar(
), controller: _tabController,
const SizedBox(width: 12), tabs: tabs,
Expanded( accentColor: RetroColors.mpBlue,
child: Text( ),
isKorean // 탭 내용
? '게임 도움말' Expanded(
: isJapanese child: TabBarView(
? 'ゲームヘルプ'
: 'Game Help',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onPrimaryContainer,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
color: theme.colorScheme.onPrimaryContainer,
),
],
),
),
// 탭 바
TabBar(
controller: _tabController, controller: _tabController,
isScrollable: true, children: [
tabs: [ _BasicsHelpView(
Tab( isKorean: isKorean,
text: isKorean isJapanese: isJapanese,
? '기본'
: isJapanese
? '基本'
: 'Basics',
), ),
Tab( _CombatHelpView(
text: isKorean isKorean: isKorean,
? '전투' isJapanese: isJapanese,
: isJapanese
? '戦闘'
: 'Combat',
), ),
Tab( _SkillsHelpView(
text: isKorean isKorean: isKorean,
? '스킬' isJapanese: isJapanese,
: isJapanese
? 'スキル'
: 'Skills',
), ),
Tab( _UIHelpView(
text: isKorean isKorean: isKorean,
? 'UI' isJapanese: isJapanese,
: isJapanese
? 'UI'
: 'UI',
), ),
], ],
), ),
// 탭 내용 ),
Expanded( ],
child: TabBarView(
controller: _tabController,
children: [
_BasicsHelpView(
isKorean: isKorean,
isJapanese: isJapanese,
),
_CombatHelpView(
isKorean: isKorean,
isJapanese: isJapanese,
),
_SkillsHelpView(
isKorean: isKorean,
isJapanese: isJapanese,
),
_UIHelpView(
isKorean: isKorean,
isJapanese: isJapanese,
),
],
),
),
],
),
), ),
); );
} }
@@ -161,10 +110,10 @@ class _BasicsHelpView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
children: [ children: [
_HelpSection( _HelpSection(
icon: Icons.info_outline, icon: '',
title: isKorean title: isKorean
? '게임 소개' ? '게임 소개'
: isJapanese : isJapanese
@@ -179,9 +128,9 @@ class _BasicsHelpView extends StatelessWidget {
: 'Askii Never Die is an idle RPG. Your character automatically fights monsters, ' : 'Askii Never Die is an idle RPG. Your character automatically fights monsters, '
'completes quests, and levels up. You manage equipment and skills.', 'completes quests, and levels up. You manage equipment and skills.',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.trending_up, icon: '',
title: isKorean title: isKorean
? '진행 방식' ? '진행 방식'
: isJapanese : isJapanese
@@ -202,9 +151,9 @@ class _BasicsHelpView extends StatelessWidget {
'• Complete quests → Get rewards\n' '• Complete quests → Get rewards\n'
'• Progress plot → Unlock new Acts', '• Progress plot → Unlock new Acts',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.save, icon: '💾',
title: isKorean title: isKorean
? '저장' ? '저장'
: isJapanese : isJapanese
@@ -237,10 +186,10 @@ class _CombatHelpView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
children: [ children: [
_HelpSection( _HelpSection(
icon: Icons.sports_mma, icon: '',
title: isKorean title: isKorean
? '전투 시스템' ? '전투 시스템'
: isJapanese : isJapanese
@@ -255,9 +204,9 @@ class _CombatHelpView extends StatelessWidget {
: 'Combat is automatic. Player and monster take turns attacking, ' : 'Combat is automatic. Player and monster take turns attacking, '
'with attack frequency based on Attack Speed.', 'with attack frequency based on Attack Speed.',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.shield, icon: '🛡',
title: isKorean title: isKorean
? '방어 메카닉' ? '방어 메카닉'
: isJapanese : isJapanese
@@ -278,9 +227,9 @@ class _CombatHelpView extends StatelessWidget {
'• Parry: Deflect some damage with weapon\n' '• Parry: Deflect some damage with weapon\n'
'• DEF: Subtracted from all damage', '• DEF: Subtracted from all damage',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.favorite, icon: '',
title: isKorean title: isKorean
? '사망과 부활' ? '사망과 부활'
: isJapanese : isJapanese
@@ -313,10 +262,10 @@ class _SkillsHelpView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
children: [ children: [
_HelpSection( _HelpSection(
icon: Icons.auto_awesome, icon: '',
title: isKorean title: isKorean
? '스킬 종류' ? '스킬 종류'
: isJapanese : isJapanese
@@ -340,9 +289,9 @@ class _SkillsHelpView extends StatelessWidget {
'• Debuff: Harmful effects on enemies\n' '• Debuff: Harmful effects on enemies\n'
'• DOT: Damage over time', '• DOT: Damage over time',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.psychology, icon: '🤖',
title: isKorean title: isKorean
? '자동 스킬 선택' ? '자동 스킬 선택'
: isJapanese : isJapanese
@@ -366,9 +315,9 @@ class _SkillsHelpView extends StatelessWidget {
'3. Monster HP high → Apply debuffs\n' '3. Monster HP high → Apply debuffs\n'
'4. Finish with attack skills', '4. Finish with attack skills',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.upgrade, icon: '',
title: isKorean title: isKorean
? '스킬 랭크' ? '스킬 랭크'
: isJapanese : isJapanese
@@ -410,10 +359,10 @@ class _UIHelpView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
children: [ children: [
_HelpSection( _HelpSection(
icon: Icons.view_column, icon: '📺',
title: isKorean title: isKorean
? '화면 구성' ? '화면 구성'
: isJapanese : isJapanese
@@ -434,9 +383,9 @@ class _UIHelpView extends StatelessWidget {
'• Center: Equipment, inventory\n' '• Center: Equipment, inventory\n'
'• Right: Plot/quest progress, spellbook', '• Right: Plot/quest progress, spellbook',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.speed, icon: '',
title: isKorean title: isKorean
? '속도 조절' ? '속도 조절'
: isJapanese : isJapanese
@@ -460,9 +409,9 @@ class _UIHelpView extends StatelessWidget {
'• 5x: 5x speed\n' '• 5x: 5x speed\n'
'• 10x: 10x speed', '• 10x: 10x speed',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.pause, icon: '',
title: isKorean title: isKorean
? '일시정지' ? '일시정지'
: isJapanese : isJapanese
@@ -477,9 +426,9 @@ class _UIHelpView extends StatelessWidget {
: 'Use the pause button to stop the game. ' : 'Use the pause button to stop the game. '
'You can still view UI and change settings while paused.', 'You can still view UI and change settings while paused.',
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_HelpSection( _HelpSection(
icon: Icons.bar_chart, icon: '📊',
title: isKorean title: isKorean
? '통계' ? '통계'
: isJapanese : isJapanese
@@ -499,7 +448,7 @@ class _UIHelpView extends StatelessWidget {
} }
} }
/// 도움말 섹션 위젯 /// 레트로 스타일 도움말 섹션 위젯
class _HelpSection extends StatelessWidget { class _HelpSection extends StatelessWidget {
const _HelpSection({ const _HelpSection({
required this.icon, required this.icon,
@@ -507,46 +456,23 @@ class _HelpSection extends StatelessWidget {
required this.content, required this.content,
}); });
final IconData icon; final String icon;
final String title; final String title;
final String content; final String content;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 섹션 헤더 // 섹션 헤더
Row( RetroSectionHeader(
children: [ title: title,
Icon(icon, size: 20, color: theme.colorScheme.primary), icon: icon,
const SizedBox(width: 8), accentColor: RetroColors.mpBlue,
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
],
), ),
const SizedBox(height: 8),
// 내용 // 내용
Container( RetroInfoBox(content: content),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Text(
content,
style: theme.textTheme.bodyMedium?.copyWith(
height: 1.5,
),
),
),
], ],
); );
} }

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// HP/MP 바 위젯 (Phase 8: 변화 시 시각 효과) /// HP/MP 바 위젯 (레트로 RPG 스타일)
/// ///
/// - 세그먼트 스타일의 8-bit 프로그레스 바
/// - HP가 20% 미만일 때 빨간색 깜빡임 /// - HP가 20% 미만일 때 빨간색 깜빡임
/// - HP/MP 변화 시 색상 플래시 + 변화량 표시 /// - HP/MP 변화 시 색상 플래시 + 변화량 표시
/// - 전투 중 몬스터 HP 바 표시 /// - 전투 중 몬스터 HP 바 표시
@@ -158,8 +160,12 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
widget.monsterHpMax != null && widget.monsterHpMax != null &&
widget.monsterHpMax! > 0; widget.monsterHpMax! > 0;
return Padding( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: RetroColors.panelBg,
border: Border.all(color: RetroColors.panelBorderOuter, width: 2),
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -169,13 +175,14 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
current: widget.hpCurrent, current: widget.hpCurrent,
max: widget.hpMax, max: widget.hpMax,
ratio: hpRatio, ratio: hpRatio,
color: Colors.red, fillColor: RetroColors.hpRed,
emptyColor: RetroColors.hpRedDark,
isLow: hpRatio < 0.2 && hpRatio > 0, isLow: hpRatio < 0.2 && hpRatio > 0,
flashController: _hpFlashAnimation, flashController: _hpFlashAnimation,
change: _hpChange, change: _hpChange,
isDamage: _hpDamage, isDamage: _hpDamage,
), ),
const SizedBox(height: 4), const SizedBox(height: 6),
// MP 바 (플래시 효과 포함) // MP 바 (플래시 효과 포함)
_buildAnimatedBar( _buildAnimatedBar(
@@ -183,7 +190,8 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
current: widget.mpCurrent, current: widget.mpCurrent,
max: widget.mpMax, max: widget.mpMax,
ratio: mpRatio, ratio: mpRatio,
color: Colors.blue, fillColor: RetroColors.mpBlue,
emptyColor: RetroColors.mpBlueDark,
isLow: false, isLow: false,
flashController: _mpFlashAnimation, flashController: _mpFlashAnimation,
change: _mpChange, change: _mpChange,
@@ -202,7 +210,8 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
required int current, required int current,
required int max, required int max,
required double ratio, required double ratio,
required Color color, required Color fillColor,
required Color emptyColor,
required bool isLow, required bool isLow,
required Animation<double> flashController, required Animation<double> flashController,
required int change, required int change,
@@ -213,27 +222,28 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
builder: (context, child) { builder: (context, child) {
// 플래시 색상 (데미지=빨강, 회복=녹색) // 플래시 색상 (데미지=빨강, 회복=녹색)
final flashColor = isDamage final flashColor = isDamage
? Colors.red.withValues(alpha: flashController.value * 0.4) ? RetroColors.hpRed.withValues(alpha: flashController.value * 0.4)
: Colors.green.withValues(alpha: flashController.value * 0.4); : RetroColors.expGreen.withValues(alpha: flashController.value * 0.4);
// 위험 깜빡임 배경 // 위험 깜빡임 배경
final lowBgColor = isLow final lowBgColor = isLow
? Colors.red.withValues(alpha: (1 - _blinkAnimation.value) * 0.3) ? RetroColors.hpRed.withValues(alpha: (1 - _blinkAnimation.value) * 0.3)
: Colors.transparent; : Colors.transparent;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: flashController.value > 0.1 ? flashColor : lowBgColor, color: flashController.value > 0.1 ? flashColor : lowBgColor,
borderRadius: BorderRadius.circular(4),
), ),
child: Stack( child: Stack(
children: [ children: [
_buildBar( _buildRetroBar(
label: label, label: label,
current: current, current: current,
max: max, max: max,
ratio: ratio, ratio: ratio,
color: color, fillColor: fillColor,
emptyColor: emptyColor,
blinkOpacity: isLow ? _blinkAnimation.value : 1.0,
), ),
// 플로팅 변화량 텍스트 (위로 떠오르며 사라짐) // 플로팅 변화량 텍스트 (위로 떠오르며 사라짐)
@@ -250,9 +260,10 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
child: Text( child: Text(
change > 0 ? '+$change' : '$change', change > 0 ? '+$change' : '$change',
style: TextStyle( style: TextStyle(
fontSize: 12, fontFamily: 'PressStart2P',
fontSize: 8,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDamage ? Colors.red : Colors.green, color: isDamage ? RetroColors.hpRed : RetroColors.expGreen,
shadows: const [ shadows: const [
Shadow(color: Colors.black, blurRadius: 3), Shadow(color: Colors.black, blurRadius: 3),
Shadow(color: Colors.black, blurRadius: 6), Shadow(color: Colors.black, blurRadius: 6),
@@ -269,40 +280,81 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
); );
} }
Widget _buildBar({ /// 레트로 스타일 세그먼트 바
Widget _buildRetroBar({
required String label, required String label,
required int current, required int current,
required int max, required int max,
required double ratio, required double ratio,
required Color color, required Color fillColor,
required Color emptyColor,
required double blinkOpacity,
}) { }) {
const segmentCount = 15;
final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round();
return Row( return Row(
children: [ children: [
// 레이블 (HP/MP)
SizedBox( SizedBox(
width: 24, width: 28,
child: Text( child: Text(
label, label,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), style: TextStyle(
), fontFamily: 'PressStart2P',
), fontSize: 7,
Expanded( fontWeight: FontWeight.bold,
child: ClipRRect( color: RetroColors.gold.withValues(alpha: blinkOpacity),
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: ratio.clamp(0.0, 1.0),
backgroundColor: color.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 10,
), ),
), ),
), ),
const SizedBox(width: 4), // 세그먼트 바
// Flexible로 오버플로우 방지 Expanded(
Flexible( child: Container(
flex: 0, height: 12,
decoration: BoxDecoration(
color: emptyColor.withValues(alpha: 0.3),
border: Border.all(
color: RetroColors.panelBorderOuter,
width: 1,
),
),
child: Row(
children: List.generate(segmentCount, (index) {
final isFilled = index < filledSegments;
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isFilled
? fillColor.withValues(alpha: blinkOpacity)
: emptyColor.withValues(alpha: 0.2),
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.panelBorderOuter
.withValues(alpha: 0.3),
width: 1,
)
: BorderSide.none,
),
),
),
);
}),
),
),
),
const SizedBox(width: 6),
// 수치 표시
SizedBox(
width: 60,
child: Text( child: Text(
'$current/$max', '$current/$max',
style: const TextStyle(fontSize: 9), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textLight,
),
textAlign: TextAlign.right, textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -311,53 +363,90 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
); );
} }
/// 몬스터 HP 바 /// 몬스터 HP 바 (레트로 스타일)
Widget _buildMonsterBar() { Widget _buildMonsterBar() {
final max = widget.monsterHpMax!; final max = widget.monsterHpMax!;
final ratio = max > 0 ? widget.monsterHpCurrent! / max : 0.0; final ratio = max > 0 ? widget.monsterHpCurrent! / max : 0.0;
const segmentCount = 10;
final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round();
return AnimatedBuilder( return AnimatedBuilder(
animation: _monsterFlashAnimation, animation: _monsterFlashAnimation,
builder: (context, child) { builder: (context, child) {
// 데미지 플래시 (몬스터는 항상 데미지를 받음) // 데미지 플래시 (몬스터는 항상 데미지를 받음)
final flashColor = Colors.yellow.withValues( final flashColor = RetroColors.gold.withValues(
alpha: _monsterFlashAnimation.value * 0.3, alpha: _monsterFlashAnimation.value * 0.3,
); );
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _monsterFlashAnimation.value > 0.1 color: _monsterFlashAnimation.value > 0.1
? flashColor ? flashColor
: Colors.orange.withValues(alpha: 0.1), : RetroColors.panelBgLight.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(4), border: Border.all(
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), color: RetroColors.gold.withValues(alpha: 0.6),
width: 1,
),
), ),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
// HP 바만 표시 (공간 제약으로 아이콘/이름 생략)
Row( Row(
children: [ children: [
// HP 바 // 몬스터 아이콘
const Icon(
Icons.pest_control,
size: 12,
color: RetroColors.gold,
),
const SizedBox(width: 6),
// 세그먼트 HP 바
Expanded( Expanded(
child: ClipRRect( child: Container(
borderRadius: BorderRadius.circular(2), height: 10,
child: LinearProgressIndicator( decoration: BoxDecoration(
value: ratio.clamp(0.0, 1.0), color: RetroColors.hpRedDark.withValues(alpha: 0.3),
backgroundColor: Colors.orange.withValues(alpha: 0.2), border: Border.all(
valueColor: const AlwaysStoppedAnimation<Color>( color: RetroColors.panelBorderOuter,
Colors.orange, width: 1,
), ),
minHeight: 8, ),
child: Row(
children: List.generate(segmentCount, (index) {
final isFilled = index < filledSegments;
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isFilled
? RetroColors.gold
: RetroColors.panelBorderOuter
.withValues(alpha: 0.3),
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.panelBorderOuter
.withValues(alpha: 0.3),
width: 1,
)
: BorderSide.none,
),
),
),
);
}),
), ),
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 6),
// HP 퍼센트 // HP 퍼센트
Text( Text(
'${(ratio * 100).toInt()}%', '${(ratio * 100).toInt()}%',
style: const TextStyle(fontSize: 8, color: Colors.orange), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.gold,
),
), ),
], ],
), ),
@@ -365,8 +454,8 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
// 플로팅 데미지 텍스트 // 플로팅 데미지 텍스트
if (_monsterHpChange != 0 && _monsterFlashAnimation.value > 0.05) if (_monsterHpChange != 0 && _monsterFlashAnimation.value > 0.05)
Positioned( Positioned(
right: 60, right: 50,
top: -5, top: -8,
child: Transform.translate( child: Transform.translate(
offset: Offset(0, -12 * (1 - _monsterFlashAnimation.value)), offset: Offset(0, -12 * (1 - _monsterFlashAnimation.value)),
child: Opacity( child: Opacity(
@@ -376,11 +465,12 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
? '+$_monsterHpChange' ? '+$_monsterHpChange'
: '$_monsterHpChange', : '$_monsterHpChange',
style: TextStyle( style: TextStyle(
fontSize: 11, fontFamily: 'PressStart2P',
fontSize: 7,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _monsterHpChange < 0 color: _monsterHpChange < 0
? Colors.yellow ? RetroColors.gold
: Colors.green, : RetroColors.expGreen,
shadows: const [ shadows: const [
Shadow(color: Colors.black, blurRadius: 3), Shadow(color: Colors.black, blurRadius: 3),
Shadow(color: Colors.black, blurRadius: 6), Shadow(color: Colors.black, blurRadius: 6),

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:askiineverdie/src/core/notification/notification_service.dart'; import 'package:askiineverdie/src/core/notification/notification_service.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 알림 오버레이 위젯 (Phase 8: 팝업/토스트 알림) /// 알림 오버레이 위젯 (Phase 8: 팝업/토스트 알림)
/// ///
@@ -106,7 +107,7 @@ class _NotificationOverlayState extends State<NotificationOverlay>
} }
} }
/// 알림 카드 위젯 /// 레트로 스타일 알림 카드 위젯
class _NotificationCard extends StatelessWidget { class _NotificationCard extends StatelessWidget {
const _NotificationCard({ const _NotificationCard({
required this.notification, required this.notification,
@@ -118,118 +119,191 @@ class _NotificationCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final (bgColor, icon, iconColor) = _getStyleForType(notification.type); final (accentColor, icon, asciiIcon) = _getStyleForType(notification.type);
return Material( return GestureDetector(
elevation: 8, onTap: onDismiss,
borderRadius: BorderRadius.circular(12), child: Container(
color: bgColor, decoration: BoxDecoration(
child: InkWell( color: RetroColors.panelBg,
onTap: onDismiss, border: Border(
borderRadius: BorderRadius.circular(12), top: BorderSide(color: accentColor, width: 3),
child: Container( left: BorderSide(color: accentColor, width: 3),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), bottom: const BorderSide(color: RetroColors.panelBorderOuter, width: 3),
child: Row( right: const BorderSide(color: RetroColors.panelBorderOuter, width: 3),
children: [ ),
// 아이콘 boxShadow: [
Container( BoxShadow(
width: 40, color: accentColor.withValues(alpha: 0.4),
height: 40, blurRadius: 12,
decoration: BoxDecoration( spreadRadius: 2,
color: iconColor.withValues(alpha: 0.2), ),
shape: BoxShape.circle, ],
), ),
child: Icon(icon, color: iconColor, size: 24), child: Column(
), mainAxisSize: MainAxisSize.min,
const SizedBox(width: 12), children: [
// 텍스트 // 헤더 바
Expanded( Container(
child: Column( width: double.infinity,
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
mainAxisSize: MainAxisSize.min, color: accentColor.withValues(alpha: 0.3),
children: [ child: Row(
Text( children: [
notification.title, // ASCII 아이콘
style: const TextStyle( Text(
fontSize: 16, asciiIcon,
fontWeight: FontWeight.bold, style: TextStyle(
color: Colors.white, fontFamily: 'JetBrainsMono',
fontSize: 12,
color: accentColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
// 타입 표시
Expanded(
child: Text(
_getTypeLabel(notification.type),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: accentColor,
letterSpacing: 1,
), ),
), ),
if (notification.subtitle != null) ...[ ),
const SizedBox(height: 2), // 닫기 버튼
Text( GestureDetector(
notification.subtitle!, onTap: onDismiss,
style: TextStyle( child: const Text(
fontSize: 13, '[X]',
color: Colors.white.withValues(alpha: 0.8), style: TextStyle(
), fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
), ),
], ),
], ),
), ],
), ),
// 닫기 버튼 ),
IconButton( // 본문
icon: const Icon(Icons.close, color: Colors.white70, size: 20), Padding(
onPressed: onDismiss, padding: const EdgeInsets.all(12),
padding: EdgeInsets.zero, child: Row(
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), children: [
// 아이콘 박스
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: RetroColors.panelBgLight,
border: Border.all(color: accentColor, width: 2),
),
child: Icon(icon, color: accentColor, size: 20),
),
const SizedBox(width: 12),
// 텍스트
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
notification.title,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: RetroColors.textLight,
),
),
if (notification.subtitle != null) ...[
const SizedBox(height: 4),
Text(
notification.subtitle!,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
),
],
],
),
),
],
), ),
], ),
), ],
), ),
), ),
); );
} }
(Color, IconData, Color) _getStyleForType(NotificationType type) { /// 알림 타입별 레트로 스타일 (강조 색상, 아이콘, ASCII 아이콘)
(Color, IconData, String) _getStyleForType(NotificationType type) {
return switch (type) { return switch (type) {
NotificationType.levelUp => ( NotificationType.levelUp => (
const Color(0xFF1565C0), RetroColors.gold,
Icons.trending_up, Icons.arrow_upward,
Colors.amber, '',
), ),
NotificationType.questComplete => ( NotificationType.questComplete => (
const Color(0xFF2E7D32), RetroColors.expGreen,
Icons.check_circle, Icons.check,
Colors.lightGreen, '',
), ),
NotificationType.actComplete => ( NotificationType.actComplete => (
const Color(0xFF6A1B9A), RetroColors.mpBlue,
Icons.flag, Icons.flag,
Colors.purpleAccent, '',
), ),
NotificationType.newSpell => ( NotificationType.newSpell => (
const Color(0xFF4527A0), const Color(0xFF9966FF),
Icons.auto_fix_high, Icons.auto_fix_high,
Colors.deepPurpleAccent, '',
), ),
NotificationType.newEquipment => ( NotificationType.newEquipment => (
const Color(0xFFE65100), const Color(0xFFFF9933),
Icons.shield, Icons.shield,
Colors.orange, '',
), ),
NotificationType.bossDefeat => ( NotificationType.bossDefeat => (
const Color(0xFFC62828), RetroColors.hpRed,
Icons.whatshot, Icons.whatshot,
Colors.redAccent, '',
), ),
NotificationType.gameSaved => ( NotificationType.gameSaved => (
const Color(0xFF00695C), RetroColors.expGreen,
Icons.save, Icons.save,
Colors.tealAccent, '💾',
), ),
NotificationType.info => ( NotificationType.info => (
const Color(0xFF0277BD), RetroColors.mpBlue,
Icons.info_outline, Icons.info_outline,
Colors.lightBlueAccent, '',
), ),
NotificationType.warning => ( NotificationType.warning => (
const Color(0xFFF57C00), const Color(0xFFFFCC00),
Icons.warning_amber, Icons.warning,
Colors.amber, '',
), ),
}; };
} }
/// 알림 타입 라벨
String _getTypeLabel(NotificationType type) {
return switch (type) {
NotificationType.levelUp => 'LEVEL UP',
NotificationType.questComplete => 'QUEST DONE',
NotificationType.actComplete => 'ACT CLEAR',
NotificationType.newSpell => 'NEW SPELL',
NotificationType.newEquipment => 'NEW ITEM',
NotificationType.bossDefeat => 'BOSS SLAIN',
NotificationType.gameSaved => 'SAVED',
NotificationType.info => 'INFO',
NotificationType.warning => 'WARNING',
};
}
} }

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:askiineverdie/src/core/model/game_statistics.dart'; import 'package:askiineverdie/src/core/model/game_statistics.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
import 'package:askiineverdie/src/shared/widgets/retro_dialog.dart';
/// 게임 통계 다이얼로그 (Statistics Dialog) /// 게임 통계 다이얼로그 (Statistics Dialog)
/// ///
@@ -23,6 +25,7 @@ class StatisticsDialog extends StatefulWidget {
}) { }) {
return showDialog( return showDialog(
context: context, context: context,
barrierColor: Colors.black87,
builder: (_) => StatisticsDialog( builder: (_) => StatisticsDialog(
session: session, session: session,
cumulative: cumulative, cumulative: cumulative,
@@ -52,84 +55,46 @@ class _StatisticsDialogState extends State<StatisticsDialog>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final isKorean = Localizations.localeOf(context).languageCode == 'ko'; final isKorean = Localizations.localeOf(context).languageCode == 'ko';
final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
return Dialog( final title = isKorean
child: Container( ? '통계'
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 500), : isJapanese
child: Column( ? '統計'
mainAxisSize: MainAxisSize.min, : 'Statistics';
children: [
// 헤더 final tabs = isKorean
Container( ? ['세션', '누적']
padding: const EdgeInsets.all(16), : isJapanese
decoration: BoxDecoration( ? ['セッション', '累積']
color: theme.colorScheme.primaryContainer, : ['Session', 'Total'];
borderRadius: const BorderRadius.vertical(
top: Radius.circular(28), return RetroDialog(
), title: title,
), titleIcon: '📊',
child: Row( maxWidth: 420,
children: [ maxHeight: 520,
Icon( accentColor: RetroColors.gold,
Icons.bar_chart, child: Column(
color: theme.colorScheme.onPrimaryContainer, children: [
), // 탭 바
const SizedBox(width: 12), RetroTabBar(
Expanded( controller: _tabController,
child: Text( tabs: tabs,
isKorean accentColor: RetroColors.gold,
? '게임 통계' ),
: isJapanese // 탭 내용
? 'ゲーム統計' Expanded(
: 'Game Statistics', child: TabBarView(
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onPrimaryContainer,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
color: theme.colorScheme.onPrimaryContainer,
),
],
),
),
// 탭 바
TabBar(
controller: _tabController, controller: _tabController,
tabs: [ children: [
Tab( _SessionStatisticsView(stats: widget.session),
text: isKorean _CumulativeStatisticsView(stats: widget.cumulative),
? '현재 세션'
: isJapanese
? '現在のセッション'
: 'Session',
),
Tab(
text: isKorean
? '누적 통계'
: isJapanese
? '累積統計'
: 'Cumulative',
),
], ],
), ),
// 탭 내용 ),
Expanded( ],
child: TabBarView(
controller: _tabController,
children: [
_SessionStatisticsView(stats: widget.session),
_CumulativeStatisticsView(stats: widget.cumulative),
],
),
),
],
),
), ),
); );
} }
@@ -147,7 +112,7 @@ class _SessionStatisticsView extends StatelessWidget {
final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
children: [ children: [
_StatSection( _StatSection(
title: isKorean title: isKorean
@@ -155,7 +120,7 @@ class _SessionStatisticsView extends StatelessWidget {
: isJapanese : isJapanese
? '戦闘' ? '戦闘'
: 'Combat', : 'Combat',
icon: Icons.sports_mma, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -191,14 +156,14 @@ class _SessionStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '데미지' ? '데미지'
: isJapanese : isJapanese
? 'ダメージ' ? 'ダメージ'
: 'Damage', : 'Damage',
icon: Icons.flash_on, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -226,14 +191,14 @@ class _SessionStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '스킬' ? '스킬'
: isJapanese : isJapanese
? 'スキル' ? 'スキル'
: 'Skills', : 'Skills',
icon: Icons.auto_awesome, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -269,14 +234,14 @@ class _SessionStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '경제' ? '경제'
: isJapanese : isJapanese
? '経済' ? '経済'
: 'Economy', : 'Economy',
icon: Icons.monetization_on, icon: '💰',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -312,14 +277,14 @@ class _SessionStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '진행' ? '진행'
: isJapanese : isJapanese
? '進行' ? '進行'
: 'Progress', : 'Progress',
icon: Icons.trending_up, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -356,7 +321,7 @@ class _CumulativeStatisticsView extends StatelessWidget {
final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
children: [ children: [
_StatSection( _StatSection(
title: isKorean title: isKorean
@@ -364,7 +329,7 @@ class _CumulativeStatisticsView extends StatelessWidget {
: isJapanese : isJapanese
? '記録' ? '記録'
: 'Records', : 'Records',
icon: Icons.emoji_events, icon: '🏆',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -395,14 +360,14 @@ class _CumulativeStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '총 플레이' ? '총 플레이'
: isJapanese : isJapanese
? '総プレイ' ? '総プレイ'
: 'Total Play', : 'Total Play',
icon: Icons.access_time, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -438,14 +403,14 @@ class _CumulativeStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '총 전투' ? '총 전투'
: isJapanese : isJapanese
? '総戦闘' ? '総戦闘'
: 'Total Combat', : 'Total Combat',
icon: Icons.sports_mma, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -481,14 +446,14 @@ class _CumulativeStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '총 데미지' ? '총 데미지'
: isJapanese : isJapanese
? '総ダメージ' ? '総ダメージ'
: 'Total Damage', : 'Total Damage',
icon: Icons.flash_on, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -508,14 +473,14 @@ class _CumulativeStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '총 스킬' ? '총 스킬'
: isJapanese : isJapanese
? '総スキル' ? '総スキル'
: 'Total Skills', : 'Total Skills',
icon: Icons.auto_awesome, icon: '',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -535,14 +500,14 @@ class _CumulativeStatisticsView extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
_StatSection( _StatSection(
title: isKorean title: isKorean
? '총 경제' ? '총 경제'
: isJapanese : isJapanese
? '総経済' ? '総経済'
: 'Total Economy', : 'Total Economy',
icon: Icons.monetization_on, icon: '💰',
items: [ items: [
_StatItem( _StatItem(
label: isKorean label: isKorean
@@ -591,7 +556,7 @@ class _CumulativeStatisticsView extends StatelessWidget {
} }
} }
/// 통계 섹션 위젯 /// 레트로 스타일 통계 섹션 위젯
class _StatSection extends StatelessWidget { class _StatSection extends StatelessWidget {
const _StatSection({ const _StatSection({
required this.title, required this.title,
@@ -600,31 +565,20 @@ class _StatSection extends StatelessWidget {
}); });
final String title; final String title;
final IconData icon; final String icon;
final List<_StatItem> items; final List<_StatItem> items;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 섹션 헤더 // 섹션 헤더
Row( RetroSectionHeader(
children: [ title: title,
Icon(icon, size: 18, color: theme.colorScheme.primary), icon: icon,
const SizedBox(width: 8), accentColor: RetroColors.gold,
Text(
title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
],
), ),
const Divider(height: 8),
// 통계 항목들 // 통계 항목들
...items, ...items,
], ],
@@ -632,7 +586,7 @@ class _StatSection extends StatelessWidget {
} }
} }
/// 개별 통계 항목 위젯 /// 레트로 스타일 개별 통계 항목 위젯
class _StatItem extends StatelessWidget { class _StatItem extends StatelessWidget {
const _StatItem({ const _StatItem({
required this.label, required this.label,
@@ -646,42 +600,11 @@ class _StatItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); return RetroStatRow(
label: label,
return Padding( value: value,
padding: const EdgeInsets.symmetric(vertical: 4), highlight: highlight,
child: Row( highlightColor: RetroColors.gold,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
Container(
padding: highlight
? const EdgeInsets.symmetric(horizontal: 8, vertical: 2)
: null,
decoration: highlight
? BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
)
: null,
child: Text(
value,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
color: highlight
? theme.colorScheme.onPrimaryContainer
: theme.colorScheme.onSurface,
),
),
),
],
),
); );
} }
} }

View File

@@ -13,6 +13,8 @@ import 'package:askiineverdie/src/core/util/deterministic_random.dart';
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:askiineverdie/src/core/util/pq_logic.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart';
import 'package:askiineverdie/src/features/new_character/widgets/race_preview.dart'; import 'package:askiineverdie/src/features/new_character/widgets/race_preview.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
import 'package:askiineverdie/src/shared/widgets/retro_widgets.dart';
/// 캐릭터 생성 화면 (NewGuy.pas 포팅) /// 캐릭터 생성 화면 (NewGuy.pas 포팅)
class NewCharacterScreen extends StatefulWidget { class NewCharacterScreen extends StatefulWidget {
@@ -268,9 +270,19 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: RetroColors.deepBrown,
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context).newCharacterTitle), backgroundColor: RetroColors.darkBrown,
title: Text(
L10n.of(context).newCharacterTitle.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
centerTitle: true, centerTitle: true,
iconTheme: const IconThemeData(color: RetroColors.gold),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -286,9 +298,13 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
// 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션) // 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션)
Center( RetroPanel(
child: RacePreview( title: 'PREVIEW',
raceId: _races[_selectedRaceIndex].raceId, padding: const EdgeInsets.all(8),
child: Center(
child: RacePreview(
raceId: _races[_selectedRaceIndex].raceId,
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -309,13 +325,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Sold! 버튼 // Sold! 버튼
FilledButton.icon( RetroTextButton(
text: L10n.of(context).soldButton,
icon: Icons.check,
onPressed: _onSold, onPressed: _onSold,
icon: const Icon(Icons.check),
label: Text(L10n.of(context).soldButton),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
), ),
], ],
), ),
@@ -325,124 +338,140 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
Widget _buildNameSection() { Widget _buildNameSection() {
final l10n = L10n.of(context); final l10n = L10n.of(context);
return Card( return RetroPanel(
child: Padding( title: 'NAME',
padding: const EdgeInsets.all(16), child: Row(
child: Row( children: [
children: [ Expanded(
Expanded( child: TextField(
child: TextField( controller: _nameController,
controller: _nameController, style: const TextStyle(
decoration: InputDecoration( fontFamily: 'PressStart2P',
labelText: l10n.name, fontSize: 10,
border: const OutlineInputBorder(), color: RetroColors.textLight,
),
maxLength: 30,
), ),
decoration: InputDecoration(
labelText: l10n.name,
labelStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.gold,
),
border: const OutlineInputBorder(
borderSide: BorderSide(color: RetroColors.panelBorderInner),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: RetroColors.panelBorderInner),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: RetroColors.gold, width: 2),
),
counterStyle: const TextStyle(color: RetroColors.textDisabled),
),
maxLength: 30,
), ),
const SizedBox(width: 8), ),
IconButton.filled( const SizedBox(width: 8),
onPressed: _onGenerateName, RetroIconButton(
icon: const Icon(Icons.casino), icon: Icons.casino,
tooltip: l10n.generateName, onPressed: _onGenerateName,
), ),
], ],
),
), ),
); );
} }
Widget _buildStatsSection() { Widget _buildStatsSection() {
final l10n = L10n.of(context); final l10n = L10n.of(context);
return Card( return RetroPanel(
child: Padding( title: 'STATS',
padding: const EdgeInsets.all(16), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ // 스탯 그리드
Text(l10n.stats, style: Theme.of(context).textTheme.titleMedium), Row(
const SizedBox(height: 12), children: [
Expanded(child: _buildStatTile(l10n.statStr, _str)),
Expanded(child: _buildStatTile(l10n.statCon, _con)),
Expanded(child: _buildStatTile(l10n.statDex, _dex)),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(child: _buildStatTile(l10n.statInt, _int)),
Expanded(child: _buildStatTile(l10n.statWis, _wis)),
Expanded(child: _buildStatTile(l10n.statCha, _cha)),
],
),
const SizedBox(height: 12),
// 스탯 그리드 // Total
Row( Container(
children: [ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
Expanded(child: _buildStatTile(l10n.statStr, _str)), decoration: BoxDecoration(
Expanded(child: _buildStatTile(l10n.statCon, _con)), color: _getTotalColor().withValues(alpha: 0.2),
Expanded(child: _buildStatTile(l10n.statDex, _dex)), border: Border.all(color: _getTotalColor(), width: 2),
],
), ),
const SizedBox(height: 8), child: Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildStatTile(l10n.statInt, _int)), Text(
Expanded(child: _buildStatTile(l10n.statWis, _wis)), l10n.total.toUpperCase(),
Expanded(child: _buildStatTile(l10n.statCha, _cha)), style: const TextStyle(
], fontFamily: 'PressStart2P',
), fontSize: 8,
const SizedBox(height: 12), fontWeight: FontWeight.bold,
color: RetroColors.textLight,
// Total
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _getTotalColor().withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _getTotalColor()),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.total,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'$_total',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _getTotalColor() == Colors.white
? Colors.black
: _getTotalColor(),
),
),
],
),
),
const SizedBox(height: 12),
// Roll 버튼들
Wrap(
alignment: WrapAlignment.center,
spacing: 16,
runSpacing: 8,
children: [
OutlinedButton.icon(
onPressed: _onUnroll,
icon: const Icon(Icons.undo),
label: Text(l10n.unroll),
style: OutlinedButton.styleFrom(
foregroundColor: _rollHistory.isEmpty ? Colors.grey : null,
), ),
), ),
FilledButton.icon( Text(
onPressed: _onReroll, '$_total',
icon: const Icon(Icons.casino), style: TextStyle(
label: Text(l10n.roll), fontFamily: 'PressStart2P',
fontSize: 14,
fontWeight: FontWeight.bold,
color: _getTotalColor(),
),
), ),
], ],
), ),
if (_rollHistory.isNotEmpty) ),
Padding( const SizedBox(height: 12),
padding: const EdgeInsets.only(top: 8),
// Roll 버튼들
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RetroTextButton(
text: l10n.unroll,
icon: Icons.undo,
onPressed: _rollHistory.isEmpty ? null : _onUnroll,
isPrimary: false,
),
const SizedBox(width: 16),
RetroTextButton(
text: l10n.roll,
icon: Icons.casino,
onPressed: _onReroll,
),
],
),
if (_rollHistory.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Center(
child: Text( child: Text(
game_l10n.uiRollHistory(_rollHistory.length), game_l10n.uiRollHistory(_rollHistory.length),
style: Theme.of(context).textTheme.bodySmall, style: const TextStyle(
textAlign: TextAlign.center, fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
),
), ),
), ),
], ),
), ],
), ),
); );
} }
@@ -452,16 +481,28 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
margin: const EdgeInsets.all(4), margin: const EdgeInsets.all(4),
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: RetroColors.panelBgLight,
borderRadius: BorderRadius.circular(8), border: Border.all(color: RetroColors.panelBorderInner),
), ),
child: Column( child: Column(
children: [ children: [
Text(label, style: Theme.of(context).textTheme.labelSmall), Text(
label.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.gold,
),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'$value', '$value',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
fontWeight: FontWeight.bold,
color: RetroColors.textLight,
),
), ),
], ],
), ),
@@ -469,51 +510,59 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
} }
Widget _buildRaceSection() { Widget _buildRaceSection() {
return Card( return RetroPanel(
child: Padding( title: 'RACE',
padding: const EdgeInsets.all(16), child: SizedBox(
child: Column( height: 300,
crossAxisAlignment: CrossAxisAlignment.start, child: ListView.builder(
children: [ controller: _raceScrollController,
Text( itemCount: _races.length,
L10n.of(context).race, itemBuilder: (context, index) {
style: Theme.of(context).textTheme.titleMedium, final isSelected = index == _selectedRaceIndex;
), final race = _races[index];
const SizedBox(height: 8), return GestureDetector(
SizedBox( onTap: () => setState(() => _selectedRaceIndex = index),
height: 300, child: Container(
child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
controller: _raceScrollController, decoration: BoxDecoration(
itemCount: _races.length, color: isSelected ? RetroColors.panelBgLight : null,
itemBuilder: (context, index) { border: isSelected
final isSelected = index == _selectedRaceIndex; ? Border.all(color: RetroColors.gold, width: 1)
final race = _races[index]; : null,
return ListTile( ),
leading: Icon( child: Column(
isSelected crossAxisAlignment: CrossAxisAlignment.start,
? Icons.radio_button_checked children: [
: Icons.radio_button_unchecked, Row(
color: isSelected children: [
? Theme.of(context).colorScheme.primary Icon(
: null, isSelected ? Icons.arrow_right : Icons.remove,
size: 12,
color: isSelected
? RetroColors.gold
: RetroColors.textDisabled,
),
const SizedBox(width: 4),
Expanded(
child: Text(
GameDataL10n.getRaceName(context, race.name),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: isSelected
? RetroColors.gold
: RetroColors.textLight,
),
),
),
],
), ),
title: Text( if (isSelected) _buildRaceInfo(race),
GameDataL10n.getRaceName(context, race.name), ],
style: TextStyle( ),
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
subtitle: isSelected ? _buildRaceInfo(race) : null,
dense: !isSelected,
visualDensity: VisualDensity.compact,
onTap: () => setState(() => _selectedRaceIndex = index),
);
},
), ),
), );
], },
), ),
), ),
); );
@@ -531,22 +580,23 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
? race.passives.map((p) => _translateRacePassive(p)).join(', ') ? race.passives.map((p) => _translateRacePassive(p)).join(', ')
: ''; : '';
return Column( return Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only(left: 16, top: 4),
children: [ child: Column(
if (statMods.isNotEmpty) crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
statMods.join(', '), if (statMods.isNotEmpty)
style: Theme.of(context).textTheme.bodySmall, Text(
), statMods.join(', '),
if (passiveDesc.isNotEmpty) style: const TextStyle(fontSize: 9, color: RetroColors.textLight),
Text(
passiveDesc,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
), ),
), if (passiveDesc.isNotEmpty)
], Text(
passiveDesc,
style: const TextStyle(fontSize: 9, color: RetroColors.expGreen),
),
],
),
); );
} }
@@ -576,51 +626,59 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
} }
Widget _buildKlassSection() { Widget _buildKlassSection() {
return Card( return RetroPanel(
child: Padding( title: 'CLASS',
padding: const EdgeInsets.all(16), child: SizedBox(
child: Column( height: 300,
crossAxisAlignment: CrossAxisAlignment.start, child: ListView.builder(
children: [ controller: _klassScrollController,
Text( itemCount: _klasses.length,
L10n.of(context).classTitle, itemBuilder: (context, index) {
style: Theme.of(context).textTheme.titleMedium, final isSelected = index == _selectedKlassIndex;
), final klass = _klasses[index];
const SizedBox(height: 8), return GestureDetector(
SizedBox( onTap: () => setState(() => _selectedKlassIndex = index),
height: 300, child: Container(
child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
controller: _klassScrollController, decoration: BoxDecoration(
itemCount: _klasses.length, color: isSelected ? RetroColors.panelBgLight : null,
itemBuilder: (context, index) { border: isSelected
final isSelected = index == _selectedKlassIndex; ? Border.all(color: RetroColors.gold, width: 1)
final klass = _klasses[index]; : null,
return ListTile( ),
leading: Icon( child: Column(
isSelected crossAxisAlignment: CrossAxisAlignment.start,
? Icons.radio_button_checked children: [
: Icons.radio_button_unchecked, Row(
color: isSelected children: [
? Theme.of(context).colorScheme.primary Icon(
: null, isSelected ? Icons.arrow_right : Icons.remove,
size: 12,
color: isSelected
? RetroColors.gold
: RetroColors.textDisabled,
),
const SizedBox(width: 4),
Expanded(
child: Text(
GameDataL10n.getKlassName(context, klass.name),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: isSelected
? RetroColors.gold
: RetroColors.textLight,
),
),
),
],
), ),
title: Text( if (isSelected) _buildClassInfo(klass),
GameDataL10n.getKlassName(context, klass.name), ],
style: TextStyle( ),
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
subtitle: isSelected ? _buildClassInfo(klass) : null,
dense: !isSelected,
visualDensity: VisualDensity.compact,
onTap: () => setState(() => _selectedKlassIndex = index),
);
},
), ),
), );
], },
), ),
), ),
); );
@@ -638,22 +696,23 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
? klass.passives.map((p) => _translateClassPassive(p)).join(', ') ? klass.passives.map((p) => _translateClassPassive(p)).join(', ')
: ''; : '';
return Column( return Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only(left: 16, top: 4),
children: [ child: Column(
if (statMods.isNotEmpty) crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
statMods.join(', '), if (statMods.isNotEmpty)
style: Theme.of(context).textTheme.bodySmall, Text(
), statMods.join(', '),
if (passiveDesc.isNotEmpty) style: const TextStyle(fontSize: 9, color: RetroColors.textLight),
Text(
passiveDesc,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.secondary,
), ),
), if (passiveDesc.isNotEmpty)
], Text(
passiveDesc,
style: const TextStyle(fontSize: 9, color: RetroColors.expGreen),
),
],
),
); );
} }
@@ -678,13 +737,81 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
/// 테스트 모드 토글 위젯 /// 테스트 모드 토글 위젯
Widget _buildTestModeToggle() { Widget _buildTestModeToggle() {
return Card( return RetroPanel(
child: SwitchListTile( title: 'OPTIONS',
title: Text(game_l10n.uiTestMode), child: GestureDetector(
subtitle: Text(game_l10n.uiTestModeDesc), onTap: () => setState(() => _testModeEnabled = !_testModeEnabled),
value: _testModeEnabled, child: Container(
onChanged: (value) => setState(() => _testModeEnabled = value), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
secondary: const Icon(Icons.phone_android), child: Row(
children: [
Icon(
Icons.phone_android,
size: 18,
color: _testModeEnabled
? RetroColors.gold
: RetroColors.textDisabled,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
game_l10n.uiTestMode,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: _testModeEnabled
? RetroColors.gold
: RetroColors.textLight,
),
),
const SizedBox(height: 4),
Text(
game_l10n.uiTestModeDesc,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
),
),
],
),
),
Container(
width: 40,
height: 20,
decoration: BoxDecoration(
color: _testModeEnabled
? RetroColors.expGreen
: RetroColors.panelBgLight,
border: Border.all(
color: _testModeEnabled
? RetroColors.expGreen
: RetroColors.panelBorderInner,
width: 2,
),
),
child: Row(
mainAxisAlignment: _testModeEnabled
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Container(
width: 16,
height: 16,
margin: const EdgeInsets.all(1),
color: _testModeEnabled
? RetroColors.textLight
: RetroColors.textDisabled,
),
],
),
),
],
),
),
), ),
); );
} }

View File

@@ -31,6 +31,7 @@ class RetroButton extends StatefulWidget {
class _RetroButtonState extends State<RetroButton> { class _RetroButtonState extends State<RetroButton> {
bool _isPressed = false; bool _isPressed = false;
bool _isHovered = false;
bool get _isEnabled => widget.onPressed != null; bool get _isEnabled => widget.onPressed != null;
@@ -41,6 +42,12 @@ class _RetroButtonState extends State<RetroButton> {
? RetroColors.buttonPrimaryPressed ? RetroColors.buttonPrimaryPressed
: RetroColors.buttonSecondaryPressed; : RetroColors.buttonSecondaryPressed;
} }
// 호버 시 밝아지는 효과
if (_isHovered) {
return widget.isPrimary
? RetroColors.buttonPrimary.withValues(alpha: 0.9)
: RetroColors.buttonSecondary.withValues(alpha: 0.9);
}
return widget.isPrimary return widget.isPrimary
? RetroColors.buttonPrimary ? RetroColors.buttonPrimary
: RetroColors.buttonSecondary; : RetroColors.buttonSecondary;
@@ -48,43 +55,64 @@ class _RetroButtonState extends State<RetroButton> {
Color get _borderTopLeft { Color get _borderTopLeft {
if (_isPressed) return RetroColors.panelBorderOuter; if (_isPressed) return RetroColors.panelBorderOuter;
// 호버 시 골드 테두리
if (_isHovered && _isEnabled) return RetroColors.gold;
return RetroColors.panelBorderInner; return RetroColors.panelBorderInner;
} }
Color get _borderBottomRight { Color get _borderBottomRight {
if (_isPressed) return RetroColors.panelBorderInner; if (_isPressed) return RetroColors.panelBorderInner;
// 호버 시 골드 테두리
if (_isHovered && _isEnabled) return RetroColors.goldDark;
return RetroColors.panelBorderOuter; return RetroColors.panelBorderOuter;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return MouseRegion(
onTapDown: _isEnabled ? (_) => setState(() => _isPressed = true) : null, onEnter: _isEnabled ? (_) => setState(() => _isHovered = true) : null,
onTapUp: _isEnabled ? (_) => setState(() => _isPressed = false) : null, onExit: _isEnabled ? (_) => setState(() => _isHovered = false) : null,
onTapCancel: _isEnabled ? () => setState(() => _isPressed = false) : null, cursor: _isEnabled ? SystemMouseCursors.click : SystemMouseCursors.basic,
onTap: widget.onPressed, child: GestureDetector(
child: AnimatedContainer( onTapDown: _isEnabled ? (_) => setState(() => _isPressed = true) : null,
duration: const Duration(milliseconds: 50), onTapUp: _isEnabled ? (_) => setState(() => _isPressed = false) : null,
padding: widget.padding, onTapCancel: _isEnabled ? () => setState(() => _isPressed = false) : null,
decoration: BoxDecoration( onTap: widget.onPressed,
color: _backgroundColor, child: AnimatedContainer(
border: Border( duration: const Duration(milliseconds: 100),
top: BorderSide(color: _borderTopLeft, width: 2), padding: widget.padding,
left: BorderSide(color: _borderTopLeft, width: 2), decoration: BoxDecoration(
bottom: BorderSide(color: _borderBottomRight, width: 2), color: _backgroundColor,
right: BorderSide(color: _borderBottomRight, width: 2), 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),
),
// 호버 시 글로우 효과
boxShadow: _isHovered && _isEnabled
? [
BoxShadow(
color: RetroColors.gold.withValues(alpha: 0.3),
blurRadius: 8,
spreadRadius: 1,
),
]
: null,
), ),
), transform: _isPressed
transform: _isPressed ? Matrix4.translationValues(1, 1, 0)
? Matrix4.translationValues(1, 1, 0) : Matrix4.identity(),
: Matrix4.identity(), child: DefaultTextStyle(
child: DefaultTextStyle( style: TextStyle(
style: TextStyle( fontFamily: 'PressStart2P',
fontFamily: 'PressStart2P', fontSize: 10,
fontSize: 10, color: _isEnabled
color: _isEnabled ? RetroColors.textLight : RetroColors.textDisabled, ? (_isHovered ? RetroColors.gold : RetroColors.textLight)
: RetroColors.textDisabled,
),
child: widget.child,
), ),
child: widget.child,
), ),
), ),
); );
@@ -148,3 +176,118 @@ class RetroIconButton extends StatelessWidget {
); );
} }
} }
/// 레트로 호버 효과 리스트 아이템
/// 웹/데스크톱에서 마우스 호버 시 하이라이트 효과
class RetroHoverItem extends StatefulWidget {
const RetroHoverItem({
super.key,
required this.child,
this.onTap,
this.isSelected = false,
this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
});
final Widget child;
final VoidCallback? onTap;
final bool isSelected;
final EdgeInsets padding;
@override
State<RetroHoverItem> createState() => _RetroHoverItemState();
}
class _RetroHoverItemState extends State<RetroHoverItem> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
final isHighlighted = _isHovered || widget.isSelected;
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
cursor: widget.onTap != null
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 100),
padding: widget.padding,
decoration: BoxDecoration(
color: isHighlighted
? RetroColors.panelBgLight.withValues(alpha: 0.5)
: Colors.transparent,
border: isHighlighted
? Border(
left: BorderSide(
color: widget.isSelected
? RetroColors.gold
: RetroColors.gold.withValues(alpha: 0.5),
width: 2,
),
)
: null,
),
child: widget.child,
),
),
);
}
}
/// 레트로 호버 패널
/// 웹/데스크톱에서 패널 호버 시 미세 확대 및 글로우 효과
class RetroHoverPanel extends StatefulWidget {
const RetroHoverPanel({
super.key,
required this.child,
this.onTap,
this.enableScale = true,
});
final Widget child;
final VoidCallback? onTap;
final bool enableScale;
@override
State<RetroHoverPanel> createState() => _RetroHoverPanelState();
}
class _RetroHoverPanelState extends State<RetroHoverPanel> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
cursor: widget.onTap != null
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
transform: widget.enableScale && _isHovered
? Matrix4.diagonal3Values(1.01, 1.01, 1.0)
: Matrix4.identity(),
transformAlignment: Alignment.center,
decoration: BoxDecoration(
boxShadow: _isHovered
? [
BoxShadow(
color: RetroColors.gold.withValues(alpha: 0.2),
blurRadius: 12,
spreadRadius: 2,
),
]
: null,
),
child: widget.child,
),
),
);
}
}

View File

@@ -0,0 +1,303 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 레트로 스타일 다이얼로그 베이스 위젯
///
/// 8-bit RPG 스타일의 다이얼로그 프레임
class RetroDialog extends StatelessWidget {
const RetroDialog({
super.key,
required this.title,
required this.child,
this.titleIcon,
this.maxWidth = 500,
this.maxHeight = 600,
this.accentColor = RetroColors.gold,
});
final String title;
final Widget child;
final String? titleIcon;
final double maxWidth;
final double maxHeight;
final Color accentColor;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight),
decoration: BoxDecoration(
color: RetroColors.panelBg,
border: Border(
top: BorderSide(color: accentColor, width: 3),
left: BorderSide(color: accentColor, width: 3),
bottom: const BorderSide(color: RetroColors.panelBorderOuter, width: 3),
right: const BorderSide(color: RetroColors.panelBorderOuter, width: 3),
),
boxShadow: [
BoxShadow(
color: accentColor.withValues(alpha: 0.3),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 헤더 바
_buildHeader(context),
// 본문
Flexible(child: child),
],
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.2),
border: Border(
bottom: BorderSide(color: accentColor, width: 2),
),
),
child: Row(
children: [
if (titleIcon != null) ...[
Text(
titleIcon!,
style: TextStyle(fontSize: 14, color: accentColor),
),
const SizedBox(width: 8),
],
Expanded(
child: Text(
title.toUpperCase(),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: accentColor,
letterSpacing: 1,
),
),
),
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(color: RetroColors.textDisabled, width: 1),
),
child: const Text(
'X',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.textDisabled,
),
),
),
),
],
),
);
}
}
/// 레트로 스타일 탭 바
class RetroTabBar extends StatelessWidget {
const RetroTabBar({
super.key,
required this.controller,
required this.tabs,
this.accentColor = RetroColors.gold,
});
final TabController controller;
final List<String> tabs;
final Color accentColor;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: RetroColors.panelBorderOuter, width: 2),
),
),
child: TabBar(
controller: controller,
isScrollable: tabs.length > 3,
indicator: BoxDecoration(
color: accentColor.withValues(alpha: 0.3),
border: Border(
bottom: BorderSide(color: accentColor, width: 2),
),
),
labelColor: accentColor,
unselectedLabelColor: RetroColors.textDisabled,
labelStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
),
unselectedLabelStyle: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
),
dividerColor: Colors.transparent,
tabs: tabs.map((t) => Tab(text: t.toUpperCase())).toList(),
),
);
}
}
/// 레트로 스타일 섹션 헤더
class RetroSectionHeader extends StatelessWidget {
const RetroSectionHeader({
super.key,
required this.title,
this.icon,
this.accentColor = RetroColors.gold,
});
final String title;
final String? icon;
final Color accentColor;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
if (icon != null) ...[
Text(
icon!,
style: TextStyle(fontSize: 12, color: accentColor),
),
const SizedBox(width: 6),
],
Text(
title.toUpperCase(),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: accentColor,
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 2,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
accentColor,
accentColor.withValues(alpha: 0.3),
Colors.transparent,
],
),
),
),
),
],
),
);
}
}
/// 레트로 스타일 정보 박스
class RetroInfoBox extends StatelessWidget {
const RetroInfoBox({
super.key,
required this.content,
this.backgroundColor,
});
final String content;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: backgroundColor ?? RetroColors.deepBrown,
border: Border.all(color: RetroColors.panelBorderOuter, width: 1),
),
child: Text(
content,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textLight,
height: 1.8,
),
),
);
}
}
/// 레트로 스타일 통계 행
class RetroStatRow extends StatelessWidget {
const RetroStatRow({
super.key,
required this.label,
required this.value,
this.highlight = false,
this.highlightColor = RetroColors.gold,
});
final String label;
final String value;
final bool highlight;
final Color highlightColor;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textDisabled,
),
),
Container(
padding: highlight
? const EdgeInsets.symmetric(horizontal: 6, vertical: 2)
: null,
decoration: highlight
? BoxDecoration(
color: highlightColor.withValues(alpha: 0.2),
border: Border.all(color: highlightColor, width: 1),
)
: null,
child: Text(
value,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 9,
color: highlight ? highlightColor : RetroColors.textLight,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}

View File

@@ -4,5 +4,6 @@ library;
export 'pixel_border_painter.dart'; export 'pixel_border_painter.dart';
export 'retro_button.dart'; export 'retro_button.dart';
export 'retro_dialog.dart';
export 'retro_panel.dart'; export 'retro_panel.dart';
export 'retro_progress_bar.dart'; export 'retro_progress_bar.dart';