From 27e05fb3c12418b1e3daa45c268d8598cf986ec8 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Tue, 30 Dec 2025 19:03:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=EA=B2=8C=EC=9E=84=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=EB=93=A4=20=EB=A0=88=ED=8A=B8=EB=A1=9C=20UI=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - death_overlay: 사망 화면 레트로 스타일로 재디자인 - help_dialog: RetroDialog 사용으로 통일 - hp_mp_bar: 레트로 프로그레스 바 스타일 적용 - notification_overlay: 레트로 패널 스타일 적용 - statistics_dialog: RetroDialog로 변경 --- .../features/game/widgets/death_overlay.dart | 464 +++++++++++------- .../features/game/widgets/help_dialog.dart | 232 +++------ lib/src/features/game/widgets/hp_mp_bar.dart | 204 +++++--- .../game/widgets/notification_overlay.dart | 228 ++++++--- .../game/widgets/statistics_dialog.dart | 219 +++------ 5 files changed, 742 insertions(+), 605 deletions(-) diff --git a/lib/src/features/game/widgets/death_overlay.dart b/lib/src/features/game/widgets/death_overlay.dart index e02d851..5dcecfe 100644 --- a/lib/src/features/game/widgets/death_overlay.dart +++ b/lib/src/features/game/widgets/death_overlay.dart @@ -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/model/combat_event.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; +import 'package:askiineverdie/src/shared/retro_colors.dart'; /// 사망 오버레이 위젯 (Phase 4) /// @@ -27,96 +28,161 @@ class DeathOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - return Material( - color: Colors.black87, + color: Colors.black.withValues(alpha: 0.9), child: Center( child: Container( - constraints: const BoxConstraints(maxWidth: 400), + constraints: const BoxConstraints(maxWidth: 420), margin: const EdgeInsets.all(24), - padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colorScheme.error.withValues(alpha: 0.5), - width: 2, + color: RetroColors.panelBg, + border: const Border( + top: BorderSide(color: RetroColors.hpRed, width: 4), + left: BorderSide(color: RetroColors.hpRed, width: 4), + bottom: BorderSide(color: RetroColors.panelBorderOuter, width: 4), + right: BorderSide(color: RetroColors.panelBorderOuter, width: 4), ), boxShadow: [ BoxShadow( - color: colorScheme.error.withValues(alpha: 0.3), - blurRadius: 20, + color: RetroColors.hpRed.withValues(alpha: 0.5), + blurRadius: 30, spreadRadius: 5, ), ], ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 사망 타이틀 - _buildDeathTitle(context), - const SizedBox(height: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 헤더 바 + Container( + width: double.infinity, + 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), - const SizedBox(height: 16), + // 캐릭터 정보 + _buildCharacterInfo(context), + const SizedBox(height: 16), - // 사망 원인 - _buildDeathCause(context), - const SizedBox(height: 24), + // 사망 원인 + _buildDeathCause(context), + const SizedBox(height: 20), - // 구분선 - Divider(color: colorScheme.outlineVariant), - const SizedBox(height: 16), + // 구분선 + _buildRetroDivider(), + const SizedBox(height: 16), - // 상실 정보 - _buildLossInfo(context), + // 상실 정보 + _buildLossInfo(context), - // 전투 로그 (있는 경우만 표시) - if (deathInfo.lastCombatEvents.isNotEmpty) ...[ - const SizedBox(height: 16), - Divider(color: colorScheme.outlineVariant), - const SizedBox(height: 8), - _buildCombatLog(context), - ], + // 전투 로그 (있는 경우만 표시) + if (deathInfo.lastCombatEvents.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildRetroDivider(), + const SizedBox(height: 8), + _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) { return Column( children: [ - // ASCII 스컬 - Text( - ' _____\n / \\\n| () () |\n \\ ^ /\n |||||', + // ASCII 스컬 (더 큰 버전) + const Text( + ' _____ \n' + ' / \\\n' + ' | () () |\n' + ' \\ ^ /\n' + ' ||||| ', style: TextStyle( fontFamily: 'JetBrainsMono', - fontSize: 14, - color: Theme.of(context).colorScheme.error, + fontSize: 12, + color: RetroColors.hpRed, height: 1.0, ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), + const SizedBox(height: 12), Text( - l10n.deathYouDied, - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.error, - letterSpacing: 4, + l10n.deathYouDied.toUpperCase(), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 14, + color: RetroColors.hpRed, + 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) { - final theme = Theme.of(context); - return Column( - children: [ - Text( - traits.name, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: RetroColors.panelBgLight.withValues(alpha: 0.5), + border: Border.all(color: RetroColors.panelBorderInner, width: 1), + ), + child: Column( + children: [ + Text( + traits.name, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 11, + color: RetroColors.gold, + ), ), - ), - const SizedBox(height: 4), - Text( - 'Level ${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}', - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + const SizedBox(height: 6), + Text( + 'Lv.${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}', + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + color: RetroColors.textLight, + ), ), - ), - ], + ], + ), ); } Widget _buildDeathCause(BuildContext context) { - final theme = Theme.of(context); final causeText = _getDeathCauseText(); return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: theme.colorScheme.errorContainer.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(8), + color: RetroColors.hpRedDark.withValues(alpha: 0.3), + border: Border.all(color: RetroColors.hpRed.withValues(alpha: 0.5)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.dangerous_outlined, - size: 20, - color: theme.colorScheme.error, + const Text( + '⚔', + style: TextStyle(fontSize: 14, color: RetroColors.hpRed), ), const SizedBox(width: 8), - Text( - causeText, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.error, + Flexible( + child: Text( + causeText, + 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) { - final theme = Theme.of(context); final hasLostItem = deathInfo.lostItemName != null; return Column( @@ -191,20 +269,18 @@ class DeathOverlay extends StatelessWidget { // 제물로 바친 아이템 표시 if (hasLostItem) ...[ Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: theme.colorScheme.errorContainer.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8), + color: RetroColors.hpRedDark.withValues(alpha: 0.2), border: Border.all( - color: theme.colorScheme.error.withValues(alpha: 0.3), + color: RetroColors.hpRed.withValues(alpha: 0.4), ), ), child: Row( children: [ - Icon( - Icons.local_fire_department, - size: 20, - color: theme.colorScheme.error, + const Text( + '🔥', + style: TextStyle(fontSize: 16), ), const SizedBox(width: 8), Expanded( @@ -212,17 +288,20 @@ class DeathOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - l10n.deathSacrificedToResurrect, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + l10n.deathSacrificedToResurrect.toUpperCase(), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textDisabled, ), ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( deathInfo.lostItemName!, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.error, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + color: RetroColors.hpRed, ), ), ], @@ -235,19 +314,19 @@ class DeathOverlay extends StatelessWidget { ] else ...[ _buildInfoRow( context, - icon: Icons.check_circle_outline, + asciiIcon: '✓', label: l10n.deathEquipment, value: l10n.deathNoSacrificeNeeded, - isNegative: false, + valueColor: RetroColors.expGreen, ), const SizedBox(height: 8), ], _buildInfoRow( context, - icon: Icons.monetization_on_outlined, + asciiIcon: '💰', label: l10n.deathCoinRemaining, value: _formatGold(deathInfo.goldAtDeath), - isNegative: false, + valueColor: RetroColors.gold, ), ], ); @@ -255,35 +334,36 @@ class DeathOverlay extends StatelessWidget { Widget _buildInfoRow( BuildContext context, { - required IconData icon, + required String asciiIcon, required String label, 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( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Icon(icon, size: 18, color: theme.colorScheme.onSurfaceVariant), + Text( + asciiIcon, + style: TextStyle(fontSize: 14, color: valueColor), + ), const SizedBox(width: 8), Text( label, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textDisabled, ), ), ], ), Text( value, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, color: valueColor, ), ), @@ -301,17 +381,48 @@ class DeathOverlay extends StatelessWidget { } Widget _buildResurrectButton(BuildContext context) { - final theme = Theme.of(context); - - return SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: onResurrect, - icon: const Icon(Icons.replay), - label: Text(l10n.deathResurrect), - style: FilledButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - padding: const EdgeInsets.symmetric(vertical: 16), + return GestureDetector( + onTap: onResurrect, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: RetroColors.expGreen.withValues(alpha: 0.2), + border: Border( + top: const BorderSide(color: RetroColors.expGreen, width: 3), + left: const BorderSide(color: RetroColors.expGreen, width: 3), + 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) { - final theme = Theme.of(context); final events = deathInfo.lastCombatEvents; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.deathCombatLog, - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - fontWeight: FontWeight.bold, - ), + Row( + children: [ + const Text( + '📜', + 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), Container( - constraints: const BoxConstraints(maxHeight: 120), + constraints: const BoxConstraints(maxHeight: 100), decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), + color: RetroColors.deepBrown, + border: Border.all(color: RetroColors.panelBorderOuter, width: 2), ), child: ListView.builder( shrinkWrap: true, - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(6), itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; @@ -355,22 +475,26 @@ class DeathOverlay extends StatelessWidget { /// 개별 전투 이벤트 타일 Widget _buildCombatEventTile(BuildContext context, CombatEvent event) { - final (icon, color, message) = _formatCombatEvent(event); + final (asciiIcon, color, message) = _formatCombatEvent(event); return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(vertical: 1), child: Row( children: [ - Icon(icon, size: 12, color: color), - const SizedBox(width: 6), + Text( + asciiIcon, + style: TextStyle(fontSize: 10, color: color), + ), + const SizedBox(width: 4), Expanded( child: Text( message, style: TextStyle( - fontSize: 11, - color: color, fontFamily: 'JetBrainsMono', + fontSize: 8, + color: color, ), + overflow: TextOverflow.ellipsis, ), ), ], @@ -378,75 +502,75 @@ class DeathOverlay extends StatelessWidget { ); } - /// 전투 이벤트를 아이콘, 색상, 메시지로 포맷 - (IconData, Color, String) _formatCombatEvent(CombatEvent event) { + /// 전투 이벤트를 ASCII 아이콘, 색상, 메시지로 포맷 + (String, Color, String) _formatCombatEvent(CombatEvent event) { final target = event.targetName ?? ''; return switch (event.type) { CombatEventType.playerAttack => ( - event.isCritical ? Icons.flash_on : Icons.local_fire_department, - event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300, + event.isCritical ? '⚡' : '⚔', + event.isCritical ? RetroColors.gold : RetroColors.expGreen, event.isCritical ? l10n.combatCritical(event.damage, target) : l10n.combatYouHit(target, event.damage), ), CombatEventType.monsterAttack => ( - Icons.dangerous, - Colors.red.shade300, + '💀', + RetroColors.hpRed, l10n.combatMonsterHitsYou(target, event.damage), ), CombatEventType.playerEvade => ( - Icons.directions_run, - Colors.cyan.shade300, + '➤', + RetroColors.asciiCyan, l10n.combatEvadedAttackFrom(target), ), CombatEventType.monsterEvade => ( - Icons.directions_run, - Colors.orange.shade300, + '➤', + const Color(0xFFFF9933), l10n.combatMonsterEvaded(target), ), CombatEventType.playerBlock => ( - Icons.shield, - Colors.blueGrey.shade300, + '🛡', + RetroColors.mpBlue, l10n.combatBlockedAttack(target, event.damage), ), CombatEventType.playerParry => ( - Icons.sports_kabaddi, - Colors.teal.shade300, + '⚔', + const Color(0xFF00CCCC), l10n.combatParriedAttack(target, event.damage), ), CombatEventType.playerSkill => ( - Icons.auto_fix_high, - Colors.purple.shade300, + '✧', + const Color(0xFF9966FF), l10n.combatSkillDamage(event.skillName ?? '', event.damage), ), CombatEventType.playerHeal => ( - Icons.healing, - Colors.green.shade300, + '♥', + RetroColors.expGreen, l10n.combatHealedFor(event.healAmount), ), CombatEventType.playerBuff => ( - Icons.trending_up, - Colors.lightBlue.shade300, + '↑', + RetroColors.mpBlue, l10n.combatBuffActivated(event.skillName ?? ''), ), CombatEventType.playerDebuff => ( - Icons.trending_down, - Colors.deepOrange.shade300, + '↓', + const Color(0xFFFF6633), l10n.combatDebuffApplied(event.skillName ?? '', target), ), CombatEventType.dotTick => ( - Icons.whatshot, - Colors.deepOrange.shade300, + '🔥', + const Color(0xFFFF6633), l10n.combatDotTick(event.skillName ?? '', event.damage), ), CombatEventType.playerPotion => ( - Icons.local_drink, - Colors.lightGreen.shade300, + '🧪', + RetroColors.expGreen, l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target), ), CombatEventType.potionDrop => ( - Icons.card_giftcard, - Colors.lime.shade300, + '🎁', + RetroColors.gold, l10n.combatPotionDrop(event.skillName ?? ''), ), }; diff --git a/lib/src/features/game/widgets/help_dialog.dart b/lib/src/features/game/widgets/help_dialog.dart index fddc356..60f58ca 100644 --- a/lib/src/features/game/widgets/help_dialog.dart +++ b/lib/src/features/game/widgets/help_dialog.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/src/shared/retro_colors.dart'; +import 'package:askiineverdie/src/shared/widgets/retro_dialog.dart'; + /// 도움말 다이얼로그 (Help Dialog) /// /// 게임 메카닉과 UI 설명을 제공 @@ -10,6 +13,7 @@ class HelpDialog extends StatefulWidget { static Future show(BuildContext context) { return showDialog( context: context, + barrierColor: Colors.black87, builder: (_) => const HelpDialog(), ); } @@ -36,113 +40,58 @@ class _HelpDialogState extends State @override Widget build(BuildContext context) { - final theme = Theme.of(context); final isKorean = Localizations.localeOf(context).languageCode == 'ko'; final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; - return Dialog( - child: Container( - constraints: const BoxConstraints(maxWidth: 500, maxHeight: 600), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 헤더 - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(28), - ), - ), - child: Row( - children: [ - Icon( - Icons.help_outline, - color: theme.colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - isKorean - ? '게임 도움말' - : isJapanese - ? 'ゲームヘルプ' - : '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( + final title = isKorean + ? '도움말' + : isJapanese + ? 'ヘルプ' + : 'Help'; + + final tabs = isKorean + ? ['기본', '전투', '스킬', 'UI'] + : isJapanese + ? ['基本', '戦闘', 'スキル', 'UI'] + : ['Basics', 'Combat', 'Skills', 'UI']; + + return RetroDialog( + title: title, + titleIcon: '❓', + accentColor: RetroColors.mpBlue, + child: Column( + children: [ + // 탭 바 + RetroTabBar( + controller: _tabController, + tabs: tabs, + accentColor: RetroColors.mpBlue, + ), + // 탭 내용 + Expanded( + child: TabBarView( controller: _tabController, - isScrollable: true, - tabs: [ - Tab( - text: isKorean - ? '기본' - : isJapanese - ? '基本' - : 'Basics', + children: [ + _BasicsHelpView( + isKorean: isKorean, + isJapanese: isJapanese, ), - Tab( - text: isKorean - ? '전투' - : isJapanese - ? '戦闘' - : 'Combat', + _CombatHelpView( + isKorean: isKorean, + isJapanese: isJapanese, ), - Tab( - text: isKorean - ? '스킬' - : isJapanese - ? 'スキル' - : 'Skills', + _SkillsHelpView( + isKorean: isKorean, + isJapanese: isJapanese, ), - Tab( - text: isKorean - ? 'UI' - : isJapanese - ? 'UI' - : 'UI', + _UIHelpView( + isKorean: isKorean, + isJapanese: isJapanese, ), ], ), - // 탭 내용 - 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 Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), children: [ _HelpSection( - icon: Icons.info_outline, + icon: 'ℹ', title: isKorean ? '게임 소개' : isJapanese @@ -179,9 +128,9 @@ class _BasicsHelpView extends StatelessWidget { : 'Askii Never Die is an idle RPG. Your character automatically fights monsters, ' 'completes quests, and levels up. You manage equipment and skills.', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.trending_up, + icon: '↑', title: isKorean ? '진행 방식' : isJapanese @@ -202,9 +151,9 @@ class _BasicsHelpView extends StatelessWidget { '• Complete quests → Get rewards\n' '• Progress plot → Unlock new Acts', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.save, + icon: '💾', title: isKorean ? '저장' : isJapanese @@ -237,10 +186,10 @@ class _CombatHelpView extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), children: [ _HelpSection( - icon: Icons.sports_mma, + icon: '⚔', title: isKorean ? '전투 시스템' : isJapanese @@ -255,9 +204,9 @@ class _CombatHelpView extends StatelessWidget { : 'Combat is automatic. Player and monster take turns attacking, ' 'with attack frequency based on Attack Speed.', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.shield, + icon: '🛡', title: isKorean ? '방어 메카닉' : isJapanese @@ -278,9 +227,9 @@ class _CombatHelpView extends StatelessWidget { '• Parry: Deflect some damage with weapon\n' '• DEF: Subtracted from all damage', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.favorite, + icon: '♥', title: isKorean ? '사망과 부활' : isJapanese @@ -313,10 +262,10 @@ class _SkillsHelpView extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), children: [ _HelpSection( - icon: Icons.auto_awesome, + icon: '✧', title: isKorean ? '스킬 종류' : isJapanese @@ -340,9 +289,9 @@ class _SkillsHelpView extends StatelessWidget { '• Debuff: Harmful effects on enemies\n' '• DOT: Damage over time', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.psychology, + icon: '🤖', title: isKorean ? '자동 스킬 선택' : isJapanese @@ -366,9 +315,9 @@ class _SkillsHelpView extends StatelessWidget { '3. Monster HP high → Apply debuffs\n' '4. Finish with attack skills', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.upgrade, + icon: '★', title: isKorean ? '스킬 랭크' : isJapanese @@ -410,10 +359,10 @@ class _UIHelpView extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), children: [ _HelpSection( - icon: Icons.view_column, + icon: '📺', title: isKorean ? '화면 구성' : isJapanese @@ -434,9 +383,9 @@ class _UIHelpView extends StatelessWidget { '• Center: Equipment, inventory\n' '• Right: Plot/quest progress, spellbook', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.speed, + icon: '⏩', title: isKorean ? '속도 조절' : isJapanese @@ -460,9 +409,9 @@ class _UIHelpView extends StatelessWidget { '• 5x: 5x speed\n' '• 10x: 10x speed', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.pause, + icon: '⏸', title: isKorean ? '일시정지' : isJapanese @@ -477,9 +426,9 @@ class _UIHelpView extends StatelessWidget { : 'Use the pause button to stop the game. ' 'You can still view UI and change settings while paused.', ), - const SizedBox(height: 16), + const SizedBox(height: 12), _HelpSection( - icon: Icons.bar_chart, + icon: '📊', title: isKorean ? '통계' : isJapanese @@ -499,7 +448,7 @@ class _UIHelpView extends StatelessWidget { } } -/// 도움말 섹션 위젯 +/// 레트로 스타일 도움말 섹션 위젯 class _HelpSection extends StatelessWidget { const _HelpSection({ required this.icon, @@ -507,46 +456,23 @@ class _HelpSection extends StatelessWidget { required this.content, }); - final IconData icon; + final String icon; final String title; final String content; @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 섹션 헤더 - Row( - children: [ - Icon(icon, size: 20, color: theme.colorScheme.primary), - const SizedBox(width: 8), - Text( - title, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - ), - ], + RetroSectionHeader( + title: title, + icon: icon, + accentColor: RetroColors.mpBlue, ), - const SizedBox(height: 8), // 내용 - Container( - 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, - ), - ), - ), + RetroInfoBox(content: content), ], ); } diff --git a/lib/src/features/game/widgets/hp_mp_bar.dart b/lib/src/features/game/widgets/hp_mp_bar.dart index 75d8c1b..2778291 100644 --- a/lib/src/features/game/widgets/hp_mp_bar.dart +++ b/lib/src/features/game/widgets/hp_mp_bar.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; 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/MP 변화 시 색상 플래시 + 변화량 표시 /// - 전투 중 몬스터 HP 바 표시 @@ -158,8 +160,12 @@ class _HpMpBarState extends State with TickerProviderStateMixin { widget.monsterHpMax != null && widget.monsterHpMax! > 0; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: RetroColors.panelBg, + border: Border.all(color: RetroColors.panelBorderOuter, width: 2), + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -169,13 +175,14 @@ class _HpMpBarState extends State with TickerProviderStateMixin { current: widget.hpCurrent, max: widget.hpMax, ratio: hpRatio, - color: Colors.red, + fillColor: RetroColors.hpRed, + emptyColor: RetroColors.hpRedDark, isLow: hpRatio < 0.2 && hpRatio > 0, flashController: _hpFlashAnimation, change: _hpChange, isDamage: _hpDamage, ), - const SizedBox(height: 4), + const SizedBox(height: 6), // MP 바 (플래시 효과 포함) _buildAnimatedBar( @@ -183,7 +190,8 @@ class _HpMpBarState extends State with TickerProviderStateMixin { current: widget.mpCurrent, max: widget.mpMax, ratio: mpRatio, - color: Colors.blue, + fillColor: RetroColors.mpBlue, + emptyColor: RetroColors.mpBlueDark, isLow: false, flashController: _mpFlashAnimation, change: _mpChange, @@ -202,7 +210,8 @@ class _HpMpBarState extends State with TickerProviderStateMixin { required int current, required int max, required double ratio, - required Color color, + required Color fillColor, + required Color emptyColor, required bool isLow, required Animation flashController, required int change, @@ -213,27 +222,28 @@ class _HpMpBarState extends State with TickerProviderStateMixin { builder: (context, child) { // 플래시 색상 (데미지=빨강, 회복=녹색) final flashColor = isDamage - ? Colors.red.withValues(alpha: flashController.value * 0.4) - : Colors.green.withValues(alpha: flashController.value * 0.4); + ? RetroColors.hpRed.withValues(alpha: flashController.value * 0.4) + : RetroColors.expGreen.withValues(alpha: flashController.value * 0.4); // 위험 깜빡임 배경 final lowBgColor = isLow - ? Colors.red.withValues(alpha: (1 - _blinkAnimation.value) * 0.3) + ? RetroColors.hpRed.withValues(alpha: (1 - _blinkAnimation.value) * 0.3) : Colors.transparent; return Container( decoration: BoxDecoration( color: flashController.value > 0.1 ? flashColor : lowBgColor, - borderRadius: BorderRadius.circular(4), ), child: Stack( children: [ - _buildBar( + _buildRetroBar( label: label, current: current, max: max, ratio: ratio, - color: color, + fillColor: fillColor, + emptyColor: emptyColor, + blinkOpacity: isLow ? _blinkAnimation.value : 1.0, ), // 플로팅 변화량 텍스트 (위로 떠오르며 사라짐) @@ -250,9 +260,10 @@ class _HpMpBarState extends State with TickerProviderStateMixin { child: Text( change > 0 ? '+$change' : '$change', style: TextStyle( - fontSize: 12, + fontFamily: 'PressStart2P', + fontSize: 8, fontWeight: FontWeight.bold, - color: isDamage ? Colors.red : Colors.green, + color: isDamage ? RetroColors.hpRed : RetroColors.expGreen, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), Shadow(color: Colors.black, blurRadius: 6), @@ -269,40 +280,81 @@ class _HpMpBarState extends State with TickerProviderStateMixin { ); } - Widget _buildBar({ + /// 레트로 스타일 세그먼트 바 + Widget _buildRetroBar({ required String label, required int current, required int max, 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( children: [ + // 레이블 (HP/MP) SizedBox( - width: 24, + width: 28, child: Text( label, - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), - ), - ), - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(2), - child: LinearProgressIndicator( - value: ratio.clamp(0.0, 1.0), - backgroundColor: color.withValues(alpha: 0.2), - valueColor: AlwaysStoppedAnimation(color), - minHeight: 10, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + fontWeight: FontWeight.bold, + color: RetroColors.gold.withValues(alpha: blinkOpacity), ), ), ), - const SizedBox(width: 4), - // Flexible로 오버플로우 방지 - Flexible( - flex: 0, + // 세그먼트 바 + Expanded( + child: Container( + 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( '$current/$max', - style: const TextStyle(fontSize: 9), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textLight, + ), textAlign: TextAlign.right, overflow: TextOverflow.ellipsis, ), @@ -311,53 +363,90 @@ class _HpMpBarState extends State with TickerProviderStateMixin { ); } - /// 몬스터 HP 바 + /// 몬스터 HP 바 (레트로 스타일) Widget _buildMonsterBar() { final max = widget.monsterHpMax!; final ratio = max > 0 ? widget.monsterHpCurrent! / max : 0.0; + const segmentCount = 10; + final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round(); return AnimatedBuilder( animation: _monsterFlashAnimation, builder: (context, child) { // 데미지 플래시 (몬스터는 항상 데미지를 받음) - final flashColor = Colors.yellow.withValues( + final flashColor = RetroColors.gold.withValues( alpha: _monsterFlashAnimation.value * 0.3, ); return Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: _monsterFlashAnimation.value > 0.1 ? flashColor - : Colors.orange.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), + : RetroColors.panelBgLight.withValues(alpha: 0.5), + border: Border.all( + color: RetroColors.gold.withValues(alpha: 0.6), + width: 1, + ), ), child: Stack( clipBehavior: Clip.none, children: [ - // HP 바만 표시 (공간 제약으로 아이콘/이름 생략) Row( children: [ - // HP 바 + // 몬스터 아이콘 + const Icon( + Icons.pest_control, + size: 12, + color: RetroColors.gold, + ), + const SizedBox(width: 6), + // 세그먼트 HP 바 Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(2), - child: LinearProgressIndicator( - value: ratio.clamp(0.0, 1.0), - backgroundColor: Colors.orange.withValues(alpha: 0.2), - valueColor: const AlwaysStoppedAnimation( - Colors.orange, + child: Container( + height: 10, + decoration: BoxDecoration( + color: RetroColors.hpRedDark.withValues(alpha: 0.3), + border: Border.all( + color: RetroColors.panelBorderOuter, + 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 퍼센트 Text( '${(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 with TickerProviderStateMixin { // 플로팅 데미지 텍스트 if (_monsterHpChange != 0 && _monsterFlashAnimation.value > 0.05) Positioned( - right: 60, - top: -5, + right: 50, + top: -8, child: Transform.translate( offset: Offset(0, -12 * (1 - _monsterFlashAnimation.value)), child: Opacity( @@ -376,11 +465,12 @@ class _HpMpBarState extends State with TickerProviderStateMixin { ? '+$_monsterHpChange' : '$_monsterHpChange', style: TextStyle( - fontSize: 11, + fontFamily: 'PressStart2P', + fontSize: 7, fontWeight: FontWeight.bold, color: _monsterHpChange < 0 - ? Colors.yellow - : Colors.green, + ? RetroColors.gold + : RetroColors.expGreen, shadows: const [ Shadow(color: Colors.black, blurRadius: 3), Shadow(color: Colors.black, blurRadius: 6), diff --git a/lib/src/features/game/widgets/notification_overlay.dart b/lib/src/features/game/widgets/notification_overlay.dart index e37690e..0354266 100644 --- a/lib/src/features/game/widgets/notification_overlay.dart +++ b/lib/src/features/game/widgets/notification_overlay.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:askiineverdie/src/core/notification/notification_service.dart'; +import 'package:askiineverdie/src/shared/retro_colors.dart'; /// 알림 오버레이 위젯 (Phase 8: 팝업/토스트 알림) /// @@ -106,7 +107,7 @@ class _NotificationOverlayState extends State } } -/// 알림 카드 위젯 +/// 레트로 스타일 알림 카드 위젯 class _NotificationCard extends StatelessWidget { const _NotificationCard({ required this.notification, @@ -118,118 +119,191 @@ class _NotificationCard extends StatelessWidget { @override Widget build(BuildContext context) { - final (bgColor, icon, iconColor) = _getStyleForType(notification.type); + final (accentColor, icon, asciiIcon) = _getStyleForType(notification.type); - return Material( - elevation: 8, - borderRadius: BorderRadius.circular(12), - color: bgColor, - child: InkWell( - onTap: onDismiss, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - // 아이콘 - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: iconColor.withValues(alpha: 0.2), - shape: BoxShape.circle, - ), - child: Icon(icon, color: iconColor, size: 24), - ), - const SizedBox(width: 12), - // 텍스트 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - notification.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, + return GestureDetector( + onTap: onDismiss, + child: Container( + 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.4), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 헤더 바 + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + color: accentColor.withValues(alpha: 0.3), + child: Row( + children: [ + // ASCII 아이콘 + Text( + asciiIcon, + style: TextStyle( + 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( - notification.subtitle!, - style: TextStyle( - fontSize: 13, - color: Colors.white.withValues(alpha: 0.8), - ), + ), + // 닫기 버튼 + GestureDetector( + onTap: onDismiss, + child: const Text( + '[X]', + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + color: RetroColors.textDisabled, ), - ], - ], - ), + ), + ), + ], ), - // 닫기 버튼 - IconButton( - icon: const Icon(Icons.close, color: Colors.white70, size: 20), - onPressed: onDismiss, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + // 본문 + Padding( + padding: const EdgeInsets.all(12), + child: Row( + 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) { NotificationType.levelUp => ( - const Color(0xFF1565C0), - Icons.trending_up, - Colors.amber, + RetroColors.gold, + Icons.arrow_upward, + '★', ), NotificationType.questComplete => ( - const Color(0xFF2E7D32), - Icons.check_circle, - Colors.lightGreen, + RetroColors.expGreen, + Icons.check, + '☑', ), NotificationType.actComplete => ( - const Color(0xFF6A1B9A), + RetroColors.mpBlue, Icons.flag, - Colors.purpleAccent, + '⚑', ), NotificationType.newSpell => ( - const Color(0xFF4527A0), + const Color(0xFF9966FF), Icons.auto_fix_high, - Colors.deepPurpleAccent, + '✧', ), NotificationType.newEquipment => ( - const Color(0xFFE65100), + const Color(0xFFFF9933), Icons.shield, - Colors.orange, + '⚔', ), NotificationType.bossDefeat => ( - const Color(0xFFC62828), + RetroColors.hpRed, Icons.whatshot, - Colors.redAccent, + '☠', ), NotificationType.gameSaved => ( - const Color(0xFF00695C), + RetroColors.expGreen, Icons.save, - Colors.tealAccent, + '💾', ), NotificationType.info => ( - const Color(0xFF0277BD), + RetroColors.mpBlue, Icons.info_outline, - Colors.lightBlueAccent, + 'ℹ', ), NotificationType.warning => ( - const Color(0xFFF57C00), - Icons.warning_amber, - Colors.amber, + const Color(0xFFFFCC00), + Icons.warning, + '⚠', ), }; } + + /// 알림 타입 라벨 + 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', + }; + } } diff --git a/lib/src/features/game/widgets/statistics_dialog.dart b/lib/src/features/game/widgets/statistics_dialog.dart index b199398..5578d41 100644 --- a/lib/src/features/game/widgets/statistics_dialog.dart +++ b/lib/src/features/game/widgets/statistics_dialog.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.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) /// @@ -23,6 +25,7 @@ class StatisticsDialog extends StatefulWidget { }) { return showDialog( context: context, + barrierColor: Colors.black87, builder: (_) => StatisticsDialog( session: session, cumulative: cumulative, @@ -52,84 +55,46 @@ class _StatisticsDialogState extends State @override Widget build(BuildContext context) { - final theme = Theme.of(context); final isKorean = Localizations.localeOf(context).languageCode == 'ko'; final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; - return Dialog( - child: Container( - constraints: const BoxConstraints(maxWidth: 400, maxHeight: 500), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 헤더 - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(28), - ), - ), - child: Row( - children: [ - Icon( - Icons.bar_chart, - color: theme.colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - isKorean - ? '게임 통계' - : isJapanese - ? 'ゲーム統計' - : 'Game Statistics', - 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( + final title = isKorean + ? '통계' + : isJapanese + ? '統計' + : 'Statistics'; + + final tabs = isKorean + ? ['세션', '누적'] + : isJapanese + ? ['セッション', '累積'] + : ['Session', 'Total']; + + return RetroDialog( + title: title, + titleIcon: '📊', + maxWidth: 420, + maxHeight: 520, + accentColor: RetroColors.gold, + child: Column( + children: [ + // 탭 바 + RetroTabBar( + controller: _tabController, + tabs: tabs, + accentColor: RetroColors.gold, + ), + // 탭 내용 + Expanded( + child: TabBarView( controller: _tabController, - tabs: [ - Tab( - text: isKorean - ? '현재 세션' - : isJapanese - ? '現在のセッション' - : 'Session', - ), - Tab( - text: isKorean - ? '누적 통계' - : isJapanese - ? '累積統計' - : 'Cumulative', - ), + children: [ + _SessionStatisticsView(stats: widget.session), + _CumulativeStatisticsView(stats: widget.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'; return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), children: [ _StatSection( title: isKorean @@ -155,7 +120,7 @@ class _SessionStatisticsView extends StatelessWidget { : isJapanese ? '戦闘' : 'Combat', - icon: Icons.sports_mma, + icon: '⚔', items: [ _StatItem( label: isKorean @@ -191,14 +156,14 @@ class _SessionStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '데미지' : isJapanese ? 'ダメージ' : 'Damage', - icon: Icons.flash_on, + icon: '⚡', items: [ _StatItem( label: isKorean @@ -226,14 +191,14 @@ class _SessionStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '스킬' : isJapanese ? 'スキル' : 'Skills', - icon: Icons.auto_awesome, + icon: '✧', items: [ _StatItem( label: isKorean @@ -269,14 +234,14 @@ class _SessionStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '경제' : isJapanese ? '経済' : 'Economy', - icon: Icons.monetization_on, + icon: '💰', items: [ _StatItem( label: isKorean @@ -312,14 +277,14 @@ class _SessionStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '진행' : isJapanese ? '進行' : 'Progress', - icon: Icons.trending_up, + icon: '↑', items: [ _StatItem( label: isKorean @@ -356,7 +321,7 @@ class _CumulativeStatisticsView extends StatelessWidget { final isJapanese = Localizations.localeOf(context).languageCode == 'ja'; return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), children: [ _StatSection( title: isKorean @@ -364,7 +329,7 @@ class _CumulativeStatisticsView extends StatelessWidget { : isJapanese ? '記録' : 'Records', - icon: Icons.emoji_events, + icon: '🏆', items: [ _StatItem( label: isKorean @@ -395,14 +360,14 @@ class _CumulativeStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '총 플레이' : isJapanese ? '総プレイ' : 'Total Play', - icon: Icons.access_time, + icon: '⏱', items: [ _StatItem( label: isKorean @@ -438,14 +403,14 @@ class _CumulativeStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '총 전투' : isJapanese ? '総戦闘' : 'Total Combat', - icon: Icons.sports_mma, + icon: '⚔', items: [ _StatItem( label: isKorean @@ -481,14 +446,14 @@ class _CumulativeStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '총 데미지' : isJapanese ? '総ダメージ' : 'Total Damage', - icon: Icons.flash_on, + icon: '⚡', items: [ _StatItem( label: isKorean @@ -508,14 +473,14 @@ class _CumulativeStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '총 스킬' : isJapanese ? '総スキル' : 'Total Skills', - icon: Icons.auto_awesome, + icon: '✧', items: [ _StatItem( label: isKorean @@ -535,14 +500,14 @@ class _CumulativeStatisticsView extends StatelessWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), _StatSection( title: isKorean ? '총 경제' : isJapanese ? '総経済' : 'Total Economy', - icon: Icons.monetization_on, + icon: '💰', items: [ _StatItem( label: isKorean @@ -591,7 +556,7 @@ class _CumulativeStatisticsView extends StatelessWidget { } } -/// 통계 섹션 위젯 +/// 레트로 스타일 통계 섹션 위젯 class _StatSection extends StatelessWidget { const _StatSection({ required this.title, @@ -600,31 +565,20 @@ class _StatSection extends StatelessWidget { }); final String title; - final IconData icon; + final String icon; final List<_StatItem> items; @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 섹션 헤더 - Row( - children: [ - Icon(icon, size: 18, color: theme.colorScheme.primary), - const SizedBox(width: 8), - Text( - title, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - ), - ], + RetroSectionHeader( + title: title, + icon: icon, + accentColor: RetroColors.gold, ), - const Divider(height: 8), // 통계 항목들 ...items, ], @@ -632,7 +586,7 @@ class _StatSection extends StatelessWidget { } } -/// 개별 통계 항목 위젯 +/// 레트로 스타일 개별 통계 항목 위젯 class _StatItem extends StatelessWidget { const _StatItem({ required this.label, @@ -646,42 +600,11 @@ class _StatItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - 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, - ), - ), - ), - ], - ), + return RetroStatRow( + label: label, + value: value, + highlight: highlight, + highlightColor: RetroColors.gold, ); } }