import 'dart:async'; import 'package:flutter/material.dart'; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/story_data.dart'; /// 시네마틱 뷰 위젯 (Phase 9: Cinematic UI) /// /// Act 전환 시 풀스크린 시네마틱 표시 class CinematicView extends StatefulWidget { const CinematicView({ super.key, required this.steps, required this.onComplete, this.canSkip = true, }); final List steps; final VoidCallback onComplete; final bool canSkip; @override State createState() => _CinematicViewState(); } class _CinematicViewState extends State with SingleTickerProviderStateMixin { int _currentStep = 0; Timer? _autoAdvanceTimer; late AnimationController _fadeController; late Animation _fadeAnimation; @override void initState() { super.initState(); _fadeController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _fadeAnimation = CurvedAnimation( parent: _fadeController, curve: Curves.easeInOut, ); _fadeController.forward(); _scheduleAutoAdvance(); } void _scheduleAutoAdvance() { _autoAdvanceTimer?.cancel(); if (_currentStep < widget.steps.length) { final step = widget.steps[_currentStep]; _autoAdvanceTimer = Timer( Duration(milliseconds: step.durationMs), _advanceStep, ); } } void _advanceStep() { if (_currentStep >= widget.steps.length - 1) { _complete(); return; } _fadeController.reverse().then((_) { if (mounted) { setState(() { _currentStep++; }); _fadeController.forward(); _scheduleAutoAdvance(); } }); } void _complete() { _autoAdvanceTimer?.cancel(); widget.onComplete(); } void _skip() { if (widget.canSkip) { _complete(); } } @override void dispose() { _autoAdvanceTimer?.cancel(); _fadeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (widget.steps.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { widget.onComplete(); }); return const SizedBox.shrink(); } final step = widget.steps[_currentStep]; return GestureDetector( onTap: _advanceStep, child: Material( color: Colors.black, child: SafeArea( child: Stack( children: [ // 메인 콘텐츠 Center( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ASCII 아트 if (step.asciiArt != null) ...[ _AsciiArtDisplay(asciiArt: step.asciiArt!), const SizedBox(height: 24), ], // 텍스트 Text( step.text, style: const TextStyle( color: Colors.white, fontSize: 18, fontFamily: 'JetBrainsMono', height: 1.5, ), textAlign: TextAlign.center, ), ], ), ), ), ), // 진행 표시 (Progress Indicator) Positioned( bottom: 40, left: 0, right: 0, child: _ProgressDots( total: widget.steps.length, current: _currentStep, ), ), // 스킵 버튼 if (widget.canSkip) Positioned( top: 16, right: 16, child: TextButton( onPressed: _skip, child: Text( l10n.uiSkip, style: const TextStyle( color: Colors.white54, fontSize: 14, ), ), ), ), // 탭 힌트 Positioned( bottom: 16, left: 0, right: 0, child: Text( l10n.uiTapToContinue, style: TextStyle( color: Colors.white.withValues(alpha: 0.3), fontSize: 12, ), textAlign: TextAlign.center, ), ), ], ), ), ), ); } } /// ASCII 아트 표시 위젯 class _AsciiArtDisplay extends StatelessWidget { const _AsciiArtDisplay({required this.asciiArt}); final String asciiArt; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all(color: Colors.cyan.withValues(alpha: 0.5)), borderRadius: BorderRadius.circular(8), ), child: Text( asciiArt, style: const TextStyle( color: Colors.cyan, fontSize: 14, fontFamily: 'JetBrainsMono', height: 1.2, ), textAlign: TextAlign.center, ), ); } } /// 진행 도트 표시 위젯 class _ProgressDots extends StatelessWidget { const _ProgressDots({required this.total, required this.current}); final int total; final int current; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(total, (index) { final isActive = index == current; final isPast = index < current; return Container( width: isActive ? 12 : 8, height: isActive ? 12 : 8, margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? Colors.cyan : isPast ? Colors.cyan.withValues(alpha: 0.5) : Colors.white.withValues(alpha: 0.2), ), ); }), ); } } /// 시네마틱 표시 다이얼로그 함수 (Show Cinematic Dialog) Future showCinematic( BuildContext context, { required List steps, bool canSkip = true, }) async { if (steps.isEmpty) return; return showDialog( context: context, barrierDismissible: false, barrierColor: Colors.black, builder: (context) => CinematicView( steps: steps, canSkip: canSkip, onComplete: () => Navigator.of(context).pop(), ), ); } /// Act 시네마틱 표시 함수 (Show Act Cinematic) Future showActCinematic(BuildContext context, StoryAct act) async { final steps = cinematicData[act]; if (steps == null || steps.isEmpty) return; await showCinematic(context, steps: steps); }