....
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/finddiff_models.dart';
|
||||
|
||||
class FindDiffController with ChangeNotifier {
|
||||
late FindDiffDifficulty difficulty;
|
||||
late String userId;
|
||||
late String? userName;
|
||||
|
||||
List<FindDiffItem> _items = [];
|
||||
List<FindDiffItem> get items => _items;
|
||||
|
||||
// 게임 상태
|
||||
int _currentRound = 0;
|
||||
int get currentRound => _currentRound;
|
||||
|
||||
int _score = 0;
|
||||
int get score => _score;
|
||||
|
||||
int _incorrectCount = 0;
|
||||
int get incorrectCount => _incorrectCount;
|
||||
|
||||
bool _isGameCompleted = false;
|
||||
bool get isGameCompleted => _isGameCompleted;
|
||||
|
||||
// 타이머
|
||||
Timer? _timer;
|
||||
int _remainingTime = 0;
|
||||
int get remainingTime => _remainingTime;
|
||||
|
||||
// [🔥 신규] 게임 시작 여부 (가이드 확인 후 true)
|
||||
bool _isGameStarted = false;
|
||||
bool get isGameStarted => _isGameStarted;
|
||||
|
||||
// [🔥 신규] 피드백 상태
|
||||
bool _showFeedback = false;
|
||||
bool get showFeedback => _showFeedback;
|
||||
bool _isLastAnswerCorrect = false;
|
||||
bool get isLastAnswerCorrect => _isLastAnswerCorrect;
|
||||
|
||||
final Random _random = Random();
|
||||
|
||||
void setUserInfo(String userId, String? userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
void startNewGame(FindDiffDifficulty level) {
|
||||
difficulty = level;
|
||||
_score = 0;
|
||||
_incorrectCount = 0;
|
||||
_currentRound = 1;
|
||||
_isGameCompleted = false;
|
||||
_isGameStarted = false; // 대기 상태
|
||||
_showFeedback = false;
|
||||
|
||||
_generateLevel();
|
||||
// [🔥 수정] 여기서 타이머를 시작하지 않음
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void restartGame() {
|
||||
startNewGame(difficulty);
|
||||
}
|
||||
|
||||
// [🔥 신규] UI에서 호출할 타이머 시작 함수
|
||||
void startGameTimer() {
|
||||
if (_isGameStarted) return;
|
||||
_isGameStarted = true;
|
||||
_startTimer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _generateLevel() {
|
||||
_items.clear();
|
||||
final int totalItems = difficulty.totalItems;
|
||||
final int targetIndex = _random.nextInt(totalItems);
|
||||
|
||||
// 1. 기본 속성 결정
|
||||
IconData baseIcon = Icons.circle;
|
||||
Color baseColor = Colors.blue;
|
||||
double baseAngle = 0.0;
|
||||
|
||||
// 난이도별 속성 설정
|
||||
if (difficulty.diffType == FindDiffType.icon) {
|
||||
final pair = FindDiffDifficulties.iconPairs[_random.nextInt(FindDiffDifficulties.iconPairs.length)];
|
||||
baseIcon = pair[0];
|
||||
} else {
|
||||
baseIcon = FindDiffDifficulties.basicIcons[_random.nextInt(FindDiffDifficulties.basicIcons.length)];
|
||||
baseColor = Color.fromARGB(255, _random.nextInt(200), _random.nextInt(200), _random.nextInt(200));
|
||||
}
|
||||
|
||||
// 2. 아이템 생성
|
||||
for (int i = 0; i < totalItems; i++) {
|
||||
bool isTarget = (i == targetIndex);
|
||||
|
||||
IconData icon = baseIcon;
|
||||
Color color = baseColor;
|
||||
double angle = baseAngle;
|
||||
|
||||
if (isTarget) {
|
||||
// 정답 아이템 변형
|
||||
switch (difficulty.diffType) {
|
||||
case FindDiffType.color:
|
||||
int offset = (difficulty.levelIndex >= 4) ? 30 : 60;
|
||||
color = Color.fromARGB(
|
||||
255,
|
||||
(baseColor.red + offset) % 255,
|
||||
(baseColor.green + offset) % 255,
|
||||
(baseColor.blue + offset) % 255,
|
||||
);
|
||||
break;
|
||||
case FindDiffType.icon:
|
||||
final pair = FindDiffDifficulties.iconPairs.firstWhere((p) => p.contains(baseIcon));
|
||||
icon = (baseIcon == pair[0]) ? pair[1] : pair[0];
|
||||
break;
|
||||
case FindDiffType.rotate:
|
||||
angle = 0.5; // 약 30도 회전
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_items.add(FindDiffItem(
|
||||
id: i,
|
||||
icon: icon,
|
||||
color: color,
|
||||
angle: angle,
|
||||
isTarget: isTarget,
|
||||
));
|
||||
}
|
||||
|
||||
_remainingTime = difficulty.timeLimitSeconds;
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_remainingTime--;
|
||||
|
||||
if (_remainingTime <= 0) {
|
||||
// 시간 초과: 오답 처리 및 다음 문제
|
||||
_handleAnswer(false);
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
void onItemTapped(FindDiffItem item) {
|
||||
if (!_isGameStarted || _isGameCompleted || _showFeedback) return;
|
||||
|
||||
_handleAnswer(item.isTarget);
|
||||
}
|
||||
|
||||
void _handleAnswer(bool isCorrect) {
|
||||
_timer?.cancel(); // 잠시 멈춤
|
||||
_showFeedback = true;
|
||||
_isLastAnswerCorrect = isCorrect;
|
||||
|
||||
if (isCorrect) {
|
||||
_score++;
|
||||
} else {
|
||||
_incorrectCount++;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// 피드백 후 다음 라운드
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (_score >= 10) { // [목표] 10문제 맞추면 클리어
|
||||
_isGameCompleted = true;
|
||||
} else {
|
||||
_currentRound++;
|
||||
_showFeedback = false;
|
||||
_generateLevel();
|
||||
_startTimer(); // 타이머 재개
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:service_api/service_api.dart';
|
||||
|
||||
/// 문제 유형 (무엇이 다른가?)
|
||||
enum FindDiffType {
|
||||
color, // 색상이 다름 (빨강 vs 핑크)
|
||||
icon, // 모양이 다름 (😀 vs 😃)
|
||||
rotate, // 각도가 다름 (↑ vs ↗)
|
||||
}
|
||||
|
||||
/// 개별 아이템 데이터
|
||||
class FindDiffItem {
|
||||
final int id;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final double angle; // 라디안 (0.0 ~ 2*pi)
|
||||
final bool isTarget; // 정답 여부 (다른 그림)
|
||||
|
||||
FindDiffItem({
|
||||
required this.id,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.angle = 0.0,
|
||||
this.isTarget = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// 난이도 정의
|
||||
class FindDiffDifficulty extends GameDifficulty {
|
||||
final int levelIndex;
|
||||
final int rows; // 격자 행
|
||||
final int cols; // 격자 열
|
||||
final int timeLimitSeconds; // 제한 시간
|
||||
final FindDiffType diffType; // 문제 유형
|
||||
|
||||
const FindDiffDifficulty({
|
||||
required this.levelIndex,
|
||||
required super.name,
|
||||
required super.contextId,
|
||||
required this.rows,
|
||||
required this.cols,
|
||||
required this.timeLimitSeconds,
|
||||
required this.diffType,
|
||||
});
|
||||
|
||||
int get totalItems => rows * cols;
|
||||
}
|
||||
|
||||
class FindDiffDifficulties {
|
||||
// --- 아이콘 풀 (모양 찾기용) ---
|
||||
// 서로 비슷하게 생긴 아이콘 쌍 (정답 vs 오답)
|
||||
static const List<List<IconData>> iconPairs = [
|
||||
[Icons.sentiment_satisfied_alt, Icons.sentiment_satisfied], // 웃음 vs 미소
|
||||
[Icons.star, Icons.star_border], // 별 vs 빈별
|
||||
[Icons.check_circle, Icons.check_circle_outline], // 체크 vs 빈체크
|
||||
[Icons.favorite, Icons.favorite_border], // 하트 vs 빈하트
|
||||
[Icons.lock, Icons.lock_open], // 잠금 vs 열림
|
||||
[Icons.volume_up, Icons.volume_down], // 소리 큼 vs 작음
|
||||
[Icons.battery_full, Icons.battery_alert], // 배터리 가득 vs 경고
|
||||
[Icons.signal_wifi_4_bar, Icons.signal_wifi_off], // 와이파이 vs 꺼짐
|
||||
[Icons.brightness_5, Icons.brightness_4], // 해 vs 달
|
||||
[Icons.directions_walk, Icons.directions_run], // 걷기 vs 뛰기
|
||||
];
|
||||
|
||||
// --- 단일 아이콘 풀 (색상/회전 찾기용) ---
|
||||
static const List<IconData> basicIcons = [
|
||||
Icons.circle, Icons.square, Icons.star, Icons.favorite, Icons.change_history,
|
||||
Icons.hexagon, Icons.pentagon, Icons.emoji_emotions, Icons.pets, Icons.flight,
|
||||
];
|
||||
|
||||
// [15단계 난이도 구성]
|
||||
static final List<FindDiffDifficulty> allDifficulties = [
|
||||
// --- Phase 1: 색상 구분 (초급) ---
|
||||
// 색상 차이가 뚜렷함 -> 미세함
|
||||
const FindDiffDifficulty(levelIndex: 1, name: 'Lv. 1: 색상 (2x2)', contextId: 'DIFF_L1_COLOR_2x2', rows: 2, cols: 2, timeLimitSeconds: 10, diffType: FindDiffType.color),
|
||||
const FindDiffDifficulty(levelIndex: 2, name: 'Lv. 2: 색상 (3x3)', contextId: 'DIFF_L2_COLOR_3x3', rows: 3, cols: 3, timeLimitSeconds: 10, diffType: FindDiffType.color),
|
||||
const FindDiffDifficulty(levelIndex: 3, name: 'Lv. 3: 색상 (4x4)', contextId: 'DIFF_L3_COLOR_4x4', rows: 4, cols: 4, timeLimitSeconds: 8, diffType: FindDiffType.color),
|
||||
const FindDiffDifficulty(levelIndex: 4, name: 'Lv. 4: 미세 색상 (4x4)', contextId: 'DIFF_L4_COLOR_HARD', rows: 4, cols: 4, timeLimitSeconds: 6, diffType: FindDiffType.color),
|
||||
|
||||
// --- Phase 2: 모양 구분 (중급) ---
|
||||
// 비슷한 아이콘 찾기
|
||||
const FindDiffDifficulty(levelIndex: 5, name: 'Lv. 5: 모양 (3x3)', contextId: 'DIFF_L5_ICON_3x3', rows: 3, cols: 3, timeLimitSeconds: 10, diffType: FindDiffType.icon),
|
||||
const FindDiffDifficulty(levelIndex: 6, name: 'Lv. 6: 모양 (4x4)', contextId: 'DIFF_L6_ICON_4x4', rows: 4, cols: 4, timeLimitSeconds: 8, diffType: FindDiffType.icon),
|
||||
const FindDiffDifficulty(levelIndex: 7, name: 'Lv. 7: 모양 (5x5)', contextId: 'DIFF_L7_ICON_5x5', rows: 5, cols: 5, timeLimitSeconds: 8, diffType: FindDiffType.icon),
|
||||
const FindDiffDifficulty(levelIndex: 8, name: 'Lv. 8: 모양 (6x6)', contextId: 'DIFF_L8_ICON_6x6', rows: 6, cols: 6, timeLimitSeconds: 8, diffType: FindDiffType.icon),
|
||||
|
||||
// --- Phase 3: 회전 구분 (상급) ---
|
||||
// 같은 아이콘인데 각도가 다름
|
||||
const FindDiffDifficulty(levelIndex: 9, name: 'Lv. 9: 회전 (4x4)', contextId: 'DIFF_L9_ROT_4x4', rows: 4, cols: 4, timeLimitSeconds: 8, diffType: FindDiffType.rotate),
|
||||
const FindDiffDifficulty(levelIndex: 10, name: 'Lv. 10: 회전 (5x5)', contextId: 'DIFF_L10_ROT_5x5', rows: 5, cols: 5, timeLimitSeconds: 7, diffType: FindDiffType.rotate),
|
||||
const FindDiffDifficulty(levelIndex: 11, name: 'Lv. 11: 회전 (6x6)', contextId: 'DIFF_L11_ROT_6x6', rows: 6, cols: 6, timeLimitSeconds: 6, diffType: FindDiffType.rotate),
|
||||
|
||||
// --- Phase 4: 마스터 (대형 그리드 + 짧은 시간) ---
|
||||
const FindDiffDifficulty(levelIndex: 12, name: 'Lv. 12: 마스터 (색상 7x7)', contextId: 'DIFF_L12_COLOR_7x7', rows: 7, cols: 7, timeLimitSeconds: 5, diffType: FindDiffType.color),
|
||||
const FindDiffDifficulty(levelIndex: 13, name: 'Lv. 13: 마스터 (모양 7x7)', contextId: 'DIFF_L13_ICON_7x7', rows: 7, cols: 7, timeLimitSeconds: 5, diffType: FindDiffType.icon),
|
||||
const FindDiffDifficulty(levelIndex: 14, name: 'Lv. 14: 마스터 (회전 7x7)', contextId: 'DIFF_L14_ROT_7x7', rows: 7, cols: 7, timeLimitSeconds: 5, diffType: FindDiffType.rotate),
|
||||
const FindDiffDifficulty(levelIndex: 15, name: 'Lv. 15: 갓모드 (8x8)', contextId: 'DIFF_L15_GOD_8x8', rows: 8, cols: 8, timeLimitSeconds: 4, diffType: FindDiffType.rotate),
|
||||
];
|
||||
|
||||
static FindDiffDifficulty getLevel(int levelIndex) {
|
||||
if (levelIndex < 1) levelIndex = 1;
|
||||
if (levelIndex > allDifficulties.length) levelIndex = allDifficulties.length;
|
||||
return allDifficulties.firstWhere((level) => level.levelIndex == levelIndex,
|
||||
orElse: () => allDifficulties[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// packages/feature_game_finddiff/lib/screens/finddiff_game_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:service_api/service_api.dart';
|
||||
import 'package:feature_common/feature_common.dart';
|
||||
import '../controllers/finddiff_controller.dart';
|
||||
import '../models/finddiff_models.dart';
|
||||
|
||||
class FindDiffGameScreen extends StatefulWidget {
|
||||
const FindDiffGameScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FindDiffGameScreen> createState() => _FindDiffGameScreenState();
|
||||
}
|
||||
|
||||
class _FindDiffGameScreenState extends State<FindDiffGameScreen> {
|
||||
bool _isDialogShowing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_showGameGuide();
|
||||
});
|
||||
}
|
||||
|
||||
void _showGameGuide() {
|
||||
final controller = context.read<FindDiffController>();
|
||||
|
||||
String message = "화면에 있는 그림들 중\n나머지와 '다른 하나'를 찾으세요.";
|
||||
if (controller.difficulty.diffType == FindDiffType.color) {
|
||||
message += "\n(색상이 다릅니다)";
|
||||
} else if (controller.difficulty.diffType == FindDiffType.icon) {
|
||||
message += "\n(모양이 다릅니다)";
|
||||
} else {
|
||||
message += "\n(각도가 다릅니다)";
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("게임 방법", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
content: Text(message, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.startGameTimer();
|
||||
},
|
||||
child: const Text("시작하기"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showGameCompletion(FindDiffController controller) async {
|
||||
String formatScore(int primary, int? secondary) {
|
||||
return '성공: $primary문제 / 오답: ${secondary ?? 0}회';
|
||||
}
|
||||
|
||||
Future<void> saveProgress(String playerName) async {
|
||||
// 10문제 이상 맞춰야 성공으로 인정
|
||||
if (controller.score < 10) return;
|
||||
|
||||
final identityService = IdentityService();
|
||||
final int currentMaxLevel = await identityService.getMaxUnlockedLevel(gameType: 'FIND_DIFF'); // IdentityService에 키 추가 필요
|
||||
if (currentMaxLevel < 99) {
|
||||
if (controller.difficulty.levelIndex >= currentMaxLevel) {
|
||||
int nextLevel = controller.difficulty.levelIndex + 1;
|
||||
if (nextLevel > FindDiffDifficulties.allDifficulties.length) {
|
||||
await identityService.saveMaxUnlockedLevel(99, gameType: 'FIND_DIFF');
|
||||
} else {
|
||||
await identityService.saveMaxUnlockedLevel(nextLevel, gameType: 'FIND_DIFF');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GameCompletionScreen(
|
||||
args: GameResultArgs(
|
||||
gameType: 'FIND_DIFF',
|
||||
contextId: controller.difficulty.contextId,
|
||||
primaryScore: controller.score,
|
||||
secondaryScore: controller.incorrectCount,
|
||||
userId: controller.userId,
|
||||
userName: controller.userName,
|
||||
scoreFormatter: formatScore,
|
||||
onProgressSave: saveProgress,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (mounted && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<FindDiffController>();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (controller.isGameCompleted && !_isDialogShowing) {
|
||||
_isDialogShowing = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_showGameCompletion(controller);
|
||||
});
|
||||
}
|
||||
|
||||
// 시간 임박 경고 색상
|
||||
final Color timeColor = controller.remainingTime <= 3 ? theme.colorScheme.error : theme.colorScheme.onSurface;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.difficulty.name),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'⏳ ${controller.remainingTime}s',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: timeColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 정보 바
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text("목표: ${controller.score}/10", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.primaryColor)),
|
||||
Text("오답: ${controller.incorrectCount}", style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 피드백 오버레이
|
||||
if (controller.showFeedback)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
controller.isLastAnswerCorrect ? Icons.check_circle : Icons.cancel,
|
||||
size: 100,
|
||||
color: controller.isLastAnswerCorrect ? Colors.green : theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
// 게임 그리드
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return GridView.builder(
|
||||
itemCount: controller.items.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: controller.difficulty.cols,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(controller.items[index], controller);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(FindDiffItem item, FindDiffController controller) {
|
||||
return GestureDetector(
|
||||
onTap: () => controller.onItemTapped(item),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.black12),
|
||||
),
|
||||
child: Transform.rotate(
|
||||
angle: item.angle,
|
||||
child: Icon(
|
||||
item.icon,
|
||||
size: 40, // 동적 크기 조절이 필요하면 LayoutBuilder 활용 가능
|
||||
color: item.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
// [C] 공통 서비스
|
||||
import 'package:service_api/service_api.dart';
|
||||
// [A] 공통 UI 셸
|
||||
import 'package:feature_common/feature_common.dart';
|
||||
// [B] 다른 그림 찾기 모델/컨트롤러
|
||||
import '../models/finddiff_models.dart';
|
||||
import '../controllers/finddiff_controller.dart';
|
||||
import 'finddiff_game_screen.dart';
|
||||
|
||||
class FindDiffLobbyScreen extends StatefulWidget {
|
||||
const FindDiffLobbyScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FindDiffLobbyScreen> createState() => _FindDiffLobbyScreenState();
|
||||
}
|
||||
|
||||
class _FindDiffLobbyScreenState extends State<FindDiffLobbyScreen> {
|
||||
int _maxUnlockedLevel = 1;
|
||||
Map<int, (int, int)> _rankHistory = {};
|
||||
bool _isLoading = false;
|
||||
|
||||
late final SessionNotifier _sessionNotifier;
|
||||
late final LobbyHelperService _lobbyHelper;
|
||||
final PuzzleService _puzzleService = PuzzleService();
|
||||
final IdentityService _identityService = IdentityService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sessionNotifier = context.read<SessionNotifier>();
|
||||
|
||||
// 헬퍼 서비스 초기화
|
||||
_lobbyHelper = LobbyHelperService(
|
||||
identityService: _identityService,
|
||||
puzzleService: _puzzleService,
|
||||
);
|
||||
|
||||
_loadProgress(forceRefreshRanks: true);
|
||||
}
|
||||
|
||||
/// [공통 로직] 레벨 잠금 상태 및 랭킹 이력 로드
|
||||
Future<void> _loadProgress({bool forceRefreshRanks = false}) async {
|
||||
// 1. 최대 레벨 로드 ('FIND_DIFF' 키 사용)
|
||||
final maxLevel = await _lobbyHelper.loadMaxLevel('FIND_DIFF');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_maxUnlockedLevel = maxLevel;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 랭킹 이력 로드
|
||||
if (!forceRefreshRanks) return;
|
||||
final String? myName = _sessionNotifier.session?.userName;
|
||||
if (myName == null) return;
|
||||
|
||||
try {
|
||||
final rankHistory = await _lobbyHelper.loadRankHistory<FindDiffDifficulty>(
|
||||
gameType: 'FIND_DIFF',
|
||||
myName: myName,
|
||||
allLevels: FindDiffDifficulties.allDifficulties,
|
||||
getLevelIndex: (level) => level.levelIndex,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_rankHistory = rankHistory;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log("FindDiffLobbyScreen: 랭킹 확인 실패: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// [게임 시작] 컨트롤러 생성 및 화면 이동
|
||||
Future<void> _startGame(FindDiffDifficulty level) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final session = _sessionNotifier.session;
|
||||
if (session == null) {
|
||||
log("세션이 로드되지 않아 게임을 시작할 수 없습니다.");
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final String userId = session.userId;
|
||||
final String? userName = session.userName;
|
||||
|
||||
// 1. 컨트롤러 생성 및 시작
|
||||
final gameController = FindDiffController();
|
||||
gameController.setUserInfo(userId, userName);
|
||||
gameController.startNewGame(level);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
if (!mounted) return;
|
||||
|
||||
// 2. 게임 화면으로 이동
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider.value(
|
||||
value: gameController,
|
||||
child: const FindDiffGameScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 3. 복귀 후 레벨 상태 갱신
|
||||
_loadProgress(forceRefreshRanks: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
context.watch<ThemeNotifier>();
|
||||
context.watch<SessionNotifier>();
|
||||
|
||||
final bool allLevelsUnlocked =
|
||||
_maxUnlockedLevel >= FindDiffDifficulties.allDifficulties.length;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return CommonGameShell(
|
||||
title: '다른 그림 찾기',
|
||||
onRankingPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RankingScreen(
|
||||
gameType: 'FIND_DIFF',
|
||||
difficulties: FindDiffDifficulties.allDifficulties,
|
||||
initialDifficultyName:
|
||||
FindDiffDifficulties.getLevel(_maxUnlockedLevel).name,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const double maxContentRatio = 0.6;
|
||||
final double constrainedWidth =
|
||||
(constraints.maxHeight * maxContentRatio) > 500
|
||||
? 500
|
||||
: (constraints.maxHeight * maxContentRatio);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: constrainedWidth),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _loadProgress(forceRefreshRanks: true),
|
||||
child: ListView.builder(
|
||||
itemCount: FindDiffDifficulties.allDifficulties.length,
|
||||
itemBuilder: (context, index) {
|
||||
final FindDiffDifficulty level =
|
||||
FindDiffDifficulties.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.blue;
|
||||
trailingWidget = const Icon(
|
||||
Icons.new_releases_rounded,
|
||||
color: Colors.blue,
|
||||
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)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user