...
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// packages/feature_game_spider/lib/screens/spider_game_screen.dart
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -12,8 +11,6 @@ import '../widgets/tableau_pile_widget.dart';
|
||||
import '../widgets/bottom_bar_widget.dart';
|
||||
import '../widgets/card_widget.dart';
|
||||
|
||||
// ❌ [삭제] enum _RankSubmissionStep
|
||||
|
||||
class SpiderGameScreen extends StatefulWidget {
|
||||
const SpiderGameScreen({super.key});
|
||||
|
||||
@@ -34,7 +31,9 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
bool _isDealAnimationRunning = false;
|
||||
bool _isStackAnimationRunning = false;
|
||||
|
||||
// ( _buildGameAppBar, _showSurrenderDialog 는 동일 )
|
||||
// ( ... _buildGameAppBar, _showSurrenderDialog, initState, dispose ... )
|
||||
// ( ... build, _runDealAnimation, _runStackCompletionAnimation ... )
|
||||
// ( ... 이 메서드들은 모두 동일합니다 ... )
|
||||
AppBar _buildGameAppBar(BuildContext context, SpiderGameController controller) {
|
||||
return AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),
|
||||
@@ -89,47 +88,33 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final controller = Provider.of<SpiderGameController>(context, listen: false);
|
||||
|
||||
_controllerListener = () {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
const double horizontalPadding = 10;
|
||||
const double cardGap = 5;
|
||||
final double cardWidth = (screenSize.width - (horizontalPadding * 2) - (cardGap * 9)) / 10;
|
||||
final double cardHeight = cardWidth * 1.45;
|
||||
|
||||
// 🔽 [수정] 덱 분배 애니메이션 (플래그 가드 추가)
|
||||
if (controller.cardsToDealAnimate.isNotEmpty && !_isDealAnimationRunning) {
|
||||
_isDealAnimationRunning = true; // 👈 [잠금]
|
||||
_isDealAnimationRunning = true;
|
||||
debugPrint("[LOG] initState Listener: Detected cardsToDealAnimate. Running animation...");
|
||||
_runDealAnimation(controller, cardWidth, cardHeight);
|
||||
}
|
||||
|
||||
// 🔽 [수정] 스택 완성 애니메이션 (경주 조건 해결 로직)
|
||||
if (controller.cardsToAnimateStack.isNotEmpty && !_isStackAnimationRunning) {
|
||||
_isStackAnimationRunning = true; // 👈 [잠금]
|
||||
|
||||
// [핵심] 큐를 복사하고, 인덱스도 *지금* 읽어서 복사합니다.
|
||||
_isStackAnimationRunning = true;
|
||||
final List<SpiderCard> cardsToAnimate = List.of(controller.cardsToAnimateStack);
|
||||
final int sourceIndex = controller.animationSourcePileIndex;
|
||||
final int targetIndex = controller.animationTargetFoundationIndex;
|
||||
|
||||
// 큐를 즉시 비웁니다.
|
||||
controller.clearStackAnimationTrigger();
|
||||
|
||||
debugPrint("[LOG] initState Listener: Detected cardsToAnimateStack (Source: $sourceIndex). Running animation...");
|
||||
// 복사한 데이터를 인자로 전달합니다.
|
||||
_runStackCompletionAnimation(controller, cardWidth, cardHeight, cardsToAnimate, sourceIndex, targetIndex);
|
||||
}
|
||||
};
|
||||
|
||||
controller.addListener(_controllerListener!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controllerListener != null) {
|
||||
@@ -138,20 +123,16 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] SpiderGameScreen: --- Main Build Method CALLED ---");
|
||||
|
||||
final controller = context.read<SpiderGameController>();
|
||||
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
const double horizontalPadding = 10;
|
||||
const double cardGap = 5;
|
||||
final double cardWidth = (screenSize.width - (horizontalPadding * 2) - (cardGap * 9)) / 10;
|
||||
final double cardHeight = cardWidth * 1.45;
|
||||
final double cardOverlap = cardHeight * 0.4;
|
||||
|
||||
final bool isGameCompleted = context.select((SpiderGameController c) => c.isGameCompleted);
|
||||
if (isGameCompleted && !_isDialogShowing) {
|
||||
_isDialogShowing = true;
|
||||
@@ -161,7 +142,6 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: _buildGameAppBar(context, controller),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
@@ -209,54 +189,42 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (_showDimOverlay)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
|
||||
..._animationOverlays,
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔽 덱 분배 애니메이션 (오버레이)
|
||||
void _runDealAnimation(
|
||||
SpiderGameController controller,
|
||||
double cardWidth,
|
||||
double cardHeight,
|
||||
) {
|
||||
// 🔽 [수정] 덱 분배 애니메이션도 경주 조건을 피하기 위해 인자로 받도록 수정
|
||||
final List<SpiderCard> cardsToDeal = List.of(controller.cardsToDealAnimate);
|
||||
controller.clearDealAnimationTrigger();
|
||||
|
||||
debugPrint("[LOG] _runDealAnimation: Starting. ${cardsToDeal.length} cards.");
|
||||
|
||||
final RenderBox? bodyStackBox = _bodyStackKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final RenderBox? stockBox = _stockKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (bodyStackBox == null || stockBox == null) {
|
||||
debugPrint("[LOG] _runDealAnimation: FAILED (Keys not ready)");
|
||||
_isDealAnimationRunning = false; // 👈 [잠금 해제]
|
||||
_isDealAnimationRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final Offset globalStartPos = stockBox.localToGlobal(Offset.zero);
|
||||
final Offset localStartPos = bodyStackBox.globalToLocal(globalStartPos);
|
||||
|
||||
for (int i = 0; i < cardsToDeal.length; i++) {
|
||||
final card = cardsToDeal[i];
|
||||
final RenderBox? targetBox = _tableauKeys[i].currentContext?.findRenderObject() as RenderBox?;
|
||||
if (targetBox == null) continue;
|
||||
|
||||
final Offset globalEndPos = targetBox.localToGlobal(Offset.zero);
|
||||
final double targetY = globalEndPos.dy + controller.currentState.tableau[i].length * (cardHeight * 0.4);
|
||||
final Offset localEndPos = bodyStackBox.globalToLocal(Offset(globalEndPos.dx, targetY));
|
||||
|
||||
final animationDelayMs = i * 100;
|
||||
final animationDurationMs = 600;
|
||||
|
||||
final overlayEntry = TweenAnimationBuilder<double>(
|
||||
key: ValueKey('deal_${card.id}'),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
@@ -265,7 +233,6 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
final currentPos = Offset.lerp(localStartPos, localEndPos, value)!;
|
||||
final bool isFlipping = value > 0.5;
|
||||
final double rotationY = isFlipping ? (value - 0.5) * 2 * pi : 0;
|
||||
|
||||
return Positioned(
|
||||
left: currentPos.dx,
|
||||
top: currentPos.dy,
|
||||
@@ -284,23 +251,16 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Future.delayed(Duration(milliseconds: animationDelayMs), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationOverlays.add(overlayEntry);
|
||||
});
|
||||
|
||||
setState(() { _animationOverlays.add(overlayEntry); });
|
||||
Future.delayed(Duration(milliseconds: animationDurationMs), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationOverlays.remove(overlayEntry);
|
||||
});
|
||||
|
||||
setState(() { _animationOverlays.remove(overlayEntry); });
|
||||
if (i == cardsToDeal.length - 1) {
|
||||
debugPrint("[LOG] _runDealAnimation: Animation FINISHED. Calling finalizeDealFromStock.");
|
||||
controller.finalizeDealFromStock(cardsToDeal);
|
||||
_isDealAnimationRunning = false; // 👈 [잠금 해제]
|
||||
_isDealAnimationRunning = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -308,44 +268,32 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔽 스택 완성 애니메이션 (오버레이)
|
||||
void _runStackCompletionAnimation(
|
||||
SpiderGameController controller,
|
||||
double cardWidth,
|
||||
double cardHeight,
|
||||
// 🔽 [수정] 인자를 받습니다.
|
||||
List<SpiderCard> cardsToAnimate,
|
||||
int sourceIndex,
|
||||
int targetIndex,
|
||||
) {
|
||||
debugPrint("[LOG] _runStackCompletionAnimation: Starting. ${cardsToAnimate.length} cards from index $sourceIndex.");
|
||||
|
||||
final RenderBox? bodyStackBox = _bodyStackKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final RenderBox? stockBox = _stockKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
// 🔽 [수정] 인자로 받은 sourceIndex 사용
|
||||
final RenderBox? startBox = _tableauKeys[sourceIndex].currentContext?.findRenderObject() as RenderBox?;
|
||||
|
||||
if (bodyStackBox == null || stockBox == null || startBox == null) {
|
||||
debugPrint("[LOG] _runStackCompletionAnimation: FAILED (Keys not ready for index $sourceIndex)");
|
||||
_isStackAnimationRunning = false; // 👈 [잠금 해제]
|
||||
_isStackAnimationRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final Offset globalStartPos = startBox.localToGlobal(Offset.zero);
|
||||
final Offset localStartPos = bodyStackBox.globalToLocal(globalStartPos);
|
||||
// 🔽 [수정] 스택이 제거되기 '전'의 길이를 기준으로 계산 (정확)
|
||||
final double startY = localStartPos.dy + (controller.currentState.tableau[sourceIndex].length - cardsToAnimate.length) * (cardHeight * 0.4);
|
||||
|
||||
// 🔽 [수정] 인자로 받은 targetIndex 사용
|
||||
final Offset globalEndPos = stockBox.localToGlobal(Offset( (targetIndex * (cardWidth * 0.15)) - cardWidth*3, 10));
|
||||
final Offset localEndPos = bodyStackBox.globalToLocal(globalEndPos);
|
||||
|
||||
for (int i = 0; i < cardsToAnimate.length; i++) {
|
||||
final card = cardsToAnimate[i];
|
||||
final animationDelayMs = i * 80;
|
||||
final animationDurationMs = 400;
|
||||
|
||||
final overlayEntry = TweenAnimationBuilder<double>(
|
||||
key: ValueKey('stack_${card.id}'),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
@@ -364,23 +312,16 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Future.delayed(Duration(milliseconds: animationDelayMs), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationOverlays.add(overlayEntry);
|
||||
});
|
||||
|
||||
setState(() { _animationOverlays.add(overlayEntry); });
|
||||
Future.delayed(Duration(milliseconds: animationDurationMs), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationOverlays.remove(overlayEntry);
|
||||
});
|
||||
setState(() { _animationOverlays.remove(overlayEntry); });
|
||||
if (i == cardsToAnimate.length - 1) {
|
||||
debugPrint("[LOG] _runStackCompletionAnimation: Animation FINISHED. Calling finalizeStackCompletion for index $sourceIndex.");
|
||||
// 🔽 [수정] finalize가 어떤 스택을 처리할지 인덱스를 전달
|
||||
controller.finalizeStackCompletion(cardsToAnimate, sourceIndex);
|
||||
_isStackAnimationRunning = false; // 👈 [잠금 해제]
|
||||
_isStackAnimationRunning = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -388,8 +329,8 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔽 [수정] _runGameWinAnimation (팝업 호출 로직 변경)
|
||||
|
||||
/// 🔽 [수정] _runGameWinAnimation (Navigation 로직 변경)
|
||||
void _runGameWinAnimation(
|
||||
BuildContext context,
|
||||
SpiderGameController controller,
|
||||
@@ -405,12 +346,12 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
final Offset localStartPos = bodyStackBox.globalToLocal(globalStartPos);
|
||||
|
||||
for (int i = 0; i < allCards.length; i++) {
|
||||
// ... (애니메이션 오버레이 생성 로직은 동일) ...
|
||||
final card = allCards[i];
|
||||
final animationDelay = Duration(milliseconds: i * 30);
|
||||
final animationDuration = const Duration(milliseconds: 1500);
|
||||
final Offset globalEndPos = Offset(random.nextDouble() * screenSize.width, -cardHeight - (AppBar().preferredSize.height));
|
||||
final Offset localEndPos = bodyStackBox.globalToLocal(globalEndPos);
|
||||
|
||||
final overlayEntry = TweenAnimationBuilder<double>(
|
||||
key: ValueKey('win_${card.id}'),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
@@ -432,41 +373,28 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Future.delayed(animationDelay, () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationOverlays.add(overlayEntry);
|
||||
});
|
||||
|
||||
// ❌ [삭제] 500ms 후에 팝업을 띄우는 로직
|
||||
// if (i == 0) { ... }
|
||||
|
||||
setState(() { _animationOverlays.add(overlayEntry); });
|
||||
Future.delayed(animationDuration, () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationOverlays.remove(overlayEntry);
|
||||
});
|
||||
setState(() { _animationOverlays.remove(overlayEntry); });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 🔽 [추가] 딤 오버레이(배경 어두워짐)는 500ms 뒤에 바로 표시
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted && controller.isGameCompleted) {
|
||||
setState(() { _showDimOverlay = true; });
|
||||
}
|
||||
});
|
||||
|
||||
// 🔽 [추가] 랭킹 팝업은 약 2초 뒤 표시
|
||||
final popupDelay = (allCards.length > 70) ? const Duration(seconds: 2) : const Duration(milliseconds: 500);
|
||||
Future.delayed(popupDelay, () {
|
||||
Future.delayed(popupDelay, () async { // 👈 [수정] async 추가
|
||||
if (mounted && controller.isGameCompleted) {
|
||||
|
||||
// [수정] _showGameCompletedDialog() 호출 대신 공통 화면으로 이동
|
||||
|
||||
// 1. 점수 포맷터 정의
|
||||
String formatSpiderScore(int primary, int? secondary) {
|
||||
final moves = primary.toString();
|
||||
@@ -476,10 +404,7 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
|
||||
// 2. 레벨 저장 콜백 정의
|
||||
Future<void> saveSpiderProgress(String playerName) async {
|
||||
// (playerName은 공통 화면이 IdentityService로 저장)
|
||||
|
||||
final identityService = IdentityService();
|
||||
|
||||
final int currentMaxLevel = await identityService.getMaxUnlockedLevel(gameType: 'SPIDER');
|
||||
if (currentMaxLevel < 99) {
|
||||
if (controller.difficulty.levelIndex >= currentMaxLevel) {
|
||||
@@ -493,8 +418,8 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 화면 이동
|
||||
Navigator.pushReplacement(
|
||||
// 3. [수정] 'pushReplacement' 대신 'await push' 사용
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GameCompletionScreen(
|
||||
@@ -507,16 +432,16 @@ class _SpiderGameScreenState extends State<SpiderGameScreen> {
|
||||
userName: controller.userName,
|
||||
scoreFormatter: formatSpiderScore,
|
||||
onProgressSave: saveSpiderProgress,
|
||||
onScreenClose: () {
|
||||
// 공통 화면에서 '닫기'를 누르면 게임 화면도 닫힘
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
// ❌ onScreenClose 제거
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 4. [추가] 랭킹 화면에서 돌아오면, 게임 화면(self)을 닫고 로비로 돌아감
|
||||
if (mounted && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:service_api/service_api.dart';
|
||||
import 'package:service_api/service_api.dart'; // 👈 SessionNotifier 포함
|
||||
import 'package:feature_common/feature_common.dart';
|
||||
import 'spider_game_screen.dart';
|
||||
import '../models/spider_difficulty.dart';
|
||||
@@ -17,24 +17,42 @@ class SpiderLobbyScreen extends StatefulWidget {
|
||||
class _SpiderLobbyScreenState extends State<SpiderLobbyScreen> {
|
||||
int _maxUnlockedLevel = 1;
|
||||
Map<int, (int, int)> _rankHistory = {};
|
||||
String? _userName;
|
||||
// ❌ String? _userName; (SessionNotifier가 관리)
|
||||
bool _isLoading = false;
|
||||
|
||||
// 🔽 [수정] SessionNotifier를 사용하기 위해 IdentityService 대신 추가
|
||||
late final SessionNotifier _sessionNotifier;
|
||||
final PuzzleService _puzzleService = PuzzleService();
|
||||
final IdentityService _identityService = IdentityService();
|
||||
final IdentityService _identityService = IdentityService(); // 👈 (save/load progress를 위해 유지)
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProgress();
|
||||
// 🔽 [수정] initState에서 SessionNotifier를 read
|
||||
// SessionNotifier의 loadSession()이 먼저 완료되었다고 가정
|
||||
_sessionNotifier = context.read<SessionNotifier>();
|
||||
|
||||
// 🔽 [수정] _loadProgress가 랭킹까지 모두 새로고침 (최초 1회)
|
||||
_loadProgress(forceRefreshRanks: true);
|
||||
}
|
||||
|
||||
// ( _loadProgress 메서드는 이전과 동일 )
|
||||
Future<void> _loadProgress() async {
|
||||
/// 🔽 [수정] _loadProgress 메서드 (SessionNotifier 사용 및 로직 분리)
|
||||
Future<void> _loadProgress({bool forceRefreshRanks = false}) async {
|
||||
// 1. (가벼움) 레벨 정보 새로고침
|
||||
final maxLevel = await _identityService.getMaxUnlockedLevel(gameType: 'SPIDER');
|
||||
final String? myName = await _identityService.getSavedUserName();
|
||||
if (mounted) { setState(() { _maxUnlockedLevel = maxLevel; _userName = myName; }); }
|
||||
if (myName == null) return;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_maxUnlockedLevel = maxLevel;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. (무거움) 랭킹 정보 새로고침 (필요할 때만)
|
||||
if (!forceRefreshRanks) return;
|
||||
|
||||
// 🔽 [수정] _sessionNotifier에서 유저 이름을 가져옴
|
||||
final String? myName = _sessionNotifier.session?.userName;
|
||||
if (myName == null) return; // 게스트이거나 아직 이름 저장을 안 함
|
||||
|
||||
try {
|
||||
final Map<int, int> oldRankMap = await _identityService.getLastSavedRankMap(gameType: 'SPIDER');
|
||||
List<Future<List<GameRankDto>>> rankFutures = [];
|
||||
@@ -64,13 +82,20 @@ class _SpiderLobbyScreenState extends State<SpiderLobbyScreen> {
|
||||
}
|
||||
|
||||
|
||||
/// 🔽 [수정] _startGame 메서드 (UserInfo 주입)
|
||||
/// 🔽 [수정] _startGame 메서드 (SessionNotifier 사용)
|
||||
Future<void> _startGame(SpiderDifficulty level) async {
|
||||
setState(() { _isLoading = true; });
|
||||
|
||||
// 1. [수정] 랭킹 등록에 필요한 정보 미리 로드
|
||||
final String userId = await _identityService.getOrCreateUserId();
|
||||
final String? userName = _userName; // (이미 _loadProgress에서 로드됨)
|
||||
// 1. [수정] SessionNotifier에서 유저 정보 가져오기
|
||||
final session = _sessionNotifier.session;
|
||||
if (session == null) {
|
||||
log("세션이 로드되지 않아 게임을 시작할 수 없습니다.");
|
||||
setState(() { _isLoading = false; });
|
||||
return;
|
||||
}
|
||||
|
||||
final String userId = session.userId;
|
||||
final String? userName = session.userName;
|
||||
|
||||
// 2. 컨트롤러 생성 및 새 게임 시작
|
||||
final gameController = SpiderGameController();
|
||||
@@ -91,36 +116,27 @@ class _SpiderLobbyScreenState extends State<SpiderLobbyScreen> {
|
||||
),
|
||||
);
|
||||
|
||||
_loadProgress();
|
||||
// 🔽 [핵심 수정]
|
||||
// 랭킹(forceRefreshRanks: true)은 새로고침하지 않고,
|
||||
// 레벨 잠금 상태(forceRefreshRanks: false)만 새로고침합니다.
|
||||
_loadProgress(forceRefreshRanks: false);
|
||||
}
|
||||
|
||||
// ( build 메서드는 이전과 동일 )
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 🔽 [수정] ThemeNotifier와 SessionNotifier를 모두 watch
|
||||
context.watch<ThemeNotifier>();
|
||||
context.watch<SessionNotifier>(); // 👈 세션 변경(로그인/로그아웃) 감지
|
||||
|
||||
final bool allLevelsUnlocked = _maxUnlockedLevel >= 9;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return CommonGameShell(
|
||||
title: '스파이더 솔리테어',
|
||||
onRankingPressed: () {
|
||||
final List<GameDifficulty> spiderDifficulties = SpiderDifficulties.allDifficulties
|
||||
.map((level) => GameDifficulty(
|
||||
name: level.name,
|
||||
contextId: level.contextId,
|
||||
))
|
||||
.toList();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RankingScreen(
|
||||
gameType: 'SPIDER',
|
||||
difficulties: spiderDifficulties,
|
||||
initialDifficultyName: SpiderDifficulties.getLevel(_maxUnlockedLevel).name,
|
||||
),
|
||||
),
|
||||
);
|
||||
// ... (랭킹 버튼 로직 동일)
|
||||
},
|
||||
// 🔽 [수정] RefreshIndicator 추가 (당겨서 랭킹 새로고침)
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const double maxContentRatio = 0.6;
|
||||
@@ -132,66 +148,70 @@ class _SpiderLobbyScreenState extends State<SpiderLobbyScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: SpiderDifficulties.allDifficulties.length,
|
||||
itemBuilder: (context, index) {
|
||||
final SpiderDifficulty level = SpiderDifficulties.allDifficulties[index];
|
||||
final bool isUnlocked = allLevelsUnlocked || level.levelIndex <= _maxUnlockedLevel;
|
||||
final (int oldRank, int currentRank) = _rankHistory[level.levelIndex] ?? (0, 0);
|
||||
Widget? trailingWidget = isUnlocked ? const Icon(Icons.play_arrow_rounded) : null;
|
||||
String? subtitleText;
|
||||
Color? subtitleColor;
|
||||
if (currentRank > 0) {
|
||||
String rankStr = "${currentRank}위";
|
||||
if (oldRank > 0) {
|
||||
int change = oldRank - currentRank;
|
||||
if (change > 0) {
|
||||
subtitleText = "$rankStr (▲ $change)";
|
||||
subtitleColor = Colors.green;
|
||||
trailingWidget = const Icon(Icons.arrow_circle_up_rounded, color: Colors.green, size: 28);
|
||||
} else if (change < 0) {
|
||||
subtitleText = "$rankStr (▼ ${change.abs()})";
|
||||
subtitleColor = Colors.red;
|
||||
trailingWidget = const Icon(Icons.arrow_circle_down_rounded, color: Colors.red, size: 28);
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _loadProgress(forceRefreshRanks: true),
|
||||
child: ListView.builder(
|
||||
itemCount: SpiderDifficulties.allDifficulties.length,
|
||||
itemBuilder: (context, index) {
|
||||
// ... (이하 ListTile 로직은 모두 동일)
|
||||
final SpiderDifficulty level = SpiderDifficulties.allDifficulties[index];
|
||||
final bool isUnlocked = allLevelsUnlocked || level.levelIndex <= _maxUnlockedLevel;
|
||||
final (int oldRank, int currentRank) = _rankHistory[level.levelIndex] ?? (0, 0);
|
||||
Widget? trailingWidget = isUnlocked ? const Icon(Icons.play_arrow_rounded) : null;
|
||||
String? subtitleText;
|
||||
Color? subtitleColor;
|
||||
if (currentRank > 0) {
|
||||
String rankStr = "${currentRank}위";
|
||||
if (oldRank > 0) {
|
||||
int change = oldRank - currentRank;
|
||||
if (change > 0) {
|
||||
subtitleText = "$rankStr (▲ $change)";
|
||||
subtitleColor = Colors.green;
|
||||
trailingWidget = const Icon(Icons.arrow_circle_up_rounded, color: Colors.green, size: 28);
|
||||
} else if (change < 0) {
|
||||
subtitleText = "$rankStr (▼ ${change.abs()})";
|
||||
subtitleColor = Colors.red;
|
||||
trailingWidget = const Icon(Icons.arrow_circle_down_rounded, color: Colors.red, size: 28);
|
||||
} else {
|
||||
subtitleText = "$rankStr (유지)";
|
||||
subtitleColor = Colors.grey;
|
||||
trailingWidget = const Icon(Icons.check_circle_outline_rounded, color: Colors.grey, size: 28);
|
||||
}
|
||||
} else {
|
||||
subtitleText = "$rankStr (유지)";
|
||||
subtitleColor = Colors.grey;
|
||||
trailingWidget = const Icon(Icons.check_circle_outline_rounded, color: Colors.grey, size: 28);
|
||||
subtitleText = "$rankStr (신규 진입)";
|
||||
subtitleColor = Colors.blue;
|
||||
trailingWidget = const Icon(Icons.new_releases_rounded, color: Colors.blue, size: 28);
|
||||
}
|
||||
} else {
|
||||
subtitleText = "$rankStr (신규 진입)";
|
||||
subtitleColor = Colors.blue;
|
||||
trailingWidget = const Icon(Icons.new_releases_rounded, color: Colors.blue, size: 28);
|
||||
if (oldRank > 0) {
|
||||
subtitleText = "랭킹 이탈 (이전 ${oldRank}위)";
|
||||
subtitleColor = Colors.orange;
|
||||
trailingWidget = const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (oldRank > 0) {
|
||||
subtitleText = "랭킹 이탈 (이전 ${oldRank}위)";
|
||||
subtitleColor = Colors.orange;
|
||||
trailingWidget = const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28);
|
||||
}
|
||||
}
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
isUnlocked ? Icons.lock_open_rounded : Icons.lock_rounded,
|
||||
color: isUnlocked ? theme.primaryColor : Colors.grey,
|
||||
),
|
||||
title: Text(level.name, style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal,
|
||||
color: isUnlocked ? theme.textTheme.bodyLarge?.color : Colors.grey,
|
||||
)),
|
||||
subtitle: subtitleText != null
|
||||
? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold))
|
||||
: null,
|
||||
trailing: trailingWidget,
|
||||
onTap: isUnlocked && !_isLoading
|
||||
? () => _startGame(level)
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
isUnlocked ? Icons.lock_open_rounded : Icons.lock_rounded,
|
||||
color: isUnlocked ? theme.primaryColor : Colors.grey,
|
||||
),
|
||||
title: Text(level.name, style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal,
|
||||
color: isUnlocked ? theme.textTheme.bodyLarge?.color : Colors.grey,
|
||||
)),
|
||||
subtitle: subtitleText != null
|
||||
? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold))
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
trailing: trailingWidget,
|
||||
onTap: isUnlocked && !_isLoading
|
||||
? () => _startGame(level)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user