From 06f76e136491500d63b60db7e1677c30dd02d065 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Tue, 30 Dec 2025 19:04:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EB=A0=88=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20UI=20=EC=A0=84=EB=A9=B4=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RetroPanel, RetroButton 사용으로 통일 - 스탯 표시 레트로 스타일 적용 - 종족/직업 선택 UI 개선 - 전체 레이아웃 레트로 RPG 느낌으로 변경 --- .../new_character/new_character_screen.dart | 597 +++++++++++------- 1 file changed, 362 insertions(+), 235 deletions(-) diff --git a/lib/src/features/new_character/new_character_screen.dart b/lib/src/features/new_character/new_character_screen.dart index d98d9e9..f8b638e 100644 --- a/lib/src/features/new_character/new_character_screen.dart +++ b/lib/src/features/new_character/new_character_screen.dart @@ -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/util/pq_logic.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 포팅) class NewCharacterScreen extends StatefulWidget { @@ -268,9 +270,19 @@ class _NewCharacterScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: RetroColors.deepBrown, 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, + iconTheme: const IconThemeData(color: RetroColors.gold), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), @@ -286,9 +298,13 @@ class _NewCharacterScreenState extends State { const SizedBox(height: 16), // 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션) - Center( - child: RacePreview( - raceId: _races[_selectedRaceIndex].raceId, + RetroPanel( + title: 'PREVIEW', + padding: const EdgeInsets.all(8), + child: Center( + child: RacePreview( + raceId: _races[_selectedRaceIndex].raceId, + ), ), ), const SizedBox(height: 16), @@ -309,13 +325,10 @@ class _NewCharacterScreenState extends State { const SizedBox(height: 24), // Sold! 버튼 - FilledButton.icon( + RetroTextButton( + text: L10n.of(context).soldButton, + icon: Icons.check, 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 { Widget _buildNameSection() { final l10n = L10n.of(context); - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _nameController, - decoration: InputDecoration( - labelText: l10n.name, - border: const OutlineInputBorder(), - ), - maxLength: 30, + return RetroPanel( + title: 'NAME', + child: Row( + children: [ + Expanded( + child: TextField( + controller: _nameController, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 10, + color: RetroColors.textLight, ), + 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( - onPressed: _onGenerateName, - icon: const Icon(Icons.casino), - tooltip: l10n.generateName, - ), - ], - ), + ), + const SizedBox(width: 8), + RetroIconButton( + icon: Icons.casino, + onPressed: _onGenerateName, + ), + ], ), ); } Widget _buildStatsSection() { final l10n = L10n.of(context); - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(l10n.stats, style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 12), + return RetroPanel( + title: 'STATS', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 스탯 그리드 + Row( + 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), - // 스탯 그리드 - Row( - children: [ - Expanded(child: _buildStatTile(l10n.statStr, _str)), - Expanded(child: _buildStatTile(l10n.statCon, _con)), - Expanded(child: _buildStatTile(l10n.statDex, _dex)), - ], + // Total + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _getTotalColor().withValues(alpha: 0.2), + border: Border.all(color: _getTotalColor(), width: 2), ), - const SizedBox(height: 8), - Row( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildStatTile(l10n.statInt, _int)), - Expanded(child: _buildStatTile(l10n.statWis, _wis)), - Expanded(child: _buildStatTile(l10n.statCha, _cha)), - ], - ), - const SizedBox(height: 12), - - // 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, + Text( + l10n.total.toUpperCase(), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + fontWeight: FontWeight.bold, + color: RetroColors.textLight, ), ), - FilledButton.icon( - onPressed: _onReroll, - icon: const Icon(Icons.casino), - label: Text(l10n.roll), + Text( + '$_total', + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 14, + fontWeight: FontWeight.bold, + color: _getTotalColor(), + ), ), ], ), - if (_rollHistory.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8), + ), + const SizedBox(height: 12), + + // 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( game_l10n.uiRollHistory(_rollHistory.length), - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textDisabled, + ), ), ), - ], - ), + ), + ], ), ); } @@ -452,16 +481,28 @@ class _NewCharacterScreenState extends State { margin: const EdgeInsets.all(4), padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), + color: RetroColors.panelBgLight, + border: Border.all(color: RetroColors.panelBorderInner), ), child: Column( 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), Text( '$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 { } Widget _buildRaceSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.of(context).race, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SizedBox( - height: 300, - child: ListView.builder( - controller: _raceScrollController, - itemCount: _races.length, - itemBuilder: (context, index) { - final isSelected = index == _selectedRaceIndex; - final race = _races[index]; - return ListTile( - leading: Icon( - isSelected - ? Icons.radio_button_checked - : Icons.radio_button_unchecked, - color: isSelected - ? Theme.of(context).colorScheme.primary - : null, + return RetroPanel( + title: 'RACE', + child: SizedBox( + height: 300, + child: ListView.builder( + controller: _raceScrollController, + itemCount: _races.length, + itemBuilder: (context, index) { + final isSelected = index == _selectedRaceIndex; + final race = _races[index]; + return GestureDetector( + onTap: () => setState(() => _selectedRaceIndex = index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? RetroColors.panelBgLight : null, + border: isSelected + ? Border.all(color: RetroColors.gold, width: 1) + : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + 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( - 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), - ); - }, + if (isSelected) _buildRaceInfo(race), + ], + ), ), - ), - ], + ); + }, ), ), ); @@ -531,22 +580,23 @@ class _NewCharacterScreenState extends State { ? race.passives.map((p) => _translateRacePassive(p)).join(', ') : ''; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (statMods.isNotEmpty) - Text( - statMods.join(', '), - style: Theme.of(context).textTheme.bodySmall, - ), - if (passiveDesc.isNotEmpty) - Text( - passiveDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.primary, + return Padding( + padding: const EdgeInsets.only(left: 16, top: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (statMods.isNotEmpty) + Text( + statMods.join(', '), + style: const TextStyle(fontSize: 9, color: RetroColors.textLight), ), - ), - ], + if (passiveDesc.isNotEmpty) + Text( + passiveDesc, + style: const TextStyle(fontSize: 9, color: RetroColors.expGreen), + ), + ], + ), ); } @@ -576,51 +626,59 @@ class _NewCharacterScreenState extends State { } Widget _buildKlassSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.of(context).classTitle, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SizedBox( - height: 300, - child: ListView.builder( - controller: _klassScrollController, - itemCount: _klasses.length, - itemBuilder: (context, index) { - final isSelected = index == _selectedKlassIndex; - final klass = _klasses[index]; - return ListTile( - leading: Icon( - isSelected - ? Icons.radio_button_checked - : Icons.radio_button_unchecked, - color: isSelected - ? Theme.of(context).colorScheme.primary - : null, + return RetroPanel( + title: 'CLASS', + child: SizedBox( + height: 300, + child: ListView.builder( + controller: _klassScrollController, + itemCount: _klasses.length, + itemBuilder: (context, index) { + final isSelected = index == _selectedKlassIndex; + final klass = _klasses[index]; + return GestureDetector( + onTap: () => setState(() => _selectedKlassIndex = index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? RetroColors.panelBgLight : null, + border: isSelected + ? Border.all(color: RetroColors.gold, width: 1) + : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + 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( - 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), - ); - }, + if (isSelected) _buildClassInfo(klass), + ], + ), ), - ), - ], + ); + }, ), ), ); @@ -638,22 +696,23 @@ class _NewCharacterScreenState extends State { ? klass.passives.map((p) => _translateClassPassive(p)).join(', ') : ''; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (statMods.isNotEmpty) - Text( - statMods.join(', '), - style: Theme.of(context).textTheme.bodySmall, - ), - if (passiveDesc.isNotEmpty) - Text( - passiveDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.secondary, + return Padding( + padding: const EdgeInsets.only(left: 16, top: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (statMods.isNotEmpty) + Text( + statMods.join(', '), + style: const TextStyle(fontSize: 9, color: RetroColors.textLight), ), - ), - ], + if (passiveDesc.isNotEmpty) + Text( + passiveDesc, + style: const TextStyle(fontSize: 9, color: RetroColors.expGreen), + ), + ], + ), ); } @@ -678,13 +737,81 @@ class _NewCharacterScreenState extends State { /// 테스트 모드 토글 위젯 Widget _buildTestModeToggle() { - return Card( - child: SwitchListTile( - title: Text(game_l10n.uiTestMode), - subtitle: Text(game_l10n.uiTestModeDesc), - value: _testModeEnabled, - onChanged: (value) => setState(() => _testModeEnabled = value), - secondary: const Icon(Icons.phone_android), + return RetroPanel( + title: 'OPTIONS', + child: GestureDetector( + onTap: () => setState(() => _testModeEnabled = !_testModeEnabled), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + 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, + ), + ], + ), + ), + ], + ), + ), ), ); }