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
|
||||
|
||||
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.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.TextButton
|
||||
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.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.GameEngine
|
||||
import com.livrets.game.GameResult
|
||||
import com.livrets.game.GameState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Composable
|
||||
@@ -17,5 +46,168 @@ fun GameScreen(
|
||||
onFinish: (GameResult) -> 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