Compare commits
15
Commits
Seafarers
...
b8d3d5909c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d3d5909c | ||
|
|
f967c31368 | ||
|
|
4ed110e68c | ||
|
|
5a2c8a0d33 | ||
|
|
b818997699 | ||
|
|
aeda2924f4 | ||
|
|
2e234c7f20 | ||
|
|
f4f53a93b6 | ||
|
|
5360545398 | ||
|
|
bce9b68ece | ||
|
|
6278f472db | ||
|
|
8c89174c21 | ||
|
|
ee8a4f27d3 | ||
|
|
518f80b998 | ||
|
|
5fdb70cae0 |
@@ -49,8 +49,8 @@ jobs:
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: Build debug APK
|
||||
run: ./gradlew assembleDebug --no-daemon --stacktrace
|
||||
- name: Build release APK
|
||||
run: ./gradlew assembleRelease --no-daemon --stacktrace
|
||||
|
||||
- name: Stage APK for release
|
||||
id: stage
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
test -n "$offset"
|
||||
# Same arithmetic as build.gradle.kts, so the tag names the APK's own number.
|
||||
version="v${series}.$(( ${{ github.run_number }} + offset ))"
|
||||
src="app/build/outputs/apk/debug/app-debug.apk"
|
||||
src="app/build/outputs/apk/release/app-release.apk"
|
||||
dest="ProLibertateGames-${version}.apk"
|
||||
cp "$src" "$dest"
|
||||
# The same build again under a name that never changes. GitHub's
|
||||
|
||||
@@ -104,6 +104,11 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// Signed with the same key as the debug builds, so a release installs
|
||||
// over what is already on a device; a different key would make the
|
||||
// updater's installs fail with a signature mismatch.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
|
||||
@@ -88,6 +88,9 @@ sealed interface Route {
|
||||
data class Section(val menu: GameMenu) : Route
|
||||
data object Settings : Route
|
||||
data object Scorekeeper : Route
|
||||
|
||||
/** Every saved game, to carry on from or delete. */
|
||||
data object Resume : Route
|
||||
data class Setup(val gameId: String) : Route
|
||||
data class Lobby(val gameId: String, val optionsJson: String, val hosting: Boolean) : Route
|
||||
data class Play(
|
||||
@@ -188,10 +191,8 @@ private fun AppContent(
|
||||
// Dev builds show the games that are not finished yet; production
|
||||
// releases list only what can actually be played.
|
||||
showComingSoon = showComingSoon,
|
||||
saved = savedGames(env, stack),
|
||||
onResume = { saved ->
|
||||
push(Route.Play(saved.gameId, saved.config, true, saved.localSeat, saved.stateJson))
|
||||
},
|
||||
hasSaved = savedGames(env, stack).isNotEmpty(),
|
||||
onResume = { push(Route.Resume) },
|
||||
onPickSection = { push(Route.Section(it)) },
|
||||
onJoinGame = { push(Route.Lobby(gameId = "", optionsJson = "{}", hosting = false)) },
|
||||
onScorekeeper = { push(Route.Scorekeeper) },
|
||||
@@ -206,6 +207,25 @@ private fun AppContent(
|
||||
onBack = pop,
|
||||
)
|
||||
|
||||
is Route.Resume -> {
|
||||
// Bumped when a game is deleted, so the list is read again.
|
||||
var deletions by remember(route) { mutableIntStateOf(0) }
|
||||
val saved by produceState(emptyList<SavedMatch>(), route, deletions) {
|
||||
value = withContext(Dispatchers.IO) { env.saves.all() }
|
||||
}
|
||||
ResumeScreen(
|
||||
saved = saved,
|
||||
onResume = { match ->
|
||||
push(Route.Play(match.gameId, match.config, true, match.localSeat, match.stateJson))
|
||||
},
|
||||
onDelete = { match ->
|
||||
env.saves.clear(match.gameId)
|
||||
deletions++
|
||||
},
|
||||
onBack = pop,
|
||||
)
|
||||
}
|
||||
|
||||
is Route.Settings -> SettingsScreen(
|
||||
env = env,
|
||||
settings = settings,
|
||||
@@ -271,8 +291,8 @@ private fun AppContent(
|
||||
@Composable
|
||||
fun MainMenuScreen(
|
||||
showComingSoon: Boolean,
|
||||
saved: List<SavedMatch>,
|
||||
onResume: (SavedMatch) -> Unit,
|
||||
hasSaved: Boolean,
|
||||
onResume: () -> Unit,
|
||||
onPickSection: (GameMenu) -> Unit,
|
||||
onJoinGame: () -> Unit,
|
||||
onScorekeeper: () -> Unit,
|
||||
@@ -322,20 +342,11 @@ fun MainMenuScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Games left half-played, most recent first, above everything
|
||||
// else: coming back to the app is mostly coming back to one.
|
||||
if (saved.isNotEmpty()) {
|
||||
Text(
|
||||
"Continue",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
val now = System.currentTimeMillis()
|
||||
saved.forEach { match ->
|
||||
val title = GameCatalog.byId(match.gameId)?.title ?: match.gameId
|
||||
Button(onClick = { onResume(match) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(savedLine(title, match, now))
|
||||
}
|
||||
// Games left half-played, above everything else: coming back to
|
||||
// the app is mostly coming back to one.
|
||||
if (hasSaved) {
|
||||
Button(onClick = onResume, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Resume game")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.prolibertate.games.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.prolibertate.games.game.GameCatalog
|
||||
import org.prolibertate.games.game.engine.SavedMatch
|
||||
|
||||
/**
|
||||
* Every game that can be carried on from, most recently played first, each with
|
||||
* a way to pick it up and a way to throw it away.
|
||||
*/
|
||||
@Composable
|
||||
fun ResumeScreen(
|
||||
saved: List<SavedMatch>,
|
||||
onResume: (SavedMatch) -> Unit,
|
||||
onDelete: (SavedMatch) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var deleting by remember { mutableStateOf<SavedMatch?>(null) }
|
||||
|
||||
ScreenScaffold(title = "Resume game", onBack = onBack) { modifier ->
|
||||
if (saved.isEmpty()) {
|
||||
Text("No games to resume.", modifier = modifier)
|
||||
} else {
|
||||
val now = System.currentTimeMillis()
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(saved, key = { it.gameId }) { match ->
|
||||
val title = GameCatalog.byId(match.gameId)?.title ?: match.gameId
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(title, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
buildString {
|
||||
append("Saved ${agoText(now, match.savedAtMillis)}")
|
||||
if (match.summary.isNotBlank()) append(" · ${match.summary}")
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { onResume(match) }) { Text("Resume") }
|
||||
TextButton(onClick = { deleting = match }) { Text("Delete") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleting?.let { match ->
|
||||
val title = GameCatalog.byId(match.gameId)?.title ?: match.gameId
|
||||
AlertDialog(
|
||||
onDismissRequest = { deleting = null },
|
||||
title = { Text("Delete this game?") },
|
||||
text = { Text("Your saved game of $title will be removed and cannot be resumed.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { deleting = null; onDelete(match) }) { Text("Delete") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { deleting = null }) { Text("Keep it") } },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,22 @@ fun CrazyEightsScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
|
||||
// Playing an eight is two decisions: the card, then the suit it turns the
|
||||
@@ -68,13 +84,15 @@ fun CrazyEightsScreen(
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Crazy 8s", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Crazy 8s", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ScreenScaffold(title = "Crazy 8s", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Crazy 8s", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val cardWidth = (minOf(maxWidth, maxHeight) * 0.16f).coerceIn(44.dp, 92.dp)
|
||||
|
||||
|
||||
@@ -65,11 +65,28 @@ fun CribbageScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Cribbage", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Cribbage", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
@@ -79,7 +96,8 @@ fun CribbageScreen(
|
||||
// together. Forgotten whenever a new hand is dealt.
|
||||
var chosen by remember(current.handNumber) { mutableStateOf(emptySet<Card>()) }
|
||||
|
||||
ScreenScaffold(title = "Cribbage", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Cribbage", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val cardWidth = (minOf(maxWidth, maxHeight) * 0.16f).coerceIn(44.dp, 92.dp)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -72,18 +73,36 @@ fun EuchreScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
val notice by controller.notice.collectAsState()
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Euchre", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Euchre", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ScreenScaffold(title = "Euchre", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Euchre", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val compact = maxWidth < 600.dp
|
||||
val cardWidth = (minOf(maxWidth, maxHeight) * if (compact) 0.17f else 0.12f)
|
||||
|
||||
@@ -69,18 +69,36 @@ fun GolfScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
var mode by remember { mutableStateOf(PlaceMode.SWAP) }
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Golf", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Golf", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ScreenScaffold(title = "Golf", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Golf", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val shortest = minOf(maxWidth, maxHeight)
|
||||
val ownCard = (shortest * 0.16f).coerceIn(40.dp, 84.dp)
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
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
|
||||
@@ -74,18 +75,36 @@ fun KaiserScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
val notice by controller.notice.collectAsState()
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Kaiser", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Kaiser", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ScreenScaffold(title = "Kaiser", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Kaiser", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val compact = maxWidth < 600.dp
|
||||
val cardWidth = (minOf(maxWidth, maxHeight) * if (compact) 0.15f else 0.11f)
|
||||
|
||||
@@ -20,11 +20,15 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
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.draw.rotate
|
||||
@@ -66,16 +70,34 @@ fun PresidentScreen(
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "President", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "President", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
dialogs()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ScreenScaffold(title = "President", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "President", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val cardWidth = (minOf(maxWidth, maxHeight) * 0.15f).coerceIn(44.dp, 88.dp)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -81,12 +82,29 @@ fun SequenceScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
var selected by remember { mutableStateOf<Card?>(null) }
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Sequence", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Sequence", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
@@ -97,7 +115,8 @@ fun SequenceScreen(
|
||||
val removals = legal.filterIsInstance<RemoveChip>().filter { it.card == selected }
|
||||
val highlighted = (placements.map { it.cell } + removals.map { it.cell }).toSet()
|
||||
|
||||
ScreenScaffold(title = "Sequence", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Sequence", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val wide = maxWidth > maxHeight
|
||||
val boardSide = if (wide) {
|
||||
|
||||
@@ -75,18 +75,36 @@ fun WizardScreen(
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val state by controller.state.collectAsState()
|
||||
val abandoned by controller.abandoned.collectAsState()
|
||||
var confirmingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val endGameAction: @Composable () -> Unit = {
|
||||
TextButton(onClick = { confirmingEnd = true }) { Text("End game") }
|
||||
}
|
||||
val dialogs: @Composable () -> Unit = {
|
||||
abandoned?.let { PatienceLeftDialog(it, onExit, controller::dismissAbandoned) }
|
||||
if (confirmingEnd) {
|
||||
PatienceEndDialog(
|
||||
what = "The game ends here. Anyone else at the table is told you have left.",
|
||||
onEnd = onExit,
|
||||
onKeepPlaying = { confirmingEnd = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
val legal by controller.legalMoves.collectAsState()
|
||||
val notice by controller.notice.collectAsState()
|
||||
|
||||
val current = state
|
||||
if (current == null) {
|
||||
ScreenScaffold(title = "Wizard", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Wizard", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
Box(modifier, contentAlignment = Alignment.Center) { Text("Dealing…") }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ScreenScaffold(title = "Wizard", onBack = onExit) { modifier ->
|
||||
ScreenScaffold(title = "Wizard", onBack = onExit, actions = endGameAction) { modifier ->
|
||||
dialogs()
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val cardWidth = (minOf(maxWidth, maxHeight) * 0.14f).coerceIn(40.dp, 88.dp)
|
||||
|
||||
|
||||
@@ -313,6 +313,10 @@ fun CatanHud(
|
||||
) {
|
||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
HudPlayerStrip(state, seats, pointsOf, style)
|
||||
// Cities & Knights: the ship and the improvement levels are as much part
|
||||
// of the game as the scores, so they sit with them rather than in a panel
|
||||
// this layout does not have room for.
|
||||
if (state.isCk) CatanBarbarians(state, Modifier.padding(horizontal = 8.dp), style)
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
// The island fills the whole area; controls float over its sea margin.
|
||||
ZoomPanBox(Modifier.fillMaxSize().padding(start = 64.dp, end = 64.dp, top = 30.dp, bottom = 44.dp)) {
|
||||
@@ -453,6 +457,16 @@ private fun HudPlayerStrip(
|
||||
Modifier.size(14.dp).clip(CircleShape).background(style.seats[seat]).border(1.dp, style.pieceOutline, CircleShape),
|
||||
)
|
||||
Text(seats.label(seat), fontSize = 12.sp, fontWeight = FontWeight.Bold, maxLines = 1, modifier = Modifier.weight(1f))
|
||||
// Trade, politics, science: the three improvement levels.
|
||||
if (state.isCk) {
|
||||
Text(
|
||||
text = org.prolibertate.games.game.catan.Track.entries.joinToString(" ") {
|
||||
"${it.name.first()}${org.prolibertate.games.game.catan.CatanCk.improvement(state, seat, it)}"
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Text("${pointsOf(seat)} VP · ${state.resourceCount(seat)}🂠", fontSize = 12.sp, maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user