update live score
This commit is contained in:
parent
b500df7296
commit
217f587965
|
@ -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")
|
||||
|
||||
|
|
|
@ -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":[]}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
package com.example.lexilearn.ui.views.pQuiz.pComponentsQuiz
|
||||
|
||||
class AnswerShuffled {
|
||||
}
|
Loading…
Reference in New Issue