diff --git a/lib/src/features/game/widgets/victory_overlay.dart b/lib/src/features/game/widgets/victory_overlay.dart index 3b6a3c8..20df4b0 100644 --- a/lib/src/features/game/widgets/victory_overlay.dart +++ b/lib/src/features/game/widgets/victory_overlay.dart @@ -34,107 +34,184 @@ class VictoryOverlay extends StatefulWidget { class _VictoryOverlayState extends State with SingleTickerProviderStateMixin { - late AnimationController _scrollController; + late AnimationController _animationController; late Animation _scrollAnimation; // 스크롤이 완료(최하단 도달) 되었는지 여부 bool _isScrollComplete = false; + // 터치 중인지 여부 (터치 시 속도업) + bool _isTouching = false; + + // 스크롤 완료 후 수동 스크롤용 컨트롤러 + late ScrollController _manualScrollController; + // 스크롤 지속 시간 (밀리초) - static const _scrollDurationMs = 25000; // 25초 + static const _scrollDurationMs = 25000; // 25초 (기본 속도) + static const _fastScrollDurationMs = 5000; // 5초 (빠른 속도, 5배속) @override void initState() { super.initState(); - _scrollController = AnimationController( + _manualScrollController = ScrollController(); + + _animationController = AnimationController( duration: const Duration(milliseconds: _scrollDurationMs), vsync: this, ); - _scrollAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation(parent: _scrollController, curve: Curves.linear)); + _scrollAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.linear), + ); - // 스크롤 완료 시 버튼 표시 (자동 종료하지 않음) - _scrollController.addStatusListener((status) { + // 스크롤 완료 시 수동 스크롤 모드로 전환 + _animationController.addStatusListener((status) { if (status == AnimationStatus.completed) { - setState(() { - _isScrollComplete = true; - }); + _onScrollComplete(); } }); - _scrollController.forward(); + _animationController.forward(); } @override void dispose() { - _scrollController.dispose(); + _animationController.dispose(); + _manualScrollController.dispose(); super.dispose(); } - /// 탭 시 스크롤 최하단으로 즉시 이동 - void _skipToEnd() { - _scrollController.stop(); - _scrollController.value = 1.0; + /// 스크롤 완료 시 호출 + void _onScrollComplete() { setState(() { _isScrollComplete = true; }); + // 수동 스크롤 시 맨 아래에서 시작 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_manualScrollController.hasClients) { + _manualScrollController.jumpTo( + _manualScrollController.position.maxScrollExtent, + ); + } + }); + } + + /// 터치 시작 - 스크롤 속도업 + void _onTouchStart() { + if (_isScrollComplete) return; + + setState(() { + _isTouching = true; + }); + + // 현재 진행도 저장 + final currentProgress = _animationController.value; + final remainingProgress = 1.0 - currentProgress; + + // 남은 시간 계산 (빠른 속도 기준) + final remainingDuration = Duration( + milliseconds: (remainingProgress * _fastScrollDurationMs).toInt(), + ); + + _animationController.duration = remainingDuration; + _animationController.forward(from: currentProgress); + } + + /// 터치 종료 - 스크롤 속도 복원 + void _onTouchEnd() { + if (_isScrollComplete) return; + + setState(() { + _isTouching = false; + }); + + // 현재 진행도 저장 + final currentProgress = _animationController.value; + final remainingProgress = 1.0 - currentProgress; + + // 남은 시간 계산 (기본 속도 기준) + final remainingDuration = Duration( + milliseconds: (remainingProgress * _scrollDurationMs).toInt(), + ); + + _animationController.duration = remainingDuration; + _animationController.forward(from: currentProgress); } @override Widget build(BuildContext context) { final gold = RetroColors.goldOf(context); + final l10n = L10n.of(context); return GestureDetector( - onTap: _isScrollComplete ? null : _skipToEnd, // 스크롤 중에만 탭으로 스킵 + // 터치 시 스크롤 속도업 (스크롤 중에만) + onTapDown: _isScrollComplete ? null : (_) => _onTouchStart(), + onTapUp: _isScrollComplete ? null : (_) => _onTouchEnd(), + onTapCancel: _isScrollComplete ? null : _onTouchEnd, + onLongPressStart: _isScrollComplete ? null : (_) => _onTouchStart(), + onLongPressEnd: _isScrollComplete ? null : (_) => _onTouchEnd(), + onLongPressCancel: _isScrollComplete ? null : _onTouchEnd, child: Material( color: Colors.black, child: SafeArea( child: Stack( children: [ - // 스크롤되는 크레딧 - AnimatedBuilder( - animation: _scrollAnimation, - builder: (context, child) { - return _buildScrollingCredits(context); - }, - ), - - // 스킵 버튼 (스크롤 중에만 표시) + // 스크롤 중: 애니메이션 기반 스크롤 + // 스크롤 완료: 수동 스크롤 가능 if (!_isScrollComplete) - Positioned( - top: 16, - right: 16, - child: TextButton( - onPressed: _skipToEnd, - child: Text( - L10n.of(context).endingSkip, - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 10, - color: gold.withValues(alpha: 0.5), - ), - ), - ), - ), + AnimatedBuilder( + animation: _scrollAnimation, + builder: (context, child) { + return _buildScrollingCredits(context); + }, + ) + else + _buildManualScrollableCredits(context), - // 하단 탭 힌트 (스크롤 중에만 표시) + // 하단 터치 힌트 (스크롤 중에만 표시) if (!_isScrollComplete) Positioned( bottom: 16, left: 0, right: 0, - child: Text( - L10n.of(context).endingTapToSkip, - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 8, - color: Colors.white.withValues(alpha: 0.2), + child: AnimatedOpacity( + opacity: _isTouching ? 0.0 : 1.0, + duration: const Duration(milliseconds: 200), + child: Text( + l10n.endingHoldToSpeedUp, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + color: Colors.white.withValues(alpha: 0.3), + ), + textAlign: TextAlign.center, + ), + ), + ), + + // 속도업 표시 (터치 중에만 표시) + if (!_isScrollComplete && _isTouching) + Positioned( + top: 16, + right: 16, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: gold.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '▶▶ x5', + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 10, + color: gold, + ), ), - textAlign: TextAlign.center, ), ), ], @@ -162,6 +239,15 @@ class _VictoryOverlayState extends State ); } + /// 수동 스크롤 가능한 크레딧 (스크롤 완료 후) + Widget _buildManualScrollableCredits(BuildContext context) { + return SingleChildScrollView( + controller: _manualScrollController, + physics: const BouncingScrollPhysics(), + child: _buildCreditContent(context), + ); + } + double _estimateContentHeight() { // 대략적인 콘텐츠 높이 추정 (스크롤 계산용) // 명예의 전당 버튼 추가로 인해 높이 증가