import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:asciineverdie/data/class_data.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:asciineverdie/data/race_data.dart'; import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/src/core/engine/character_roll_service.dart'; import 'package:asciineverdie/src/core/engine/iap_service.dart'; import 'package:asciineverdie/src/core/model/class_traits.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/race_traits.dart'; import 'package:asciineverdie/src/core/util/deterministic_random.dart'; import 'package:asciineverdie/src/core/util/pq_logic.dart'; import 'package:asciineverdie/src/features/new_character/widgets/class_selection_section.dart'; import 'package:asciineverdie/src/features/new_character/widgets/name_input_section.dart'; import 'package:asciineverdie/src/features/new_character/widgets/race_preview.dart'; import 'package:asciineverdie/src/features/new_character/widgets/race_selection_section.dart'; import 'package:asciineverdie/src/features/new_character/widgets/stats_section.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart'; /// 캐릭터 생성 화면 (NewGuy.pas 포팅) class NewCharacterScreen extends StatefulWidget { const NewCharacterScreen({super.key, this.onCharacterCreated}); /// 캐릭터 생성 완료 시 호출되는 콜백 /// testMode: 웹에서도 모바일 캐로셀 레이아웃 사용 final void Function(GameState initialState, {bool testMode})? onCharacterCreated; @override State createState() => _NewCharacterScreenState(); } class _NewCharacterScreenState extends State { final TextEditingController _nameController = TextEditingController(); final ScrollController _raceScrollController = ScrollController(); final ScrollController _klassScrollController = ScrollController(); // 종족(races)과 직업(klasses) 목록 (Phase 5) final List _races = RaceData.all; final List _klasses = ClassData.all; // 선택된 종족/직업 인덱스 int _selectedRaceIndex = 0; int _selectedKlassIndex = 0; // 능력치(stats) int _str = 0; int _con = 0; int _dex = 0; int _int = 0; int _wis = 0; int _cha = 0; // 현재 RNG 시드 (Re-Roll 전 저장) int _currentSeed = 0; // 이름 생성용 RNG late DeterministicRandom _nameRng; // 치트 모드 (디버그 전용: 100x 터보 배속 활성화) bool _cheatsEnabled = false; // 굴리기 버튼 연속 클릭 방지 bool _isRolling = false; // 굴리기/되돌리기 서비스 final CharacterRollService _rollService = CharacterRollService.instance; @override void initState() { super.initState(); // 서비스 초기화 _initializeService(); // 초기 랜덤화 final random = math.Random(); _selectedRaceIndex = random.nextInt(_races.length); _selectedKlassIndex = random.nextInt(_klasses.length); // 초기 스탯 굴림 _currentSeed = random.nextInt(0x7FFFFFFF); _nameRng = DeterministicRandom(random.nextInt(0x7FFFFFFF)); _rollStats(); // 초기 이름 생성 _nameController.text = generateName(_nameRng); // 선택된 종족/직업으로 스크롤 _scrollToSelectedItems(); } /// 서비스 초기화 Future _initializeService() async { await _rollService.initialize(); } @override void dispose() { _nameController.dispose(); _raceScrollController.dispose(); _klassScrollController.dispose(); super.dispose(); } /// 선택된 종족/직업 위치로 스크롤 void _scrollToSelectedItems() { // ListTile 높이 약 48px (dense 모드) const itemHeight = 48.0; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; // 스크롤 애니메이션 대신 즉시 점프 (WASM 모드 안정성) if (_raceScrollController.hasClients) { final raceOffset = _selectedRaceIndex * itemHeight; _raceScrollController.jumpTo( raceOffset.clamp(0.0, _raceScrollController.position.maxScrollExtent), ); } if (_klassScrollController.hasClients) { final klassOffset = _selectedKlassIndex * itemHeight; _klassScrollController.jumpTo( klassOffset.clamp( 0.0, _klassScrollController.position.maxScrollExtent, ), ); } }); } /// 스탯 굴림 (3d6 × 6) void _rollStats() { final rng = DeterministicRandom(_currentSeed); setState(() { _str = rollStat(rng); _con = rollStat(rng); _dex = rollStat(rng); _int = rollStat(rng); _wis = rollStat(rng); _cha = rollStat(rng); }); } /// Re-Roll 버튼 클릭 /// 원본 NewGuy.pas RerollClick: 스탯, 종족, 클래스 모두 랜덤화 void _onReroll() { // 연속 클릭 방지 if (_isRolling) return; _isRolling = true; // 굴리기 가능 여부 확인 if (!_rollService.canRoll) { _isRolling = false; _showRechargeDialog(); return; } // 현재 상태를 서비스에 저장 final currentStats = Stats( str: _str, con: _con, dex: _dex, intelligence: _int, wis: _wis, cha: _cha, hpMax: 0, mpMax: 0, ); final success = _rollService.roll( currentStats: currentStats, currentRaceIndex: _selectedRaceIndex, currentKlassIndex: _selectedKlassIndex, currentSeed: _currentSeed, ); if (!success) { _isRolling = false; return; } // 새 시드로 굴림 final random = math.Random(); _currentSeed = random.nextInt(0x7FFFFFFF); // 종족/클래스 랜덤 선택 및 스탯 굴림 setState(() { _selectedRaceIndex = random.nextInt(_races.length); _selectedKlassIndex = random.nextInt(_klasses.length); // 스탯 굴림 (setState 내에서 실행하여 UI 갱신 보장) final rng = DeterministicRandom(_currentSeed); _str = rollStat(rng); _con = rollStat(rng); _dex = rollStat(rng); _int = rollStat(rng); _wis = rollStat(rng); _cha = rollStat(rng); }); // 선택된 종족/직업으로 스크롤 _scrollToSelectedItems(); // 짧은 딜레이 후 다시 클릭 가능 Future.delayed(const Duration(milliseconds: 100), () { if (mounted) _isRolling = false; }); } /// 굴리기 충전 다이얼로그 Future _showRechargeDialog() async { final isPaidUser = IAPService.instance.isAdRemovalPurchased; final result = await showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: RetroColors.panelBg, title: Text( L10n.of(context).rechargeRollsTitle, style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: RetroColors.gold, ), ), content: Text( isPaidUser ? L10n.of(context).rechargeRollsFree : L10n.of(context).rechargeRollsAd, style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 12, color: RetroColors.textLight, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text( L10n.of(context).cancel.toUpperCase(), style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 11, color: RetroColors.textDisabled, ), ), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (!isPaidUser) ...[ const Icon(Icons.play_circle, size: 14, color: RetroColors.gold), const SizedBox(width: 4), ], Text( L10n.of(context).rechargeButton, style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 11, color: RetroColors.gold, ), ), ], ), ), ], ), ); if (result == true && mounted) { final success = await _rollService.rechargeRollsWithAd(); if (success && mounted) { setState(() {}); } } } /// Unroll 버튼 클릭 (이전 롤로 복원) Future _onUnroll() async { if (!_rollService.canUndo) return; final isPaidUser = IAPService.instance.isAdRemovalPurchased; RollSnapshot? snapshot; if (isPaidUser) { snapshot = _rollService.undoPaidUser(); } else { snapshot = await _rollService.undoFreeUser(); } // UI 상태 갱신 (성공/실패 여부와 관계없이 버튼 상태 업데이트) if (!mounted) return; if (snapshot != null) { setState(() { _str = snapshot!.stats.str; _con = snapshot.stats.con; _dex = snapshot.stats.dex; _int = snapshot.stats.intelligence; _wis = snapshot.stats.wis; _cha = snapshot.stats.cha; _selectedRaceIndex = snapshot.raceIndex; _selectedKlassIndex = snapshot.klassIndex; _currentSeed = snapshot.seed; }); _scrollToSelectedItems(); } else { // 광고 취소/실패 시에도 버튼 상태 갱신 setState(() {}); } } /// 이름 생성 버튼 클릭 void _onGenerateName() { setState(() { _nameController.text = generateName(_nameRng); }); } /// Sold! 버튼 클릭 - 캐릭터 생성 완료 /// 원본 Main.pas:1371-1388 RollCharacter 로직 void _onSold() { final name = _nameController.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(game_l10n.uiEnterName))); return; } // 선택된 종족/클래스 (Phase 5) final selectedRace = _races[_selectedRaceIndex]; final selectedClass = _klasses[_selectedKlassIndex]; // 게임에 사용할 새 RNG 생성 final gameSeed = math.Random().nextInt(0x7FFFFFFF); // HP/MP 초기값 계산 // 원본 공식: Random(8) + CON/6 → 약 1~10 HP (너무 낮음) // 수정 공식: 65 + Random(8) + CON → 약 68~91 HP (사망률 ~10% 목표) // 이유: 원본 PQ는 "항상 승리"하지만 이 게임은 실제 전투로 사망 가능 final hpMax = 65 + math.Random().nextInt(8) + _con; final mpMax = 30 + math.Random().nextInt(8) + _int; // 원본 Main.pas:1375-1379 - 기본 롤 값 그대로 저장 (보너스 없음) final finalStats = Stats( str: _str, con: _con, dex: _dex, intelligence: _int, wis: _wis, cha: _cha, hpMax: hpMax, mpMax: mpMax, ); final traits = Traits( name: name, race: selectedRace.name, klass: selectedClass.name, level: 1, motto: '', guild: '', raceId: selectedRace.raceId, classId: selectedClass.classId, ); // 초기 게임 상태 생성 final initialState = GameState.withSeed( seed: gameSeed, traits: traits, stats: finalStats, inventory: Inventory.empty(), equipment: Equipment.empty(), skillBook: SkillBook.empty(), progress: ProgressState.empty(), queue: QueueState.empty(), ); // 캐릭터 생성 완료 알림 (되돌리기 상태 초기화) _rollService.onCharacterCreated(); widget.onCharacterCreated?.call(initialState, testMode: _cheatsEnabled); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: RetroColors.deepBrown, appBar: AppBar( backgroundColor: RetroColors.darkBrown, title: Text( L10n.of(context).newCharacterTitle.toUpperCase(), style: const TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: RetroColors.gold, ), ), centerTitle: true, iconTheme: const IconThemeData(color: RetroColors.gold), ), body: SafeArea( top: false, // AppBar가 상단 처리 child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 이름 입력 섹션 NameInputSection( controller: _nameController, onGenerateName: _onGenerateName, ), const SizedBox(height: 16), // 능력치 섹션 StatsSection( str: _str, con: _con, dex: _dex, intelligence: _int, wis: _wis, cha: _cha, canRoll: _rollService.canRoll, canUndo: _rollService.canUndo, rollsRemaining: _rollService.rollsRemaining, availableUndos: _rollService.availableUndos, onRoll: _onReroll, onUndo: _onUnroll, ), const SizedBox(height: 16), // 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션) RetroPanel( title: L10n.of(context).previewTitle, padding: const EdgeInsets.all(8), child: Center( child: RacePreview(raceId: _races[_selectedRaceIndex].raceId), ), ), const SizedBox(height: 16), // 종족/직업 선택 섹션 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: RaceSelectionSection( races: _races, selectedIndex: _selectedRaceIndex, scrollController: _raceScrollController, onSelected: (index) => setState(() => _selectedRaceIndex = index), ), ), const SizedBox(width: 16), Expanded( child: ClassSelectionSection( klasses: _klasses, selectedIndex: _selectedKlassIndex, scrollController: _klassScrollController, onSelected: (index) => setState(() => _selectedKlassIndex = index), ), ), ], ), const SizedBox(height: 16), // Sold! 버튼 RetroTextButton( text: L10n.of(context).soldButton, icon: Icons.check, onPressed: _onSold, ), // 디버그 전용: 치트 모드 토글 (100x 터보 배속) if (kDebugMode) ...[ const SizedBox(height: 16), _buildDebugCheatToggle(context), ], ], ), ), ), ); } /// 디버그 치트 토글 위젯 Widget _buildDebugCheatToggle(BuildContext context) { return 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), Flexible( child: Text( L10n.of(context).debugTurbo, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 11, color: _cheatsEnabled ? RetroColors.hpRed : RetroColors.textDisabled, ), ), ), ], ), ), ); } }