update live score

This commit is contained in:
DimazzP 2025-02-26 00:23:04 +07:00
parent b500df7296
commit 217f587965
8 changed files with 268 additions and 59 deletions

View File

@ -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")

View File

@ -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":[]}

View File

@ -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")
}
}

View File

@ -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)
})
}
}
}
}

View File

@ -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<Int> = _sizeUnlock
private val _navLockPosition = MutableStateFlow(-1)
val navLockPosition: StateFlow<Int> = _navLockPosition
private val repository = MaterialRepository(application)
private val _materialList = MutableStateFlow<List<List<MaterialDataModel>>>(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}")

View File

@ -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<Int, Dp>()
}
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<Int, Float>()
@ -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

View File

@ -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<QuizState>()
val quizState: LiveData<QuizState> get() = _quizState
// private val _quizState = MutableLiveData<QuizState>()
// val quizState: LiveData<QuizState> get() = _quizState
//
// private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini
// val currentScore: LiveData<Int> get() = _currentScore
//
// private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi
// val totalScore: LiveData<Int> get() = _totalScore
//
// private var questionList = listOf<MaterialDataModel>()
// 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<MaterialDataModel>) {
// 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<Int> get() = _totalScore.asStateFlow()
private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini
val currentScore: LiveData<Int> get() = _currentScore
private val _scoreReduction = MutableStateFlow(100) // Nilai awal (100)
val scoreReduction: StateFlow<Int> get() = _scoreReduction.asStateFlow()
private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi
val totalScore: LiveData<Int> get() = _totalScore
private val _responseTimeMs = MutableStateFlow(0L) // Stopwatch waktu jawaban
val responseTimeMs: StateFlow<Long> get() = _responseTimeMs.asStateFlow()
private val _liveCurrentScore = MutableStateFlow(100) // Skor live
val liveCurrentScore: StateFlow<Int> get() = _liveCurrentScore.asStateFlow()
private val _formattedResponseTime = MutableStateFlow("0.0")
val formattedResponseTime: StateFlow<String> get() = _formattedResponseTime.asStateFlow()
private var questionList = listOf<MaterialDataModel>()
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<MaterialDataModel>) {
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)
}
}

View File

@ -1,4 +0,0 @@
package com.example.lexilearn.ui.views.pQuiz.pComponentsQuiz
class AnswerShuffled {
}