From 217f587965a8342bd8bc7681e307e9ab7bed14db Mon Sep 17 00:00:00 2001 From: DimazzP Date: Wed, 26 Feb 2025 00:23:04 +0700 Subject: [PATCH] update live score --- app/build.gradle.kts | 2 +- app/src/main/assets/unlock_lotties.json | 1 + .../ui/components/LottieAnimationComponent.kt | 38 ++++ .../views/pNavMaterial/NavMaterialScreen.kt | 18 +- .../pNavMaterial/NavMaterialViewModel.kt | 7 + .../lexilearn/ui/views/pQuiz/QuizScreen.kt | 69 +++++-- .../lexilearn/ui/views/pQuiz/QuizViewModel.kt | 188 ++++++++++++++---- .../pQuiz/pComponentsQuiz/AnswerShuffled.kt | 4 - 8 files changed, 268 insertions(+), 59 deletions(-) create mode 100644 app/src/main/assets/unlock_lotties.json create mode 100644 app/src/main/java/com/example/lexilearn/ui/components/LottieAnimationComponent.kt delete mode 100644 app/src/main/java/com/example/lexilearn/ui/views/pQuiz/pComponentsQuiz/AnswerShuffled.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 48fdd30..313ec26 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,7 +72,7 @@ dependencies { implementation("io.coil-kt:coil-compose:2.1.0") implementation ("com.google.code.gson:gson:2.10.1") implementation("com.google.accompanist:accompanist-flowlayout:0.30.1") - + implementation("com.airbnb.android:lottie-compose:6.3.0") implementation(platform("com.google.firebase:firebase-bom:32.0.0")) implementation("com.google.firebase:firebase-analytics") diff --git a/app/src/main/assets/unlock_lotties.json b/app/src/main/assets/unlock_lotties.json new file mode 100644 index 0000000..e859364 --- /dev/null +++ b/app/src/main/assets/unlock_lotties.json @@ -0,0 +1 @@ +{"v":"5.5.8","fr":60,"ip":0,"op":181,"w":200,"h":200,"nm":"Locked","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":4,"ty":3,"nm":"Null 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.664],"y":[0.825]},"o":{"x":[1],"y":[0]},"t":3,"s":[-10.402]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.341],"y":[0.229]},"t":6,"s":[0]},{"i":{"x":[0.695],"y":[0.873]},"o":{"x":[1],"y":[0]},"t":9,"s":[8]},{"i":{"x":[0.253],"y":[1]},"o":{"x":[1],"y":[0.779]},"t":12,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":15,"s":[-4]},{"i":{"x":[0.65],"y":[1.342]},"o":{"x":[0.167],"y":[0]},"t":18,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.341],"y":[-0.229]},"t":45,"s":[0]},{"i":{"x":[0.664],"y":[0.773]},"o":{"x":[1],"y":[0]},"t":47,"s":[-8]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.341],"y":[0.229]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":51,"s":[8]},{"t":53,"s":[0]}],"ix":10},"p":{"a":0,"k":[99,149,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[34,34,100],"ix":6}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"shackle","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[3.432,-174.816,0],"to":[0,-5.451,0],"ti":[0,5.451,0]},{"t":69,"s":[3.432,-207.522,0]}],"ix":2},"a":{"a":0,"k":[100.167,89.562,0],"ix":1},"s":{"a":0,"k":[294.118,294.118,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[{"i":[[0,0],[0,0],[-14.045,0],[0,-14.045],[0,0]],"o":[[0,0],[0,-14.045],[14.044,0],[0,0],[0,0]],"v":[[-25.43,42.438],[-25.43,-17.008],[0,-42.438],[25.43,-17.008],[25.36,-0.563]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0],[-14.045,0],[0,-14.045],[0,0]],"o":[[0,0],[0,-14.045],[14.044,0],[0,0],[0,0]],"v":[[-25.43,42.438],[-25.43,-17.008],[0,-42.438],[25.43,-17.008],[25.47,-11.063]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.10980392156862745,0.10980392156862745,0.10980392156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[100.167,89.562],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2.942,-93.479,0],"ix":2},"a":{"a":0,"k":[100,117.217,0],"ix":1},"s":{"a":0,"k":[294.118,294.118,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-4.226],[0,0],[-3.123,0],[0,0],[0,4.204],[0,0],[3.123,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3.123,0],[0,0],[0,4.204],[0,0],[3.123,0],[0,0],[0.021,-4.226],[0,0],[0,0]],"v":[[18.941,-32.783],[0.951,-32.783],[-0.953,-32.783],[-18.942,-32.783],[-32.31,-32.783],[-34.343,-32.783],[-40.01,-25.167],[-40.01,25.144],[-34.343,32.783],[34.32,32.783],[39.989,25.144],[39.989,-25.167],[34.341,-32.783],[32.31,-32.783]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.605,-1.672],[0,0],[1.668,0],[0,0],[0,1.74],[0,0],[0,2.554],[-4.491,0.181],[-0.47,-0.023],[0,-4.814]],"o":[[0,0],[0,1.74],[0,0],[-1.647,0],[0,0],[-1.604,-1.672],[0,-4.814],[0.471,-0.023],[4.492,0.181],[0,2.532]],"v":[[6.428,-0.057],[6.428,15.154],[3.391,18.363],[-3.412,18.363],[-6.45,15.154],[-6.45,-0.057],[-8.974,-6.566],[-0.953,-15.719],[0.951,-15.719],[8.973,-6.566]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.10980392156862745,0.10980392156862745,0.10980392156862745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[100.011,117.217],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/java/com/example/lexilearn/ui/components/LottieAnimationComponent.kt b/app/src/main/java/com/example/lexilearn/ui/components/LottieAnimationComponent.kt new file mode 100644 index 0000000..f76a99f --- /dev/null +++ b/app/src/main/java/com/example/lexilearn/ui/components/LottieAnimationComponent.kt @@ -0,0 +1,38 @@ +import android.util.Log +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.* + +@Composable +fun LottieAnimationComponent( + modifier: Modifier = Modifier, + assetName: String, + playOnce: Boolean = true +) { + val composition by rememberLottieComposition(LottieCompositionSpec.Asset(assetName)) + + var isPlaying by remember { mutableStateOf(false) } + + // Cek apakah composition berhasil dimuat sebelum menampilkan animasi + if (composition != null) { + val progress by animateLottieCompositionAsState( + composition = composition, + isPlaying = isPlaying, + restartOnPlay = !playOnce + ) + + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = modifier + ) + + // Jika playOnce = false, jalankan animasi otomatis + LaunchedEffect(Unit) { + isPlaying = true + } + } else { + Log.e("LottieAnimation", "Failed to load Lottie animation: $assetName") + } +} diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialScreen.kt b/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialScreen.kt index 012c1d9..b943a1f 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialScreen.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialScreen.kt @@ -1,5 +1,6 @@ package com.example.lexilearn.ui.views.pNavMaterial +import LottieAnimationComponent import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -58,6 +59,7 @@ fun NavMaterialScreen(navController: NavController, materialId: String) { } val materialList by viewModel.materialList.collectAsState() + val navLockPos by viewModel.navLockPosition.collectAsState() val textTitle by viewModel.textTitle.observeAsState("") val sizeUnlock by viewModel.sizeUnlock.observeAsState() @@ -82,7 +84,7 @@ fun NavMaterialScreen(navController: NavController, materialId: String) { ) { itemsIndexed(materialList) { index, chunk -> ConstraintLayout(modifier = Modifier.fillMaxWidth()) { - val (space, boxItem, centerLine, lockIcon) = createRefs() + val (space, boxItem, centerLine, lockIcon, lockLottie) = createRefs() Box(modifier = Modifier .background(color = cwhite, shape = RoundedCornerShape(12.dp)) .blur(if ((sizeUnlock ?: 0) < index) 16.dp else 0.dp) @@ -209,6 +211,20 @@ fun NavMaterialScreen(navController: NavController, materialId: String) { end.linkTo(parent.end) } ) + + } else { + if (navLockPos == index) { + LottieAnimationComponent(assetName = "unlock_lotties.json", + playOnce = true, + modifier = Modifier + .size(60.dp) + .constrainAs(lockLottie) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + }) + } } } } diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialViewModel.kt b/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialViewModel.kt index 9cedba4..2a02464 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialViewModel.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pNavMaterial/NavMaterialViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import com.example.lexilearn.data.model.MaterialDataModel import com.example.lexilearn.data.repository.MaterialRepository import com.example.lexilearn.utils.generateNumberList +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -28,6 +29,9 @@ class NavMaterialViewModel(application: Application) : AndroidViewModel(applicat private val _sizeUnlock = MutableLiveData(0) val sizeUnlock: LiveData = _sizeUnlock + private val _navLockPosition = MutableStateFlow(-1) + val navLockPosition: StateFlow = _navLockPosition + private val repository = MaterialRepository(application) private val _materialList = MutableStateFlow>>(emptyList()) @@ -80,6 +84,9 @@ class NavMaterialViewModel(application: Application) : AndroidViewModel(applicat println("numberUnlock update unlock value success $it") fetchMaterial(materialId) } + _navLockPosition.value = value + delay(1800) + _navLockPosition.value = -1 } }else{ println("numberUnlock-else $value, ${_sizeUnlock.value}, ${_materialList.value.size}") diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt index 15e1acd..bd89546 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.Button import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -47,19 +47,18 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateMapOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.zIndex import androidx.constraintlayout.compose.ConstraintLayout -import com.example.lexilearn.R import com.example.lexilearn.data.model.QuestionType import com.example.lexilearn.ui.components.CardQuiz import com.example.lexilearn.ui.components.DraggableAnswerCard import com.example.lexilearn.ui.components.FirebaseImage import com.example.lexilearn.ui.components.GradientQuiz import com.example.lexilearn.ui.components.MyShadowCard +import com.example.lexilearn.ui.theme.cAccent import com.example.lexilearn.ui.theme.ctextGray import com.example.lexilearn.ui.theme.ctextWhite import com.google.accompanist.flowlayout.FlowRow @@ -91,12 +90,18 @@ fun QuizScreen( mutableStateMapOf() } - val maxSize = 120.dp + val maxSize = 90.dp val minSize = 70.dp val dataQuiz by quizViewModel.questionShuffled.collectAsState() val listAnswer by quizViewModel.shuffledAnswerLetters.collectAsState() - val score by quizViewModel.totalScore.observeAsState(0) + + val totalScore by quizViewModel.totalScore.collectAsState() + val liveCurrentScore by quizViewModel.liveCurrentScore.collectAsState() + val formattedTime by quizViewModel.formattedResponseTime.collectAsState() + val scoreReduction by quizViewModel.scoreReduction.collectAsState() + val responseTimeMs by quizViewModel.responseTimeMs.collectAsState() + val quizXOffset = remember { mutableStateMapOf() @@ -174,7 +179,7 @@ fun QuizScreen( ) { GradientQuiz( navController = navController, - headerText = "${typeQuiz}-(${score})", + headerText = "${typeQuiz}", modifier = Modifier.fillMaxSize() ) { ConstraintLayout { @@ -195,6 +200,33 @@ fun QuizScreen( color = ctextWhite ) } + if (typeQuiz == "competition") { + Text( + text = "Total Score: $totalScore", + color = ctextWhite, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Row( + Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Score: $liveCurrentScore", + color = cAccent, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Text( + text = "Time: ${formattedTime}", + color = cAccent, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + } MyShadowCard( modifier = Modifier .padding(12.dp) @@ -220,17 +252,26 @@ fun QuizScreen( modifier = Modifier.padding(12.dp) ) } else if (question?.questionType == QuestionType.AUDIO) { - IconButton( + Button( onClick = { quizViewModel.speakLetter(question!!.question) }, ) { - Icon( - imageVector = Icons.Filled.VolumeUp, - contentDescription = "Speaker Icon", - tint = Color.Black, - modifier = Modifier.size(200.dp) - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Filled.VolumeUp, + contentDescription = "Speaker Icon", + tint = Color.White, + modifier = Modifier.size(50.dp) // Ukuran ikon lebih kecil dari 200dp agar lebih seimbang + ) + Text( + text = "Click", + fontSize = 14.sp, + color = Color.White + ) + } } } } @@ -402,7 +443,7 @@ fun QuizScreen( rectColumnAnswer = it.boundsInWindow() }, ) { - listAnswer.chunked(3).forEach { rowItems -> + listAnswer.chunked(4).forEach { rowItems -> Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt index 7f510b5..ffbb8de 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt @@ -18,8 +18,10 @@ import com.example.lexilearn.domain.models.ModelSpell import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.util.Locale +import kotlin.math.max import kotlin.random.Random class QuizViewModel(application: Application) : AndroidViewModel(application), @@ -120,6 +122,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application), override fun onInit(status: Int) { if (status == TextToSpeech.SUCCESS) { tts?.language = Locale.ENGLISH + tts?.setSpeechRate(0.5f) _isTTSInitialized.value = true } else { _isTTSInitialized.value = false @@ -133,7 +136,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application), } fun checkAnswer(answerString: String): Boolean { - if (answerString == currentQuestion.value?.correctAnswer?.correctWord) { + if (answerString.lowercase() == currentQuestion.value?.correctAnswer?.correctWord?.lowercase()) { return true } return false @@ -184,81 +187,187 @@ class QuizViewModel(application: Application) : AndroidViewModel(application), } // competition ==================================================================== - private val _quizState = MutableLiveData() - val quizState: LiveData get() = _quizState +// private val _quizState = MutableLiveData() +// val quizState: LiveData get() = _quizState +// +// private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini +// val currentScore: LiveData get() = _currentScore +// +// private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi +// val totalScore: LiveData get() = _totalScore +// +// private var questionList = listOf() +// private var currentQuestionIndex = 0 +// private var wrongAttempts = 0 +// private var startTime: Long = 0 +// +// // Konstanta untuk scoring +// private val baseScore = 100 +// private val safeTime = 5 +// private val timePenalty = 3 +// private val wrongPenalty = 5 +// private val minScore = 50 +// private val bonusQuick = 10 +// +// /** 1️⃣ Memulai Kuis **/ +// fun startQuiz(materials: List) { +// if (materials.isNotEmpty()) { +// questionList = materials +// currentQuestionIndex = 0 +// _totalScore.value = 0 // Reset skor saat mulai +// _currentScore.value = 0 +// wrongAttempts = 0 +// startTime = System.currentTimeMillis() +// _quizState.value = +// QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0) +// } else { +// _quizState.value = QuizState.Error("Tidak ada soal tersedia") +// } +// } +// +// /** 2️⃣ Dipanggil saat jawaban dikirim **/ +// fun submitAnswer(isCorrect: Boolean) { +// val responseTime = +// ((System.currentTimeMillis() - startTime) / 1000).toInt() // Hitung waktu otomatis +// +// if (isCorrect) { +// val score = calculateScore(responseTime, wrongAttempts) +// _currentScore.value = score +// _totalScore.value = (_totalScore.value ?: 0) + score // Update total skor +// moveToNextQuestion() +// } else { +// wrongAnswer() +// } +// } +// +// /** 3️⃣ Dipanggil saat jawaban salah **/ +// fun wrongAnswer() { +// wrongAttempts += 1 +// _quizState.value = QuizState.AnswerWrong(wrongAttempts) +// } +// +// /** 4️⃣ Berganti ke soal berikutnya **/ +// private fun moveToNextQuestion() { +// if (currentQuestionIndex < questionList.size - 1) { +// currentQuestionIndex++ +// wrongAttempts = 0 +// startTime = System.currentTimeMillis() +// _quizState.value = +// QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0) +// } else { +// _quizState.value = QuizState.QuizFinished(_totalScore.value ?: 0) +// } +// } +// +// /** 🔢 Perhitungan Skor **/ +// private fun calculateScore(responseTime: Int, wrongAttempts: Int): Int { +// val penaltyTime = maxOf(0, responseTime - safeTime) * timePenalty +// val penaltyWrong = wrongAttempts * wrongPenalty +// var score = baseScore - penaltyTime - penaltyWrong +// +// if (responseTime <= 3) { +// score += bonusQuick +// } +// +// return maxOf(minScore, score) +// } + private val _totalScore = MutableStateFlow(0) + val totalScore: StateFlow get() = _totalScore.asStateFlow() - private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini - val currentScore: LiveData get() = _currentScore + private val _scoreReduction = MutableStateFlow(100) // Nilai awal (100) + val scoreReduction: StateFlow get() = _scoreReduction.asStateFlow() - private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi - val totalScore: LiveData get() = _totalScore + private val _responseTimeMs = MutableStateFlow(0L) // Stopwatch waktu jawaban + val responseTimeMs: StateFlow get() = _responseTimeMs.asStateFlow() + + private val _liveCurrentScore = MutableStateFlow(100) // Skor live + val liveCurrentScore: StateFlow get() = _liveCurrentScore.asStateFlow() + private val _formattedResponseTime = MutableStateFlow("0.0") + val formattedResponseTime: StateFlow get() = _formattedResponseTime.asStateFlow() private var questionList = listOf() private var currentQuestionIndex = 0 private var wrongAttempts = 0 private var startTime: Long = 0 + private var isRunning = false - // Konstanta untuk scoring + // Konstanta skor private val baseScore = 100 - private val safeTime = 5 - private val timePenalty = 3 - private val wrongPenalty = 5 - private val minScore = 50 - private val bonusQuick = 10 + private val safeTime = 5 // Waktu aman tanpa penalti (detik) + private val timePenalty = 1 // Penalti per detik setelah safeTime + private val wrongPenalty = 5 // Penalti per jawaban salah + private val minScore = 50 // Skor minimum per soal + private val bonusQuick = 10 // Bonus jika menjawab dalam ≤ 3 detik - /** 1️⃣ Memulai Kuis **/ + /** Memulai Kuis **/ fun startQuiz(materials: List) { if (materials.isNotEmpty()) { questionList = materials currentQuestionIndex = 0 - _totalScore.value = 0 // Reset skor saat mulai - _currentScore.value = 0 + _totalScore.value = 0 + _scoreReduction.value = baseScore // Reset ke 100 + _responseTimeMs.value = 0 + _liveCurrentScore.value = baseScore wrongAttempts = 0 startTime = System.currentTimeMillis() - _quizState.value = - QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0) - } else { - _quizState.value = QuizState.Error("Tidak ada soal tersedia") + startStopwatch() } } - /** 2️⃣ Dipanggil saat jawaban dikirim **/ - fun submitAnswer(isCorrect: Boolean) { - val responseTime = - ((System.currentTimeMillis() - startTime) / 1000).toInt() // Hitung waktu otomatis + /** Memulai Stopwatch & Update Skor Live **/ + private fun startStopwatch() { + isRunning = true + viewModelScope.launch { + while (isRunning) { + val elapsedTime = System.currentTimeMillis() - startTime + _responseTimeMs.value = elapsedTime // Stopwatch berjalan + _liveCurrentScore.value = calculateRealTimeScore((elapsedTime / 1000).toInt(), wrongAttempts) + _formattedResponseTime.value = String.format("%.1f", elapsedTime / 1000.0) + delay(100) // Update setiap 100ms + } + } + } + /** Saat jawaban dikirim **/ + fun submitAnswer(isCorrect: Boolean) { if (isCorrect) { - val score = calculateScore(responseTime, wrongAttempts) - _currentScore.value = score - _totalScore.value = (_totalScore.value ?: 0) + score // Update total skor - moveToNextQuestion() + isRunning = false // Hentikan stopwatch + val responseTimeSec = (_responseTimeMs.value / 1000).toInt() + val finalScore = calculateRealTimeScore(responseTimeSec, wrongAttempts) // Skor final + + _totalScore.value += finalScore // Tambahkan ke total skor + viewModelScope.launch { + delay(1000) // Tampilkan skor selama 1 detik sebelum pindah ke soal berikutnya + moveToNextQuestion() + } } else { wrongAnswer() } } - /** 3️⃣ Dipanggil saat jawaban salah **/ + /** Saat jawaban salah **/ fun wrongAnswer() { wrongAttempts += 1 - _quizState.value = QuizState.AnswerWrong(wrongAttempts) + _liveCurrentScore.value = calculateRealTimeScore((_responseTimeMs.value / 1000).toInt(), wrongAttempts) } - /** 4️⃣ Berganti ke soal berikutnya **/ + /** Berganti ke soal berikutnya **/ private fun moveToNextQuestion() { if (currentQuestionIndex < questionList.size - 1) { currentQuestionIndex++ wrongAttempts = 0 startTime = System.currentTimeMillis() - _quizState.value = - QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0) - } else { - _quizState.value = QuizState.QuizFinished(_totalScore.value ?: 0) + _scoreReduction.value = baseScore // Reset ke 100 + _liveCurrentScore.value = baseScore + startStopwatch() // Restart stopwatch + }else{ + } } - /** 🔢 Perhitungan Skor **/ - private fun calculateScore(responseTime: Int, wrongAttempts: Int): Int { - val penaltyTime = maxOf(0, responseTime - safeTime) * timePenalty + /** Perhitungan Skor Secara Live **/ + private fun calculateRealTimeScore(responseTime: Int, wrongAttempts: Int): Int { + val penaltyTime = max(0, responseTime - safeTime) * timePenalty val penaltyWrong = wrongAttempts * wrongPenalty var score = baseScore - penaltyTime - penaltyWrong @@ -266,6 +375,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application), score += bonusQuick } - return maxOf(minScore, score) + return max(minScore, score) } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/pComponentsQuiz/AnswerShuffled.kt b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/pComponentsQuiz/AnswerShuffled.kt deleted file mode 100644 index c219233..0000000 --- a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/pComponentsQuiz/AnswerShuffled.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.lexilearn.ui.views.pQuiz.pComponentsQuiz - -class AnswerShuffled { -} \ No newline at end of file