fix(ui): 모든 화면에 SafeArea 적용
- new_character_screen: SafeArea(top: false) 추가 - mobile_carousel_layout: SafeArea(top: false) 추가 - hall_of_fame_screen: SafeArea(top: false) 추가 - 안드로이드 네비게이션 바에 UI가 가려지는 문제 해결
This commit is contained in:
@@ -1,21 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:askiineverdie/src/core/notification/notification_service.dart';
|
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/character_sheet_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/character_sheet_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/combat_log_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/combat_log_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/equipment_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/equipment_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/inventory_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/inventory_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/quest_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/quest_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/skills_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/skills_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/pages/story_page.dart';
|
import 'package:asciineverdie/src/features/game/pages/story_page.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/carousel_nav_bar.dart';
|
import 'package:asciineverdie/src/features/game/widgets/carousel_nav_bar.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/combat_log.dart';
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/enhanced_animation_panel.dart';
|
import 'package:asciineverdie/src/features/game/widgets/enhanced_animation_panel.dart';
|
||||||
import 'package:askiineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
/// 모바일 캐로셀 레이아웃
|
/// 모바일 캐로셀 레이아웃
|
||||||
///
|
///
|
||||||
@@ -159,11 +159,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThemeOption(
|
Widget _buildThemeOption(BuildContext context, ThemeMode mode, String label) {
|
||||||
BuildContext context,
|
|
||||||
ThemeMode mode,
|
|
||||||
String label,
|
|
||||||
) {
|
|
||||||
final isSelected = widget.currentThemeMode == mode;
|
final isSelected = widget.currentThemeMode == mode;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
@@ -371,188 +367,189 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 헤더 (레트로 스타일)
|
// 헤더 (레트로 스타일)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: surface,
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(color: gold, width: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'OPTIONS',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 12,
|
|
||||||
color: gold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 일시정지/재개
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
widget.isPaused ? Icons.play_arrow : Icons.pause,
|
|
||||||
color: widget.isPaused ? Colors.green : Colors.orange,
|
|
||||||
),
|
|
||||||
title: Text(widget.isPaused ? l10n.menuResume : l10n.menuPause),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
widget.onPauseToggle();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 속도 조절
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.speed),
|
|
||||||
title: Text(l10n.menuSpeed),
|
|
||||||
trailing: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
color: surface,
|
||||||
borderRadius: BorderRadius.circular(12),
|
border: Border(bottom: BorderSide(color: gold, width: 2)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${widget.speedMultiplier}x',
|
'OPTIONS',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontFamily: 'PressStart2P',
|
||||||
color: Theme.of(context).colorScheme.primary,
|
fontSize: 12,
|
||||||
|
color: gold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
widget.onSpeedCycle();
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(),
|
// 일시정지/재개
|
||||||
|
|
||||||
// 통계
|
|
||||||
if (widget.onShowStatistics != null)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.bar_chart, color: Colors.blue),
|
|
||||||
title: Text(l10n.uiStatistics),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
widget.onShowStatistics?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 도움말
|
|
||||||
if (widget.onShowHelp != null)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.help_outline, color: Colors.green),
|
|
||||||
title: Text(l10n.uiHelp),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
widget.onShowHelp?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(),
|
|
||||||
|
|
||||||
// 언어 변경
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.language, color: Colors.teal),
|
|
||||||
title: Text(l10n.menuLanguage),
|
|
||||||
trailing: Text(
|
|
||||||
_getCurrentLanguageName(),
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showLanguageDialog(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 테마 변경
|
|
||||||
if (widget.onThemeModeChange != null)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
_getThemeIcon(),
|
widget.isPaused ? Icons.play_arrow : Icons.pause,
|
||||||
color: Colors.purple,
|
color: widget.isPaused ? Colors.green : Colors.orange,
|
||||||
),
|
|
||||||
title: Text(l10n.menuTheme),
|
|
||||||
trailing: Text(
|
|
||||||
_getCurrentThemeName(),
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
),
|
||||||
|
title: Text(widget.isPaused ? l10n.menuResume : l10n.menuPause),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showThemeDialog(context);
|
widget.onPauseToggle();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 사운드 설정
|
// 속도 조절
|
||||||
if (widget.onBgmVolumeChange != null ||
|
|
||||||
widget.onSfxVolumeChange != null)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: const Icon(Icons.speed),
|
||||||
widget.bgmVolume == 0 && widget.sfxVolume == 0
|
title: Text(l10n.menuSpeed),
|
||||||
? Icons.volume_off
|
trailing: Container(
|
||||||
: Icons.volume_up,
|
padding: const EdgeInsets.symmetric(
|
||||||
color: Colors.indigo,
|
horizontal: 12,
|
||||||
),
|
vertical: 4,
|
||||||
title: Text(l10n.uiSound),
|
),
|
||||||
trailing: Text(
|
decoration: BoxDecoration(
|
||||||
_getSoundStatus(),
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${widget.speedMultiplier}x',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
widget.onSpeedCycle();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showSoundDialog(context);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
||||||
// 저장
|
// 통계
|
||||||
ListTile(
|
if (widget.onShowStatistics != null)
|
||||||
leading: const Icon(Icons.save, color: Colors.blue),
|
ListTile(
|
||||||
title: Text(l10n.menuSave),
|
leading: const Icon(Icons.bar_chart, color: Colors.blue),
|
||||||
onTap: () {
|
title: Text(l10n.uiStatistics),
|
||||||
Navigator.pop(context);
|
onTap: () {
|
||||||
widget.onSave();
|
Navigator.pop(context);
|
||||||
widget.notificationService.showGameSaved(l10n.menuSaved);
|
widget.onShowStatistics?.call();
|
||||||
},
|
},
|
||||||
),
|
|
||||||
|
|
||||||
// 새로하기 (세이브 삭제)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.refresh, color: Colors.orange),
|
|
||||||
title: Text(l10n.menuNewGame),
|
|
||||||
subtitle: Text(
|
|
||||||
l10n.menuDeleteSave,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 도움말
|
||||||
|
if (widget.onShowHelp != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.help_outline, color: Colors.green),
|
||||||
|
title: Text(l10n.uiHelp),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.onShowHelp?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// 언어 변경
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.language, color: Colors.teal),
|
||||||
|
title: Text(l10n.menuLanguage),
|
||||||
|
trailing: Text(
|
||||||
|
_getCurrentLanguageName(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showLanguageDialog(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showDeleteConfirmDialog(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 종료
|
// 테마 변경
|
||||||
ListTile(
|
if (widget.onThemeModeChange != null)
|
||||||
leading: const Icon(Icons.exit_to_app, color: Colors.red),
|
ListTile(
|
||||||
title: Text(localizations.exitGame),
|
leading: Icon(_getThemeIcon(), color: Colors.purple),
|
||||||
onTap: () {
|
title: Text(l10n.menuTheme),
|
||||||
Navigator.pop(context);
|
trailing: Text(
|
||||||
widget.onExit();
|
_getCurrentThemeName(),
|
||||||
},
|
style: TextStyle(
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showThemeDialog(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
// 사운드 설정
|
||||||
],
|
if (widget.onBgmVolumeChange != null ||
|
||||||
),
|
widget.onSfxVolumeChange != null)
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
widget.bgmVolume == 0 && widget.sfxVolume == 0
|
||||||
|
? Icons.volume_off
|
||||||
|
: Icons.volume_up,
|
||||||
|
color: Colors.indigo,
|
||||||
|
),
|
||||||
|
title: Text(l10n.uiSound),
|
||||||
|
trailing: Text(
|
||||||
|
_getSoundStatus(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showSoundDialog(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// 저장
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.save, color: Colors.blue),
|
||||||
|
title: Text(l10n.menuSave),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.onSave();
|
||||||
|
widget.notificationService.showGameSaved(l10n.menuSaved);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 새로하기 (세이브 삭제)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.refresh, color: Colors.orange),
|
||||||
|
title: Text(l10n.menuNewGame),
|
||||||
|
subtitle: Text(
|
||||||
|
l10n.menuDeleteSave,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showDeleteConfirmDialog(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 종료
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.exit_to_app, color: Colors.red),
|
||||||
|
title: Text(localizations.exitGame),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.onExit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -586,82 +583,86 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: SafeArea(
|
||||||
children: [
|
top: false, // AppBar가 상단 처리
|
||||||
// 상단: 확장 애니메이션 패널
|
child: Column(
|
||||||
EnhancedAnimationPanel(
|
children: [
|
||||||
progress: state.progress,
|
// 상단: 확장 애니메이션 패널
|
||||||
stats: state.stats,
|
EnhancedAnimationPanel(
|
||||||
skillSystem: state.skillSystem,
|
progress: state.progress,
|
||||||
speedMultiplier: widget.speedMultiplier,
|
stats: state.stats,
|
||||||
onSpeedCycle: widget.onSpeedCycle,
|
skillSystem: state.skillSystem,
|
||||||
isPaused: widget.isPaused,
|
speedMultiplier: widget.speedMultiplier,
|
||||||
onPauseToggle: widget.onPauseToggle,
|
onSpeedCycle: widget.onSpeedCycle,
|
||||||
specialAnimation: widget.specialAnimation,
|
isPaused: widget.isPaused,
|
||||||
weaponName: state.equipment.weapon,
|
onPauseToggle: widget.onPauseToggle,
|
||||||
shieldName: state.equipment.shield,
|
specialAnimation: widget.specialAnimation,
|
||||||
characterLevel: state.traits.level,
|
weaponName: state.equipment.weapon,
|
||||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
shieldName: state.equipment.shield,
|
||||||
latestCombatEvent:
|
characterLevel: state.traits.level,
|
||||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||||
raceId: state.traits.raceId,
|
latestCombatEvent:
|
||||||
),
|
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||||
|
raceId: state.traits.raceId,
|
||||||
// 중앙: 캐로셀 (PageView)
|
|
||||||
Expanded(
|
|
||||||
child: PageView(
|
|
||||||
controller: _pageController,
|
|
||||||
onPageChanged: _onPageChanged,
|
|
||||||
children: [
|
|
||||||
// 0: 스킬
|
|
||||||
SkillsPage(
|
|
||||||
spellBook: state.spellBook,
|
|
||||||
skillSystem: state.skillSystem,
|
|
||||||
),
|
|
||||||
|
|
||||||
// 1: 인벤토리
|
|
||||||
InventoryPage(
|
|
||||||
inventory: state.inventory,
|
|
||||||
potionInventory: state.potionInventory,
|
|
||||||
encumbrance: state.progress.encumbrance,
|
|
||||||
usedPotionTypes:
|
|
||||||
state.progress.currentCombat?.usedPotionTypes ?? const {},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 2: 장비
|
|
||||||
EquipmentPage(equipment: state.equipment),
|
|
||||||
|
|
||||||
// 3: 캐릭터시트 (기본)
|
|
||||||
CharacterSheetPage(
|
|
||||||
traits: state.traits,
|
|
||||||
stats: state.stats,
|
|
||||||
exp: state.progress.exp,
|
|
||||||
),
|
|
||||||
|
|
||||||
// 4: 전투로그
|
|
||||||
CombatLogPage(entries: widget.combatLogEntries),
|
|
||||||
|
|
||||||
// 5: 퀘스트
|
|
||||||
QuestPage(
|
|
||||||
questHistory: state.progress.questHistory,
|
|
||||||
quest: state.progress.quest,
|
|
||||||
),
|
|
||||||
|
|
||||||
// 6: 스토리
|
|
||||||
StoryPage(
|
|
||||||
plotStageCount: state.progress.plotStageCount,
|
|
||||||
plot: state.progress.plot,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// 하단: 네비게이션 바
|
// 중앙: 캐로셀 (PageView)
|
||||||
CarouselNavBar(
|
Expanded(
|
||||||
currentPage: _currentPage,
|
child: PageView(
|
||||||
onPageSelected: _onNavPageSelected,
|
controller: _pageController,
|
||||||
),
|
onPageChanged: _onPageChanged,
|
||||||
],
|
children: [
|
||||||
|
// 0: 스킬
|
||||||
|
SkillsPage(
|
||||||
|
spellBook: state.spellBook,
|
||||||
|
skillSystem: state.skillSystem,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 1: 인벤토리
|
||||||
|
InventoryPage(
|
||||||
|
inventory: state.inventory,
|
||||||
|
potionInventory: state.potionInventory,
|
||||||
|
encumbrance: state.progress.encumbrance,
|
||||||
|
usedPotionTypes:
|
||||||
|
state.progress.currentCombat?.usedPotionTypes ??
|
||||||
|
const {},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 2: 장비
|
||||||
|
EquipmentPage(equipment: state.equipment),
|
||||||
|
|
||||||
|
// 3: 캐릭터시트 (기본)
|
||||||
|
CharacterSheetPage(
|
||||||
|
traits: state.traits,
|
||||||
|
stats: state.stats,
|
||||||
|
exp: state.progress.exp,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 4: 전투로그
|
||||||
|
CombatLogPage(entries: widget.combatLogEntries),
|
||||||
|
|
||||||
|
// 5: 퀘스트
|
||||||
|
QuestPage(
|
||||||
|
questHistory: state.progress.questHistory,
|
||||||
|
quest: state.progress.quest,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 6: 스토리
|
||||||
|
StoryPage(
|
||||||
|
plotStageCount: state.progress.plotStageCount,
|
||||||
|
plot: state.progress.plot,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 하단: 네비게이션 바
|
||||||
|
CarouselNavBar(
|
||||||
|
currentPage: _currentPage,
|
||||||
|
onPageSelected: _onNavPageSelected,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:asciineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||||
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
|
||||||
import 'package:askiineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
|
||||||
|
|
||||||
/// 명예의 전당 화면 (Phase 10: Hall of Fame Screen)
|
/// 명예의 전당 화면 (Phase 10: Hall of Fame Screen)
|
||||||
class HallOfFameScreen extends StatefulWidget {
|
class HallOfFameScreen extends StatefulWidget {
|
||||||
@@ -32,13 +29,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadHallOfFame() async {
|
Future<void> _loadHallOfFame() async {
|
||||||
var hallOfFame = await _storage.load();
|
final hallOfFame = await _storage.load();
|
||||||
|
|
||||||
// 디버그 모드일 때 샘플 엔트리 추가 (빈 경우에만)
|
|
||||||
if (kDebugMode && hallOfFame.isEmpty) {
|
|
||||||
hallOfFame = hallOfFame.addEntry(_createDebugSampleEntry());
|
|
||||||
hallOfFame = hallOfFame.addEntry(_createDebugSampleEntry2());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -52,9 +43,12 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(l10n.uiHallOfFame), centerTitle: true),
|
appBar: AppBar(title: Text(l10n.uiHallOfFame), centerTitle: true),
|
||||||
body: _isLoading
|
body: SafeArea(
|
||||||
? const Center(child: CircularProgressIndicator())
|
top: false, // AppBar가 상단 처리
|
||||||
: _buildContent(),
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildContent(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,333 +142,6 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 디버그 모드 샘플 엔트리 생성 (kDebugMode에서만 사용)
|
|
||||||
HallOfFameEntry _createDebugSampleEntry() {
|
|
||||||
return HallOfFameEntry(
|
|
||||||
id: 'debug_sample_001',
|
|
||||||
characterName: 'Debug Hero',
|
|
||||||
race: 'byte_human',
|
|
||||||
klass: 'recursion_master',
|
|
||||||
level: 100,
|
|
||||||
totalPlayTimeMs: 10 * 60 * 60 * 1000, // 10시간
|
|
||||||
totalDeaths: 3,
|
|
||||||
monstersKilled: 1234,
|
|
||||||
questsCompleted: 42,
|
|
||||||
clearedAt: DateTime.now(),
|
|
||||||
finalEquipment: [
|
|
||||||
// 무기: Universe Simulator|15, 레벨 100 → plus=85
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+85 AI-Augmented Universe Simulator',
|
|
||||||
slot: EquipmentSlot.weapon,
|
|
||||||
level: 100,
|
|
||||||
weight: 15,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(atk: 180, magAtk: 120, criRate: 0.15, attackSpeed: 600),
|
|
||||||
),
|
|
||||||
// 방패: Entropy Shield|65, 레벨 100 → plus=35
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+35 Air-gapped Entropy Shield',
|
|
||||||
slot: EquipmentSlot.shield,
|
|
||||||
level: 100,
|
|
||||||
weight: 12,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(def: 85, magDef: 60, blockRate: 0.25),
|
|
||||||
),
|
|
||||||
// 방어구: Multiverse Armor|60, 레벨 100 → plus=40
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+40 Containerized Multiverse Armor',
|
|
||||||
slot: EquipmentSlot.helm,
|
|
||||||
level: 100,
|
|
||||||
weight: 8,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(def: 45, magDef: 55, intBonus: 5),
|
|
||||||
),
|
|
||||||
// Singularity Barrier|55, 레벨 100 → plus=45
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+45 Quantum-safe Singularity Barrier',
|
|
||||||
slot: EquipmentSlot.hauberk,
|
|
||||||
level: 100,
|
|
||||||
weight: 20,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(def: 95, magDef: 40, hpBonus: 200),
|
|
||||||
),
|
|
||||||
// AI Firewall|45, 레벨 100 → plus=55
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+55 Hardened AI Firewall',
|
|
||||||
slot: EquipmentSlot.brassairts,
|
|
||||||
level: 100,
|
|
||||||
weight: 6,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 35, magDef: 25),
|
|
||||||
),
|
|
||||||
// Neural Network Mesh|40, 레벨 100 → plus=60
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+60 Encrypted Neural Network Mesh',
|
|
||||||
slot: EquipmentSlot.vambraces,
|
|
||||||
level: 100,
|
|
||||||
weight: 5,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 30, magDef: 20, dexBonus: 3),
|
|
||||||
),
|
|
||||||
// Zero-Day Aegis|30, 레벨 100 → plus=70
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+70 Patched Zero-Day Aegis',
|
|
||||||
slot: EquipmentSlot.gauntlets,
|
|
||||||
level: 100,
|
|
||||||
weight: 4,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 25, atk: 15, criRate: 0.05),
|
|
||||||
),
|
|
||||||
// Blockchain Platemail|35, 레벨 100 → plus=65
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+65 Certified Blockchain Platemail',
|
|
||||||
slot: EquipmentSlot.gambeson,
|
|
||||||
level: 100,
|
|
||||||
weight: 10,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 40, magDef: 30),
|
|
||||||
),
|
|
||||||
// Container Suit|19, 레벨 100 → plus=81
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+81 Sandboxed Container Suit',
|
|
||||||
slot: EquipmentSlot.cuisses,
|
|
||||||
level: 100,
|
|
||||||
weight: 8,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 35, evasion: 0.05),
|
|
||||||
),
|
|
||||||
// Virtualization Mail|20, 레벨 100 → plus=80
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+80 Patched Virtualization Mail',
|
|
||||||
slot: EquipmentSlot.greaves,
|
|
||||||
level: 100,
|
|
||||||
weight: 7,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 30, dexBonus: 2),
|
|
||||||
),
|
|
||||||
// Sandbox Shell|18, 레벨 100 → plus=82
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+82 Hardened Sandbox Shell',
|
|
||||||
slot: EquipmentSlot.sollerets,
|
|
||||||
level: 100,
|
|
||||||
weight: 5,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 20, evasion: 0.03),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// 레벨 100 캐릭터의 스펠: randomLow 분포로 낮은 인덱스 스펠이 높은 랭크
|
|
||||||
// 100번의 레벨업 = 100번 스펠 학습 기회, 초반 스펠은 여러 번 학습되어 랭크 상승
|
|
||||||
finalSpells: [
|
|
||||||
{'name': 'Garbage Collection', 'rank': 'XIV'}, // 인덱스 0 - 가장 많이 학습
|
|
||||||
{'name': 'Memory Optimization', 'rank': 'XII'}, // 인덱스 1
|
|
||||||
{'name': 'Debug Mode', 'rank': 'XI'}, // 인덱스 2
|
|
||||||
{'name': 'Breakpoint', 'rank': 'X'}, // 인덱스 3
|
|
||||||
{'name': 'Step Over', 'rank': 'IX'}, // 인덱스 4
|
|
||||||
{'name': 'Step Into', 'rank': 'VIII'}, // 인덱스 5
|
|
||||||
{'name': 'Watch Variable', 'rank': 'VII'}, // 인덱스 6
|
|
||||||
{'name': 'Hot Reload', 'rank': 'VI'}, // 인덱스 7
|
|
||||||
{'name': 'Cold Boot', 'rank': 'V'}, // 인덱스 8
|
|
||||||
{'name': 'Safe Mode', 'rank': 'IV'}, // 인덱스 9
|
|
||||||
{'name': 'Kernel Panic', 'rank': 'III'}, // 인덱스 10
|
|
||||||
{'name': 'Blue Screen', 'rank': 'II'}, // 인덱스 11
|
|
||||||
{'name': 'Stack Trace', 'rank': 'I'}, // 인덱스 12
|
|
||||||
],
|
|
||||||
// 레벨 100 기본 스탯: 시작 ~60 + 99레벨 × 2스탯 = ~258 총합
|
|
||||||
// recursion_master (마법사 계열): INT/WIS 집중 빌드
|
|
||||||
// CombatStats는 게임 공식 CombatStats.fromStats()에 따라 계산
|
|
||||||
// baseAtk = STR*2 + level + equipStats.atk = 32*2 + 100 + 195 = 359
|
|
||||||
// baseDef = CON + level/2 + equipStats.def = 38 + 50 + 440 = 528
|
|
||||||
// baseMagAtk = INT*2 + level + equipStats.magAtk = 65*2 + 100 + 120 = 350
|
|
||||||
// baseMagDef = WIS + level/2 + equipStats.magDef = 55 + 50 + 210 = 315
|
|
||||||
finalStats: const CombatStats(
|
|
||||||
str: 32,
|
|
||||||
con: 38,
|
|
||||||
dex: 45,
|
|
||||||
intelligence: 65,
|
|
||||||
wis: 55,
|
|
||||||
cha: 23,
|
|
||||||
atk: 359,
|
|
||||||
def: 528,
|
|
||||||
magAtk: 350,
|
|
||||||
magDef: 315,
|
|
||||||
criRate: 0.32,
|
|
||||||
criDamage: 1.95,
|
|
||||||
evasion: 0.30,
|
|
||||||
accuracy: 0.89,
|
|
||||||
blockRate: 0.37,
|
|
||||||
parryRate: 0.15,
|
|
||||||
attackDelayMs: 650,
|
|
||||||
hpMax: 1850,
|
|
||||||
hpCurrent: 1850,
|
|
||||||
mpMax: 2100,
|
|
||||||
mpCurrent: 2100,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 디버그 모드 샘플 엔트리 2 (전사 계열 캐릭터)
|
|
||||||
HallOfFameEntry _createDebugSampleEntry2() {
|
|
||||||
return HallOfFameEntry(
|
|
||||||
id: 'debug_sample_002',
|
|
||||||
characterName: 'Binary Knight',
|
|
||||||
race: 'null_elf',
|
|
||||||
klass: 'overflow_warrior',
|
|
||||||
level: 95,
|
|
||||||
totalPlayTimeMs: 8 * 60 * 60 * 1000 + 30 * 60 * 1000, // 8시간 30분
|
|
||||||
totalDeaths: 7,
|
|
||||||
monstersKilled: 2156,
|
|
||||||
questsCompleted: 38,
|
|
||||||
clearedAt: DateTime.now().subtract(const Duration(days: 3)),
|
|
||||||
finalEquipment: [
|
|
||||||
// 무기: Quantum Entangler|10, 레벨 95 → plus=85
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+85 GPU-Powered Quantum Entangler',
|
|
||||||
slot: EquipmentSlot.weapon,
|
|
||||||
level: 95,
|
|
||||||
weight: 18,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(atk: 220, criRate: 0.12, parryRate: 0.08, attackSpeed: 850),
|
|
||||||
),
|
|
||||||
// 방패: Multiverse Barrier|50, 레벨 95 → plus=45
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+45 Air-gapped Multiverse Barrier',
|
|
||||||
slot: EquipmentSlot.shield,
|
|
||||||
level: 95,
|
|
||||||
weight: 16,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(def: 120, magDef: 45, blockRate: 0.35, hpBonus: 150),
|
|
||||||
),
|
|
||||||
// Multiverse Armor|60, 레벨 95 → plus=35
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+35 Certified Multiverse Armor',
|
|
||||||
slot: EquipmentSlot.helm,
|
|
||||||
level: 95,
|
|
||||||
weight: 10,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(def: 55, conBonus: 4),
|
|
||||||
),
|
|
||||||
// Quantum Shield Matrix|50, 레벨 95 → plus=45
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+45 Containerized Quantum Shield Matrix',
|
|
||||||
slot: EquipmentSlot.hauberk,
|
|
||||||
level: 95,
|
|
||||||
weight: 25,
|
|
||||||
rarity: ItemRarity.legendary,
|
|
||||||
stats: ItemStats(def: 110, hpBonus: 300, conBonus: 3),
|
|
||||||
),
|
|
||||||
// ASLR Armor|17, 레벨 95 → plus=78
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+78 Patched ASLR Armor',
|
|
||||||
slot: EquipmentSlot.brassairts,
|
|
||||||
level: 95,
|
|
||||||
weight: 8,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 40, strBonus: 2),
|
|
||||||
),
|
|
||||||
// Stack Protector|15, 레벨 95 → plus=80
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+80 Encrypted Stack Protector',
|
|
||||||
slot: EquipmentSlot.vambraces,
|
|
||||||
level: 95,
|
|
||||||
weight: 6,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 35, atk: 10),
|
|
||||||
),
|
|
||||||
// Heap Guard|16, 레벨 95 → plus=79
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+79 Hardened Heap Guard',
|
|
||||||
slot: EquipmentSlot.gauntlets,
|
|
||||||
level: 95,
|
|
||||||
weight: 5,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 30, atk: 25, criRate: 0.03),
|
|
||||||
),
|
|
||||||
// Memory Barrier|14, 레벨 95 → plus=81
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+81 Quantum-safe Memory Barrier',
|
|
||||||
slot: EquipmentSlot.gambeson,
|
|
||||||
level: 95,
|
|
||||||
weight: 12,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 45, magDef: 20),
|
|
||||||
),
|
|
||||||
// Kernel Guard|12, 레벨 95 → plus=83
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+83 Certified Kernel Guard',
|
|
||||||
slot: EquipmentSlot.cuisses,
|
|
||||||
level: 95,
|
|
||||||
weight: 10,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 42, strBonus: 2),
|
|
||||||
),
|
|
||||||
// Protocol Suit|10, 레벨 95 → plus=85
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+85 Sandboxed Protocol Suit',
|
|
||||||
slot: EquipmentSlot.greaves,
|
|
||||||
level: 95,
|
|
||||||
weight: 8,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 38, dexBonus: 2),
|
|
||||||
),
|
|
||||||
// Encryption Layer|6, 레벨 95 → plus=89
|
|
||||||
const EquipmentItem(
|
|
||||||
name: '+89 Patched Encryption Layer',
|
|
||||||
slot: EquipmentSlot.sollerets,
|
|
||||||
level: 95,
|
|
||||||
weight: 6,
|
|
||||||
rarity: ItemRarity.epic,
|
|
||||||
stats: ItemStats(def: 25, evasion: 0.02),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// 레벨 95 캐릭터의 스펠: randomLow 분포 적용
|
|
||||||
// 95번의 레벨업 기회, 전사 계열이라 WIS가 낮아 학습 가능 스펠 범위 제한
|
|
||||||
// WIS 30 + level 95 = 125 → 대부분의 스펠 접근 가능
|
|
||||||
finalSpells: [
|
|
||||||
{'name': 'Garbage Collection', 'rank': 'XIII'}, // 인덱스 0
|
|
||||||
{'name': 'Memory Optimization', 'rank': 'XI'}, // 인덱스 1
|
|
||||||
{'name': 'Debug Mode', 'rank': 'X'}, // 인덱스 2
|
|
||||||
{'name': 'Breakpoint', 'rank': 'IX'}, // 인덱스 3
|
|
||||||
{'name': 'Step Over', 'rank': 'VIII'}, // 인덱스 4
|
|
||||||
{'name': 'Step Into', 'rank': 'VII'}, // 인덱스 5
|
|
||||||
{'name': 'Watch Variable', 'rank': 'VI'}, // 인덱스 6
|
|
||||||
{'name': 'Hot Reload', 'rank': 'V'}, // 인덱스 7
|
|
||||||
{'name': 'Cold Boot', 'rank': 'IV'}, // 인덱스 8
|
|
||||||
{'name': 'Safe Mode', 'rank': 'III'}, // 인덱스 9
|
|
||||||
{'name': 'Kernel Panic', 'rank': 'II'}, // 인덱스 10
|
|
||||||
],
|
|
||||||
// 레벨 95 기본 스탯: 시작 ~60 + 94레벨 × 2스탯 = ~248 총합
|
|
||||||
// overflow_warrior (전사 계열): STR/CON 집중 빌드
|
|
||||||
// CombatStats는 게임 공식에 따라 계산
|
|
||||||
// baseAtk = STR*2 + level + equipStats.atk = 58*2 + 95 + 255 = 466
|
|
||||||
// baseDef = CON + level/2 + equipStats.def = 55 + 47 + 577 = 679
|
|
||||||
// baseMagAtk = INT*2 + level + equipStats.magAtk = 28*2 + 95 + 0 = 151
|
|
||||||
// baseMagDef = WIS + level/2 + equipStats.magDef = 30 + 47 + 65 = 142
|
|
||||||
finalStats: const CombatStats(
|
|
||||||
str: 58,
|
|
||||||
con: 55,
|
|
||||||
dex: 42,
|
|
||||||
intelligence: 28,
|
|
||||||
wis: 30,
|
|
||||||
cha: 35,
|
|
||||||
atk: 466,
|
|
||||||
def: 679,
|
|
||||||
magAtk: 151,
|
|
||||||
magDef: 142,
|
|
||||||
criRate: 0.26,
|
|
||||||
criDamage: 1.92,
|
|
||||||
evasion: 0.23,
|
|
||||||
accuracy: 0.88,
|
|
||||||
blockRate: 0.47,
|
|
||||||
parryRate: 0.28,
|
|
||||||
attackDelayMs: 800,
|
|
||||||
hpMax: 2200,
|
|
||||||
hpCurrent: 2200,
|
|
||||||
mpMax: 1100,
|
|
||||||
mpCurrent: 1100,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 명예의 전당 엔트리 카드
|
/// 명예의 전당 엔트리 카드
|
||||||
class _HallOfFameEntryCard extends StatelessWidget {
|
class _HallOfFameEntryCard extends StatelessWidget {
|
||||||
const _HallOfFameEntryCard({required this.entry, required this.rank});
|
const _HallOfFameEntryCard({required this.entry, required this.rank});
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:askiineverdie/data/class_data.dart';
|
import 'package:asciineverdie/data/class_data.dart';
|
||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||||
import 'package:askiineverdie/data/race_data.dart';
|
import 'package:asciineverdie/data/race_data.dart';
|
||||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:askiineverdie/src/core/model/class_traits.dart';
|
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/race_traits.dart';
|
import 'package:asciineverdie/src/core/model/race_traits.dart';
|
||||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
import 'package:asciineverdie/src/core/util/pq_logic.dart';
|
||||||
import 'package:askiineverdie/src/features/new_character/widgets/race_preview.dart';
|
import 'package:asciineverdie/src/features/new_character/widgets/race_preview.dart';
|
||||||
import 'package:askiineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
import 'package:askiineverdie/src/shared/widgets/retro_widgets.dart';
|
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||||
|
|
||||||
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
|
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
|
||||||
class NewCharacterScreen extends StatefulWidget {
|
class NewCharacterScreen extends StatefulWidget {
|
||||||
@@ -60,8 +61,8 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
// 이름 생성용 RNG
|
// 이름 생성용 RNG
|
||||||
late DeterministicRandom _nameRng;
|
late DeterministicRandom _nameRng;
|
||||||
|
|
||||||
// 테스트 모드 (웹에서 모바일 캐로셀 레이아웃 활성화)
|
// 치트 모드 (디버그 전용: 100x 터보 배속 활성화)
|
||||||
bool _testModeEnabled = false;
|
bool _cheatsEnabled = false;
|
||||||
|
|
||||||
// 굴리기 버튼 연속 클릭 방지
|
// 굴리기 버튼 연속 클릭 방지
|
||||||
bool _isRolling = false;
|
bool _isRolling = false;
|
||||||
@@ -264,7 +265,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
queue: QueueState.empty(),
|
queue: QueueState.empty(),
|
||||||
);
|
);
|
||||||
|
|
||||||
widget.onCharacterCreated?.call(initialState, testMode: _testModeEnabled);
|
widget.onCharacterCreated?.call(initialState, testMode: _cheatsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -284,53 +285,100 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
iconTheme: const IconThemeData(color: RetroColors.gold),
|
iconTheme: const IconThemeData(color: RetroColors.gold),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SafeArea(
|
||||||
padding: const EdgeInsets.all(16),
|
top: false, // AppBar가 상단 처리
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
child: Column(
|
||||||
// 이름 입력 섹션
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
_buildNameSection(),
|
children: [
|
||||||
const SizedBox(height: 16),
|
// 이름 입력 섹션
|
||||||
|
_buildNameSection(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 능력치 섹션
|
// 능력치 섹션
|
||||||
_buildStatsSection(),
|
_buildStatsSection(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션)
|
// 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션)
|
||||||
RetroPanel(
|
RetroPanel(
|
||||||
title: 'PREVIEW',
|
title: 'PREVIEW',
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: RacePreview(
|
child: RacePreview(raceId: _races[_selectedRaceIndex].raceId),
|
||||||
raceId: _races[_selectedRaceIndex].raceId,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// 종족/직업 선택 섹션
|
// 종족/직업 선택 섹션
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildRaceSection()),
|
Expanded(child: _buildRaceSection()),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(child: _buildKlassSection()),
|
Expanded(child: _buildKlassSection()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Sold! 버튼
|
||||||
|
RetroTextButton(
|
||||||
|
text: L10n.of(context).soldButton,
|
||||||
|
icon: Icons.check,
|
||||||
|
onPressed: _onSold,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 디버그 전용: 치트 모드 토글 (100x 터보 배속)
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _cheatsEnabled = !_cheatsEnabled),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _cheatsEnabled
|
||||||
|
? RetroColors.hpRed.withValues(alpha: 0.3)
|
||||||
|
: RetroColors.panelBg,
|
||||||
|
border: Border.all(
|
||||||
|
color: _cheatsEnabled
|
||||||
|
? RetroColors.hpRed
|
||||||
|
: RetroColors.panelBorderInner,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_cheatsEnabled
|
||||||
|
? Icons.bug_report
|
||||||
|
: Icons.bug_report_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: _cheatsEnabled
|
||||||
|
? RetroColors.hpRed
|
||||||
|
: RetroColors.textDisabled,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'DEBUG: TURBO MODE (100x)',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 8,
|
||||||
|
color: _cheatsEnabled
|
||||||
|
? RetroColors.hpRed
|
||||||
|
: RetroColors.textDisabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
|
|
||||||
// 테스트 모드 토글 (웹에서 모바일 레이아웃 테스트)
|
|
||||||
_buildTestModeToggle(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Sold! 버튼
|
|
||||||
RetroTextButton(
|
|
||||||
text: L10n.of(context).soldButton,
|
|
||||||
icon: Icons.check,
|
|
||||||
onPressed: _onSold,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -372,10 +420,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
RetroIconButton(
|
RetroIconButton(icon: Icons.casino, onPressed: _onGenerateName),
|
||||||
icon: Icons.casino,
|
|
||||||
onPressed: _onGenerateName,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -735,84 +780,4 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 테스트 모드 토글 위젯
|
|
||||||
Widget _buildTestModeToggle() {
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user