diff --git a/lib/src/core/animation/canvas/rarity_color_mapper.dart b/lib/src/core/animation/canvas/rarity_color_mapper.dart new file mode 100644 index 0000000..7b8fbb9 --- /dev/null +++ b/lib/src/core/animation/canvas/rarity_color_mapper.dart @@ -0,0 +1,19 @@ +import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/core/model/item_stats.dart'; + +/// 아이템 희귀도와 애니메이션 색상 간의 매핑 +/// +/// Clean Architecture 준수를 위해 model(ItemRarity) → animation(AsciiCellColor) +/// 의존성을 animation 레이어에서 처리 +extension ItemRarityColorMapper on ItemRarity { + /// 공격 이펙트 셀 색상 (Phase 9: 무기 등급별 이펙트) + /// + /// common은 기본 positive(시안), 나머지는 등급별 고유 색상 + AsciiCellColor get effectCellColor => switch (this) { + ItemRarity.common => AsciiCellColor.positive, + ItemRarity.uncommon => AsciiCellColor.rarityUncommon, + ItemRarity.rare => AsciiCellColor.rarityRare, + ItemRarity.epic => AsciiCellColor.rarityEpic, + ItemRarity.legendary => AsciiCellColor.rarityLegendary, + }; +} diff --git a/lib/src/shared/retro_theme_constants.dart b/lib/src/shared/retro_theme_constants.dart new file mode 100644 index 0000000..3554586 --- /dev/null +++ b/lib/src/shared/retro_theme_constants.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/src/shared/retro_colors.dart'; + +/// 레트로 테마 상수 모음 +/// +/// 패딩, 폰트 크기, 애니메이션 시간 등 UI 전반에 사용되는 상수 정의. +/// 색상은 [RetroColors] 참조. +abstract class RetroTheme { + RetroTheme._(); + + // =========================================================================== + // 패딩 (Padding/Spacing) + // =========================================================================== + + /// 4px - 아이콘 내부, 작은 요소 간격 + static const double paddingXs = 4.0; + + /// 8px - 텍스트 줄 간격, 버튼 내부 패딩 + static const double paddingSm = 8.0; + + /// 10px - 패널 내부 패딩 (소) + static const double paddingMd = 10.0; + + /// 12px - 섹션 간격, 패널 내부 패딩 (중) + static const double paddingLg = 12.0; + + /// 16px - 화면 여백, 패널 내부 패딩 (대) + static const double paddingXl = 16.0; + + /// 24px - 화면 간 여백 + static const double paddingXxl = 24.0; + + // =========================================================================== + // 폰트 크기 (Font Sizes) + // =========================================================================== + + /// 8px - 라벨, 작은 힌트 + static const double fontSizeXs = 8.0; + + /// 10px - 보조 텍스트, 상태 표시 + static const double fontSizeSm = 10.0; + + /// 11px - 툴팁, 버튼 라벨 + static const double fontSizeMd = 11.0; + + /// 12px - 본문 텍스트 + static const double fontSizeLg = 12.0; + + /// 14px - 부제목 + static const double fontSizeXl = 14.0; + + /// 18px - 제목 + static const double fontSizeTitle = 18.0; + + // =========================================================================== + // 애니메이션 시간 (Animation Durations) + // =========================================================================== + + /// 100ms - 버튼 눌림 효과 + static const Duration animFast = Duration(milliseconds: 100); + + /// 150ms - 호버 효과 + static const Duration animQuick = Duration(milliseconds: 150); + + /// 200ms - 리스트 아이템 전환 + static const Duration animNormal = Duration(milliseconds: 200); + + /// 300ms - 패널 전환 + static const Duration animMedium = Duration(milliseconds: 300); + + /// 400ms - 페이드 인/아웃 + static const Duration animSlow = Duration(milliseconds: 400); + + /// 500ms - 결과 패널 표시 + static const Duration animResult = Duration(milliseconds: 500); + + /// 1500ms - ASCII 디스인테그레이트 효과 + static const Duration animDisintegrate = Duration(milliseconds: 1500); + + // =========================================================================== + // 테두리 (Border) + // =========================================================================== + + /// 기본 테두리 두께 + static const double borderWidth = 1.0; + + /// 굵은 테두리 두께 + static const double borderWidthBold = 2.0; + + /// 기본 테두리 반경 (픽셀 스타일로 0 사용) + static const double borderRadius = 0.0; + + /// 라운드 테두리 반경 (현대적 스타일) + static const double borderRadiusRound = 4.0; + + // =========================================================================== + // 패널 스타일 (Panel Styles) + // =========================================================================== + + /// 패널 기본 EdgeInsets + static const EdgeInsets panelPadding = EdgeInsets.all(paddingMd); + + /// 패널 확장 EdgeInsets + static const EdgeInsets panelPaddingLarge = EdgeInsets.all(paddingLg); + + /// 화면 여백 EdgeInsets + static const EdgeInsets screenPadding = EdgeInsets.all(paddingXl); + + // =========================================================================== + // 텍스트 스타일 팩토리 (Text Style Factories) + // =========================================================================== + + /// 패널 제목 스타일 + static TextStyle panelTitle(BuildContext context) => TextStyle( + fontSize: fontSizeLg, + fontWeight: FontWeight.bold, + color: RetroColors.goldOf(context), + ); + + /// 섹션 제목 스타일 + static TextStyle sectionTitle(BuildContext context) => TextStyle( + fontSize: fontSizeMd, + fontWeight: FontWeight.bold, + color: RetroColors.textPrimaryOf(context), + ); + + /// 본문 텍스트 스타일 + static TextStyle bodyText(BuildContext context) => TextStyle( + fontSize: fontSizeLg, + color: RetroColors.textPrimaryOf(context), + ); + + /// 보조 텍스트 스타일 + static TextStyle bodyTextSecondary(BuildContext context) => TextStyle( + fontSize: fontSizeSm, + color: RetroColors.textSecondaryOf(context), + ); + + /// 뮤트 텍스트 스타일 + static TextStyle bodyTextMuted(BuildContext context) => TextStyle( + fontSize: fontSizeSm, + color: RetroColors.textMutedOf(context), + ); + + /// 라벨 텍스트 스타일 + static TextStyle labelText(BuildContext context) => TextStyle( + fontSize: fontSizeXs, + color: RetroColors.textSecondaryOf(context), + ); + + /// 숫자/통계 스타일 + static TextStyle statText(BuildContext context) => TextStyle( + fontSize: fontSizeSm, + fontWeight: FontWeight.bold, + color: RetroColors.textPrimaryOf(context), + ); + + // =========================================================================== + // 박스 데코레이션 팩토리 (Box Decoration Factories) + // =========================================================================== + + /// 기본 패널 데코레이션 + static BoxDecoration panelDecoration(BuildContext context) => BoxDecoration( + color: RetroColors.panelBgOf(context), + border: Border.all(color: RetroColors.borderOf(context)), + ); + + /// 강조 패널 데코레이션 (골드 테두리) + static BoxDecoration panelDecorationHighlight(BuildContext context) => + BoxDecoration( + color: RetroColors.panelBgOf(context), + border: Border.all(color: RetroColors.goldOf(context)), + ); + + /// 표면 데코레이션 (밝은 배경) + static BoxDecoration surfaceDecoration(BuildContext context) => BoxDecoration( + color: RetroColors.surfaceOf(context), + border: Border.all(color: RetroColors.borderOf(context)), + ); +} diff --git a/lib/src/shared/widgets/panel_header.dart b/lib/src/shared/widgets/panel_header.dart new file mode 100644 index 0000000..3cae04c --- /dev/null +++ b/lib/src/shared/widgets/panel_header.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/src/shared/retro_colors.dart'; +import 'package:asciineverdie/src/shared/retro_theme_constants.dart'; + +/// 패널 헤더 변형 +enum PanelHeaderVariant { + /// 기본 패널 헤더 (레트로 스타일, 골드 하단 테두리) + /// + /// 사용처: 메인 패널 상단, 골드 강조 필요한 곳 + primary, + + /// 섹션 헤더 (테마 색상 사용) + /// + /// 사용처: 리스트 섹션 구분, 탭 페이지 내부 + section, + + /// 장비 헤더 (3D 베벨 테두리) + /// + /// 사용처: 장비 패널, 중요 강조 영역 + equipment, +} + +/// 레트로 스타일 패널 헤더 위젯 +/// +/// 여러 화면에서 반복되는 패널/섹션 헤더를 통합한 공통 위젯. +/// [variant]에 따라 다른 스타일이 적용됨. +/// +/// 사용 예시: +/// ```dart +/// PanelHeader(title: 'INVENTORY') +/// PanelHeader(title: 'Stats', variant: PanelHeaderVariant.section) +/// ``` +class PanelHeader extends StatelessWidget { + const PanelHeader({ + required this.title, + this.variant = PanelHeaderVariant.primary, + this.icon, + this.uppercase = true, + super.key, + }); + + /// 헤더 제목 + final String title; + + /// 헤더 스타일 변형 + final PanelHeaderVariant variant; + + /// 선택적 아이콘 (equipment 변형에서 주로 사용) + final IconData? icon; + + /// 대문자 변환 여부 (기본 true) + final bool uppercase; + + @override + Widget build(BuildContext context) { + final displayTitle = uppercase ? title.toUpperCase() : title; + + return switch (variant) { + PanelHeaderVariant.primary => _buildPrimaryHeader(context, displayTitle), + PanelHeaderVariant.section => _buildSectionHeader(context, displayTitle), + PanelHeaderVariant.equipment => + _buildEquipmentHeader(context, displayTitle), + }; + } + + /// 기본 패널 헤더 (레트로 스타일) + Widget _buildPrimaryHeader(BuildContext context, String displayTitle) { + final gold = RetroColors.goldOf(context); + final panelBg = RetroColors.panelBgOf(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: panelBg, + border: Border(bottom: BorderSide(color: gold, width: 2)), + ), + child: Text( + displayTitle, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: RetroTheme.fontSizeXs, + fontWeight: FontWeight.bold, + color: gold, + ), + ), + ); + } + + /// 섹션 헤더 (테마 색상) + Widget _buildSectionHeader(BuildContext context, String displayTitle) { + final colorScheme = Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + color: colorScheme.primaryContainer, + child: Text( + displayTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: colorScheme.onPrimaryContainer, + ), + ), + ); + } + + /// 장비 헤더 (3D 베벨 테두리) + Widget _buildEquipmentHeader(BuildContext context, String displayTitle) { + final gold = RetroColors.goldOf(context); + final goldDark = RetroColors.goldDarkOf(context); + final panelBg = RetroColors.panelBgOf(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: panelBg, + border: Border( + top: BorderSide(color: gold, width: 2), + left: BorderSide(color: gold, width: 2), + bottom: BorderSide(color: goldDark, width: 2), + right: BorderSide(color: goldDark, width: 2), + ), + ), + child: Row( + children: [ + if (icon != null) ...[ + Icon(icon, size: 16, color: gold), + const SizedBox(width: 8), + ], + Text( + displayTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: gold, + ), + ), + ], + ), + ); + } +} + +/// 뮤트(회색) 섹션 헤더 +/// +/// game_play_screen.dart의 _buildSectionHeader와 동일한 스타일. +/// 메인 패널 내부의 작은 섹션 구분에 사용. +class MutedSectionHeader extends StatelessWidget { + const MutedSectionHeader({required this.title, super.key}); + + /// 헤더 제목 + final String title; + + @override + Widget build(BuildContext context) { + final textMuted = RetroColors.textMutedOf(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + title.toUpperCase(), + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: RetroTheme.fontSizeXs, + color: textMuted, + ), + ), + ); + } +}