diff --git a/app/src/main/java/com/thechance/qurio/data/repository/AchievementsRepositoryImpl.kt b/app/src/main/java/com/thechance/qurio/data/repository/AchievementsRepositoryImpl.kt index b6f7823..96ad097 100644 --- a/app/src/main/java/com/thechance/qurio/data/repository/AchievementsRepositoryImpl.kt +++ b/app/src/main/java/com/thechance/qurio/data/repository/AchievementsRepositoryImpl.kt @@ -8,9 +8,9 @@ import com.thechance.qurio.domain.repository.AchievementsRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject -import javax.inject.Singleton -class AchievementsRepositoryImpl @Inject constructor(private val context: Context) : AchievementsRepository { +class AchievementsRepositoryImpl @Inject constructor(private val context: Context) : + AchievementsRepository { private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) override suspend fun getAllAchievements(): List = withContext(Dispatchers.IO) { @@ -29,12 +29,18 @@ class AchievementsRepositoryImpl @Inject constructor(private val context: Contex (prefs.getStringSet(KEY_UNLOCKED, emptySet())?.toMutableSet() ?: mutableSetOf()) currentAchievementsSet.add(id.toString()) prefs.edit { putStringSet(KEY_UNLOCKED, currentAchievementsSet) } + } override suspend fun isAchievementUnlocked(id: Int): Boolean = withContext(Dispatchers.IO) { (prefs.getStringSet(KEY_UNLOCKED, emptySet()) ?: emptySet()).contains(id.toString()) } + override suspend fun getUnlockedAchievementsCount(): Int { + val unlockedAchievements = prefs.getStringSet(KEY_UNLOCKED, emptySet()) ?: emptySet() + return unlockedAchievements.size + } + companion object { private const val PREFS_NAME = "achievements_prefs" private const val KEY_UNLOCKED = "unlocked_ids" diff --git a/app/src/main/java/com/thechance/qurio/data/repository/GameProgressRepositoryImpl.kt b/app/src/main/java/com/thechance/qurio/data/repository/GameProgressRepositoryImpl.kt new file mode 100644 index 0000000..2113860 --- /dev/null +++ b/app/src/main/java/com/thechance/qurio/data/repository/GameProgressRepositoryImpl.kt @@ -0,0 +1,40 @@ +package com.thechance.qurio.data.repository + +import android.content.Context +import androidx.core.content.edit +import com.thechance.qurio.domain.repository.game.GameProgressRepository +import javax.inject.Inject +import javax.inject.Singleton + +class GameProgressRepositoryImpl @Inject constructor(private val context: Context) : + GameProgressRepository { + private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + override suspend fun getQuizzesCompleted(): Int = prefs.getInt(KEY_QUIZZES_COMPLETED, 0) + + override suspend fun incrementQuizzesCompleted() { + val currentQuizzesCompleted = getQuizzesCompleted() + 1 + prefs.edit { putInt(KEY_QUIZZES_COMPLETED, currentQuizzesCompleted) } + } + + override suspend fun getCategoriesPlayed(): Int = prefs.getInt(KEY_CATEGORIES_PLAYED, 0) + + override suspend fun incrementCategoriesPlayed() { + val currentCategoriesPlayed = getCategoriesPlayed() + 1 + prefs.edit { putInt(KEY_CATEGORIES_PLAYED, currentCategoriesPlayed) } + } + + override suspend fun getQuestionsAnswered(): Int = prefs.getInt(KEY_QUESTIONS_ANSWERED, 0) + + override suspend fun incrementQuestionsAnswered(){ + val currentQuestionsAnswered = getQuestionsAnswered() + 1 + prefs.edit { putInt(KEY_QUESTIONS_ANSWERED, currentQuestionsAnswered) } + } + + companion object { + private const val KEY_QUESTIONS_ANSWERED = "questions_answered" + private const val KEY_QUIZZES_COMPLETED = "quizzes_completed" + private const val KEY_CATEGORIES_PLAYED = "categories_played" + private const val PREFS_NAME = "game_progress_prefs" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/thechance/qurio/di/DataModule.kt b/app/src/main/java/com/thechance/qurio/di/DataModule.kt index 3d30184..4c88d34 100644 --- a/app/src/main/java/com/thechance/qurio/di/DataModule.kt +++ b/app/src/main/java/com/thechance/qurio/di/DataModule.kt @@ -8,13 +8,7 @@ import androidx.datastore.preferences.preferencesDataStoreFile import androidx.room.Room import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.thechance.qurio.data.ApiService -import com.thechance.qurio.data.local.UserDataSource import com.thechance.qurio.data.local.dao.GameSessionDao -import com.thechance.qurio.data.util.LoggingInterceptor -import com.thechance.qurio.presentation.main.QurioApp -import com.thechance.qurio.data.remote.service.GameService -import com.thechance.qurio.data.repository.GameRepositoryImpl -import com.thechance.qurio.domain.repository.game.GameRepository import com.thechance.qurio.data.local.dao.PlayedGameDao import com.thechance.qurio.data.local.database.QurioDatabase import com.thechance.qurio.data.remote.service.GameService diff --git a/app/src/main/java/com/thechance/qurio/di/RepositoryModule.kt b/app/src/main/java/com/thechance/qurio/di/RepositoryModule.kt index df0cb23..9b2dea0 100644 --- a/app/src/main/java/com/thechance/qurio/di/RepositoryModule.kt +++ b/app/src/main/java/com/thechance/qurio/di/RepositoryModule.kt @@ -3,6 +3,7 @@ package com.thechance.qurio.di import com.thechance.qurio.data.repository.AchievementsRepositoryImpl import com.thechance.qurio.data.repository.CharactersRepositoryImpl import com.thechance.qurio.data.repository.ExampleRepositoryImpl +import com.thechance.qurio.data.repository.GameProgressRepositoryImpl import com.thechance.qurio.data.repository.TGameRepository import com.thechance.qurio.data.repository.TGameRepositoryImpl import com.thechance.qurio.data.repository.GameSessionRepository @@ -12,10 +13,10 @@ import com.thechance.qurio.data.repository.UserPreferencesRepositoryImpl import com.thechance.qurio.data.repository.GameRepositoryImpl import com.thechance.qurio.data.repository.UserRepositoryImpl import com.thechance.qurio.domain.repository.AchievementsRepository -import com.thechance.qurio.domain.repository.CharactersRepository import com.thechance.qurio.domain.repository.ExampleRepository import com.thechance.qurio.domain.repository.ResultsRepository import com.thechance.qurio.domain.repository.UserPreferencesRepository +import com.thechance.qurio.domain.repository.game.GameProgressRepository import com.thechance.qurio.domain.repository.game.GameRepository import com.thechance.qurio.domain.repository.user.UserRepository import dagger.Binds @@ -68,4 +69,9 @@ abstract class RepositoryModule { abstract fun bindCharacterRepository( impl: CharactersRepositoryImpl ): CharactersRepository + + @Binds + abstract fun bindGameProgressRepository( + impl: GameProgressRepositoryImpl + ): GameProgressRepository } \ No newline at end of file diff --git a/app/src/main/java/com/thechance/qurio/domain/repository/AchievementsRepository.kt b/app/src/main/java/com/thechance/qurio/domain/repository/AchievementsRepository.kt index 5c9d4dd..17d3d64 100644 --- a/app/src/main/java/com/thechance/qurio/domain/repository/AchievementsRepository.kt +++ b/app/src/main/java/com/thechance/qurio/domain/repository/AchievementsRepository.kt @@ -7,4 +7,5 @@ interface AchievementsRepository { suspend fun getAchievementById(id: Int): Achievement? suspend fun unlockAchievement(id: Int) suspend fun isAchievementUnlocked(id: Int): Boolean + suspend fun getUnlockedAchievementsCount(): Int } \ No newline at end of file diff --git a/app/src/main/java/com/thechance/qurio/domain/repository/game/GameProgressRepository.kt b/app/src/main/java/com/thechance/qurio/domain/repository/game/GameProgressRepository.kt new file mode 100644 index 0000000..5d7ccd6 --- /dev/null +++ b/app/src/main/java/com/thechance/qurio/domain/repository/game/GameProgressRepository.kt @@ -0,0 +1,10 @@ +package com.thechance.qurio.domain.repository.game + +interface GameProgressRepository { + suspend fun incrementQuizzesCompleted() + suspend fun getQuizzesCompleted(): Int + suspend fun incrementCategoriesPlayed() + suspend fun getCategoriesPlayed(): Int + suspend fun incrementQuestionsAnswered() + suspend fun getQuestionsAnswered(): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/thechance/qurio/presentation/main/MainActivity.kt b/app/src/main/java/com/thechance/qurio/presentation/main/MainActivity.kt index 302f8aa..ca14053 100644 --- a/app/src/main/java/com/thechance/qurio/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/thechance/qurio/presentation/main/MainActivity.kt @@ -1,5 +1,6 @@ package com.thechance.qurio.presentation.main +import android.graphics.Color import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen diff --git a/app/src/main/java/com/thechance/qurio/presentation/screen/achievements/AchievementsManager.kt b/app/src/main/java/com/thechance/qurio/presentation/screen/achievements/AchievementsManager.kt new file mode 100644 index 0000000..8b93d96 --- /dev/null +++ b/app/src/main/java/com/thechance/qurio/presentation/screen/achievements/AchievementsManager.kt @@ -0,0 +1,106 @@ +package com.thechance.qurio.presentation.screen.achievements + +import com.thechance.qurio.domain.entity.Achievement +import com.thechance.qurio.domain.repository.AchievementsRepository +import com.thechance.qurio.domain.repository.game.GameProgressRepository +import jakarta.inject.Inject + +class AchievementsManager @Inject constructor( + private val gameProgressRepository: GameProgressRepository, + private val achievementsRepository: AchievementsRepository +) { + + suspend fun checkAndUnlockAchievements( + maxCorrectStreak: Int, + firstAnswerCorrect: Boolean, + scorePercent: Int, + totalQuestions: Int, + correctAnswers: Int, + fastestCorrectAnswerTime: Long + ): List { + val unlocked = mutableListOf() + + checkQuizzesCompleted(unlocked) + checkCategoriesPlayed(unlocked) + checkStreaks(maxCorrectStreak, unlocked) + checkLuckyGuess(firstAnswerCorrect, unlocked) + checkTriviaChamp(scorePercent, unlocked) + checkUntouchable(maxCorrectStreak, unlocked) + checkQuickThinker(fastestCorrectAnswerTime, unlocked) + checkPerfectGame(correctAnswers, totalQuestions, unlocked) + checkKnowledgeSeeker(unlocked) + checkCollector(unlocked) + + return unlocked + } + + private suspend fun checkQuizzesCompleted(unlocked: MutableList) { + gameProgressRepository.incrementQuizzesCompleted() + val quizzesCompleted = gameProgressRepository.getQuizzesCompleted() + + if (quizzesCompleted == 5) unlockAchievement(1, unlocked) + if (quizzesCompleted == 50) unlockAchievement(7, unlocked) + } + + private suspend fun checkCategoriesPlayed(unlocked: MutableList) { + gameProgressRepository.incrementCategoriesPlayed() + if (gameProgressRepository.getCategoriesPlayed() >= 4) + unlockAchievement(4, unlocked) + } + + private suspend fun checkStreaks(maxCorrectStreak: Int, unlocked: MutableList) { + if (maxCorrectStreak >= 3) unlockAchievement(2, unlocked) + } + + private suspend fun checkLuckyGuess( + firstAnswerCorrect: Boolean, + unlocked: MutableList + ) { + if (firstAnswerCorrect) unlockAchievement(3, unlocked) + } + + private suspend fun checkTriviaChamp(scorePercent: Int, unlocked: MutableList) { + if (scorePercent >= 80) unlockAchievement(5, unlocked) + } + + private suspend fun checkUntouchable( + maxCorrectStreak: Int, + unlocked: MutableList + ) { + if (maxCorrectStreak >= 10) unlockAchievement(8, unlocked) + } + + private suspend fun checkQuickThinker( + fastestCorrectAnswerTime: Long, + unlocked: MutableList + ) { + if (fastestCorrectAnswerTime < 3000) unlockAchievement(9, unlocked) + } + + private suspend fun checkPerfectGame( + correctAnswers: Int, + totalQuestions: Int, + unlocked: MutableList + ) { + if (totalQuestions > 0 && correctAnswers == totalQuestions) + unlockAchievement(10, unlocked) + } + + private suspend fun checkKnowledgeSeeker(unlocked: MutableList) { + gameProgressRepository.incrementQuestionsAnswered() + if (gameProgressRepository.getQuestionsAnswered() >= 100) + unlockAchievement(11, unlocked) + } + + private suspend fun checkCollector(unlocked: MutableList) { + val unlockedCount = achievementsRepository.getAllAchievements().count { it.isUnlocked } + if (unlockedCount >= 5) unlockAchievement(6, unlocked) + } + + private suspend fun unlockAchievement(id: Int, unlocked: MutableList) { + if (!achievementsRepository.isAchievementUnlocked(id)) { + achievementsRepository.unlockAchievement(id) + achievementsRepository.getAchievementById(id)?.let { unlocked.add(it) } + } + } +} diff --git a/app/src/main/java/com/thechance/qurio/presentation/screen/results/StartPlayPresenter.kt b/app/src/main/java/com/thechance/qurio/presentation/screen/results/StartPlayPresenter.kt index 4635c84..97d4c6a 100644 --- a/app/src/main/java/com/thechance/qurio/presentation/screen/results/StartPlayPresenter.kt +++ b/app/src/main/java/com/thechance/qurio/presentation/screen/results/StartPlayPresenter.kt @@ -1,16 +1,18 @@ package com.thechance.qurio.presentation.screen.results import android.os.CountDownTimer -import com.thechance.qurio.data.repository.TGameRepository import com.thechance.qurio.data.repository.GameSessionRepository +import com.thechance.qurio.data.repository.TGameRepository import com.thechance.qurio.domain.model.GameSession import com.thechance.qurio.domain.model.Question import com.thechance.qurio.presentation.base.BasePresenter +import com.thechance.qurio.presentation.screen.achievements.AchievementsManager import javax.inject.Inject class StartPlayPresenter @Inject constructor( - private val TGameRepository: TGameRepository, - private val gameSessionRepository: GameSessionRepository + private val tGameRepository: TGameRepository, + private val gameSessionRepository: GameSessionRepository, + private val achievementsManager: AchievementsManager ) : BasePresenter() { private var questions: List = emptyList() @@ -25,24 +27,30 @@ class StartPlayPresenter @Inject constructor( private var totalTimeSeconds = 0L private var timerStartTime = 0L + private var currentStreak = 0 + private var longestStreak = 0 + private var firstAnswerCorrect = false + private var fastestAnswerTime = Long.MAX_VALUE + + fun getQuestions(categoryId: Int) { tryToExecute( - callee = { TGameRepository.fetchQuestions(12, "easy", "multiple", categoryId) }, - onStart = { view?.showLoading() }, + callee = { tGameRepository.fetchQuestions(12, "easy", "multiple", categoryId) }, + onStart = { view.showLoading() }, onSuccess = ::onQuestionsSuccess, - onError = { view?.showError(it) }, - onFinish = { view?.hideLoading() } + onError = { view.showError(it) }, + onFinish = { view.hideLoading() } ) } private fun onQuestionsSuccess(list: List) { questions = list - view?.hideLoading() + view.hideLoading() if (list.isNotEmpty()) { - view?.showQuestions(list) + view.showQuestions(list) showCurrentQuestion() } else { - view?.showMessage("No questions found") + view.showMessage("No questions found") } } @@ -50,15 +58,15 @@ class StartPlayPresenter @Inject constructor( questionChecked = false val q = questions[currentIndex] currentAnswers = mutableListOf().apply { - q.correctAnswer?.let { add(it) } - q.incorrectAnswers?.let { addAll(it.filterNotNull()) } + add(q.correctAnswer) + addAll(q.incorrectAnswers.filterNotNull()) shuffle() } - view?.showQuestion(q, "Q ${currentIndex + 1}/${questions.size}") - view?.showAnswers(currentAnswers) - view?.resetAnswers() + view.showQuestion(q, "Q ${currentIndex + 1}/${questions.size}") + view.showAnswers(currentAnswers) + view.resetAnswers() val isLast = currentIndex == questions.size - 1 - view?.toggleSkipButton(!isLast) + view.toggleSkipButton(!isLast) timerStartTime = System.currentTimeMillis() startTimer() @@ -68,7 +76,7 @@ class StartPlayPresenter @Inject constructor( countDownTimer?.cancel() val durationSeconds = (questionTimeMillis / 1000).toInt() - view?.updateTimer(durationSeconds.toLong(), 1f) + view.updateTimer(durationSeconds.toLong(), 1f) countDownTimer = object : CountDownTimer(questionTimeMillis, 1000) { override fun onTick(millisUntilFinished: Long) { @@ -77,7 +85,7 @@ class StartPlayPresenter @Inject constructor( } override fun onFinish() { - view?.onTimerFinished() + view.onTimerFinished() nextQuestion() } }.start() @@ -86,23 +94,36 @@ class StartPlayPresenter @Inject constructor( fun onCheckButtonClicked(selectedPosition: Int?) { if (!questionChecked) { if (selectedPosition == null) { - view?.showMessage("Select an answer first") + view.showMessage("Select an answer first") return } val question = questions[currentIndex] - val correct = question.correctAnswer ?: "" - view?.highlightAnswers(correct, selectedPosition) + val correct = question.correctAnswer + view.highlightAnswers(correct, selectedPosition) questionChecked = true countDownTimer?.cancel() + val timeTaken = System.currentTimeMillis() - timerStartTime val answer = currentAnswers.getOrNull(selectedPosition) - if (answer == correct) correctCount++ else wrongCount++ + + if (answer == correct) { + correctCount++ + currentStreak++ + if (currentStreak > longestStreak) longestStreak = currentStreak + if (currentIndex == 0) firstAnswerCorrect = true + if (timeTaken < fastestAnswerTime) fastestAnswerTime = timeTaken + } else { + wrongCount++ + currentStreak = 0 + } + + totalTimeSeconds += ((System.currentTimeMillis() - timerStartTime) / 1000).toInt() if (currentIndex == questions.size - 1) { saveGameSession() - view?.showEndOfQuestions() + view.showEndOfQuestions() } } else { @@ -119,7 +140,7 @@ class StartPlayPresenter @Inject constructor( showCurrentQuestion() } else { saveGameSession() - view?.showEndOfQuestions() + view.showEndOfQuestions() } } @@ -157,13 +178,51 @@ class StartPlayPresenter @Inject constructor( totalTimeSeconds = session.totalTimeSeconds, earnedCoins = session.earnedCoins ) - view?.onGameSessionSaved(domainSession) + + checkAchievements() + view.onGameSessionSaved(domainSession) }, - onError = { view?.showError(it) }, + onError = { view.showError(it) }, onFinish = {} ) } + private fun checkAchievements() { + tryToExecute( + callee = { + achievementsManager.checkAndUnlockAchievements( + maxCorrectStreak = getLongestStreak(), + firstAnswerCorrect = wasFirstAnswerCorrect(), + scorePercent = calculateScorePercent(), + totalQuestions = questions.size, + correctAnswers = correctCount, + fastestCorrectAnswerTime = getFastestAnswerTime() + ) + }, + onSuccess = { unlockedAchievements -> + unlockedAchievements.forEach { achievement -> + view.showMessage("Achievement unlocked: ${achievement.name}") + } + }, + onError = { throwable -> + view.showError(throwable) + } + ) + } + + private fun getLongestStreak(): Int = longestStreak + + private fun wasFirstAnswerCorrect(): Boolean = firstAnswerCorrect + + private fun calculateScorePercent(): Int { + if (questions.isEmpty()) return 0 + return ((correctCount.toDouble() / questions.size) * 100).toInt() + } + + private fun getFastestAnswerTime(): Long = + if (fastestAnswerTime == Long.MAX_VALUE) 0 else fastestAnswerTime + + fun destroyTimer() { countDownTimer?.cancel() } diff --git a/app/src/main/res/layout/fragment_games.xml b/app/src/main/res/layout/fragment_games.xml index 6ee6aa1..f93e70e 100644 --- a/app/src/main/res/layout/fragment_games.xml +++ b/app/src/main/res/layout/fragment_games.xml @@ -6,6 +6,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00633e6..76b5f29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ Complete 5 quizzes. Answer 3 questions correctly in a row. - Answer one question correctly without using hints. + Answer the first question of a quiz correctly. Play in at least 4 different categories. Achieve a score of 80% or higher in any quiz. Unlock 5 different achievements.