feat: écran de jeu avec pavé numérique, barre de temps et feedback
This commit is contained in:
1 file changed
+194
-2
@@ -1,13 +1,42 @@
|
|||||||
package com.livrets.ui
|
package com.livrets.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ElevatedButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.livrets.game.GameConfig
|
import com.livrets.game.GameConfig
|
||||||
|
import com.livrets.game.GameEngine
|
||||||
import com.livrets.game.GameResult
|
import com.livrets.game.GameResult
|
||||||
|
import com.livrets.game.GameState
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -17,5 +46,168 @@ fun GameScreen(
|
|||||||
onFinish: (GameResult) -> Unit,
|
onFinish: (GameResult) -> Unit,
|
||||||
onQuit: () -> Unit,
|
onQuit: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Jeu") }
|
val engine = remember(config) { GameEngine(config, scope, onFinish = onFinish) }
|
||||||
|
val state by engine.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(engine) { engine.start() }
|
||||||
|
|
||||||
|
var showQuitDialog by remember { mutableStateOf(false) }
|
||||||
|
if (showQuitDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showQuitDialog = false },
|
||||||
|
title = { Text("Quitter la partie ?") },
|
||||||
|
text = { Text("La partie en cours sera abandonnée.") },
|
||||||
|
confirmButton = { TextButton(onClick = onQuit) { Text("Quitter") } },
|
||||||
|
dismissButton = { TextButton(onClick = { showQuitDialog = false }) { Text("Continuer") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val s = state) {
|
||||||
|
is GameState.Playing -> PlayingContent(
|
||||||
|
engine = engine,
|
||||||
|
state = s,
|
||||||
|
progress = s.remainingMs.toFloat() / (config.secondsPerQuestion * 1000L),
|
||||||
|
onQuitRequest = { showQuitDialog = true },
|
||||||
|
)
|
||||||
|
|
||||||
|
is GameState.Feedback -> FeedbackContent(s)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlayingContent(
|
||||||
|
engine: GameEngine,
|
||||||
|
state: GameState.Playing,
|
||||||
|
progress: Float,
|
||||||
|
onQuitRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Question ${state.questionIndex + 1} / ${state.totalQuestions}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
TextButton(onClick = onQuitRequest) { Text("Quitter") }
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress.coerceIn(0f, 1f) },
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.question.toString(),
|
||||||
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.entry.ifEmpty { " " },
|
||||||
|
style = MaterialTheme.typography.displayLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(80.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp)),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Keypad(
|
||||||
|
onDigit = engine::onDigit,
|
||||||
|
onDelete = engine::onDelete,
|
||||||
|
onValidate = engine::onValidate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Keypad(
|
||||||
|
onDigit: (Char) -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onValidate: () -> Unit,
|
||||||
|
) {
|
||||||
|
val rows = listOf(
|
||||||
|
listOf("1", "2", "3"),
|
||||||
|
listOf("4", "5", "6"),
|
||||||
|
listOf("7", "8", "9"),
|
||||||
|
listOf("⌫", "0", "OK"),
|
||||||
|
)
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
for (row in rows) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
for (key in row) {
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = {
|
||||||
|
when (key) {
|
||||||
|
"⌫" -> onDelete()
|
||||||
|
"OK" -> onValidate()
|
||||||
|
else -> onDigit(key.first())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(width = 96.dp, height = 64.dp),
|
||||||
|
) {
|
||||||
|
Text(key, fontSize = 24.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FeedbackContent(state: GameState.Feedback) {
|
||||||
|
val ok = state.record.isCorrect
|
||||||
|
val green = Color(0xFF2E7D32)
|
||||||
|
val red = Color(0xFFC62828)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (ok) green.copy(alpha = 0.15f) else red.copy(alpha = 0.15f))
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (ok) "✓" else "✗",
|
||||||
|
fontSize = 72.sp,
|
||||||
|
color = if (ok) green else red,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = state.record.question.toString(),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
if (!ok) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = if (state.record.givenAnswer == null) "Temps écoulé"
|
||||||
|
else "Votre réponse : ${state.record.givenAnswer}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = red,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Bonne réponse : ${state.record.correctAnswer}",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = green,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user