From aa4337de22ae968912272d48d6c72164392c4350 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 14:31:02 +0000 Subject: [PATCH] Smooth Snake, fix Play again and Carry on, and blend the Arcade logo (0.76.1) Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.76.1.md | 5 ++ android/app/build.gradle.kts | 4 +- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../main/java/tv/orbit/classic/OrbitArcade.kt | 55 ++++++++++++------ .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- .../res/drawable-nodpi/orbit_arcade_logo.webp | Bin 10652 -> 19058 bytes .../java/tv/orbit/classic/SnakeGameTest.kt | 11 ++++ design/arcade/arcade_logo_clear.svg | 1 + design/arcade/build-arcade.py | 6 ++ 9 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 VERIFICATION-0.76.1.md create mode 100644 design/arcade/arcade_logo_clear.svg diff --git a/VERIFICATION-0.76.1.md b/VERIFICATION-0.76.1.md new file mode 100644 index 0000000..e5f8c2a --- /dev/null +++ b/VERIFICATION-0.76.1.md @@ -0,0 +1,5 @@ +# Orbit TV 0.76.1: smoother Snake + +From Paul's play. Play again and Carry on no longer drop back to the pause menu: buttons disappearing looked like losing the remote, so now only the whole window losing focus pauses a game. The snake glides between cells, drawn every frame, rather than jumping. The Arcade logo has no background, so it sits into the panel. + +Checked: build, lint and all unit tests pass, including the glide. Not checked until installed: how smooth it is on the box. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7d9559b..525350c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 80 - versionName = "0.76.0-top-nav-arcade" + versionCode = 81 + versionName = "0.76.1-smoother-snake" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index 94d7388..4e47606 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -66,7 +66,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI connection.connectTimeout = 10_000 connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000 connection.setRequestProperty("Accept", "application/json") - connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.76.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.76.1\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt index 441ccb3..1af7e30 100644 --- a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt +++ b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -91,46 +92,65 @@ private fun hexagon(center: Offset, radius: Float) = Path().apply { close() } +/** The snake's line from head to tail, partway between two steps, so it glides from cell to cell rather than jumping. */ +internal fun snakeLine(game: SnakeGame, before: List?, progress: Float): List> { + val cells = game.body.map { it.x.toFloat() to it.y.toFloat() }.toMutableList() + if (before.isNullOrEmpty() || !game.alive) return cells + fun between(from: SnakeCell, to: SnakeCell) = (from.x + (to.x - from.x) * progress) to (from.y + (to.y - from.y) * progress) + cells[0] = between(before.first(), game.body.first()) + cells[cells.lastIndex] = between(before.last(), game.body.last()) + return cells +} + @Composable -private fun SnakeField(game: SnakeGame, modifier: Modifier) { +private fun SnakeField(game: SnakeGame, modifier: Modifier, before: List? = null, progress: Float = 1f) { Canvas(modifier) { val cell = minOf(size.width / game.width, size.height / game.height) val left = (size.width - cell * game.width) / 2 val top = (size.height - cell * game.height) / 2 for (y in 0 until game.height) for (x in 0 until game.width) if ((x + y) % 2 == 0) drawRect(Color.White.copy(alpha = .025f), Offset(left + x * cell, top + y * cell), Size(cell, cell)) - fun center(c: SnakeCell) = Offset(left + (c.x + .5f) * cell, top + (c.y + .5f) * cell) - drawPath(hexagon(center(game.food), cell * .46f), Berry) - game.body.zipWithNext { a, b -> drawLine(Sky, center(a), center(b), strokeWidth = cell * .68f, cap = StrokeCap.Round) } - drawCircle(Color.White, cell * .42f, center(game.body.first())) + fun center(x: Float, y: Float) = Offset(left + (x + .5f) * cell, top + (y + .5f) * cell) + drawPath(hexagon(center(game.food.x.toFloat(), game.food.y.toFloat()), cell * .46f), Berry) + val line = snakeLine(game, before, progress).map { center(it.first, it.second) } + line.zipWithNext { a, b -> drawLine(Sky, a, b, strokeWidth = cell * .68f, cap = StrokeCap.Round) } + drawCircle(Color.White, cell * .42f, line.first()) val look = Offset(game.heading.dx * cell * .16f, game.heading.dy * cell * .16f) - drawCircle(Navy, cell * .13f, center(game.body.first()) + look) + drawCircle(Navy, cell * .13f, line.first() + look) } } -/** Runs until it's left. A reminder or anything else taking the remote pauses it, so nobody loses a game to a pop-up. */ +/** Runs until it's left. A reminder taking over the remote pauses it, so nobody loses a game to a pop-up. */ @Composable private fun SnakeScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Unit) { val random = remember { java.util.Random() } var game by remember { mutableStateOf(SnakeGame.start(30, 22, random)) } + var before by remember { mutableStateOf?>(null) } + var progress by remember { mutableFloatStateOf(1f) } var paused by remember { mutableStateOf(false) } - var hadFocus by remember { mutableStateOf(false) } val focus = remember { FocusRequester() } - LaunchedEffect(Unit) { focus.requestFocus() } + // Only the whole window losing focus counts: buttons coming and going move focus around too. + val windowFocused = LocalWindowInfo.current.isWindowFocused + LaunchedEffect(windowFocused) { if (!windowFocused && game.alive) paused = true } + // Steps on the clock, drawn on every frame between them. LaunchedEffect(paused, game.alive) { + var last = withFrameNanos { it } while (!paused && game.alive) { - delay(game.tickMs) - game = game.step(random) + val time = withFrameNanos { it } + val tick = game.tickMs * 1_000_000f + if (time - last >= tick) { + last += tick.toLong() + before = game.body + game = game.step(random) + } + progress = ((time - last) / tick).coerceIn(0f, 1f) } if (!game.alive && game.score > best) onBest(game.score) } val menuFocus = remember { FocusRequester() } LaunchedEffect(paused, game.alive) { if (paused || !game.alive) menuFocus.requestFocus() else focus.requestFocus() } BackHandler { if (game.alive) paused = !paused else onLeave() } - Row(Modifier.fillMaxSize().background(Navy).padding(20.dp).focusRequester(focus).onFocusChanged { - if (hadFocus && !it.hasFocus && game.alive) paused = true - hadFocus = it.hasFocus - }.focusable().onPreviewKeyEvent { event -> + Row(Modifier.fillMaxSize().background(Navy).padding(20.dp).focusRequester(focus).focusable().onPreviewKeyEvent { event -> val native = event.nativeKeyEvent if (native.action != KeyEvent.ACTION_DOWN || paused || !game.alive) return@onPreviewKeyEvent false val direction = when (native.keyCode) { @@ -155,12 +175,13 @@ private fun SnakeScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Unit) { Text("Back Pause", color = Muted, fontSize = 13.sp) } Box(Modifier.weight(1f).fillMaxHeight().background(Color(0xFF050E2A)).border(2.dp, Blue), contentAlignment = Alignment.Center) { - SnakeField(game, Modifier.fillMaxSize().padding(6.dp)) + SnakeField(game, Modifier.fillMaxSize().padding(6.dp), before, progress) if (paused || !game.alive) Column(Modifier.background(Navy, RoundedCornerShape(6.dp)).border(2.dp, Honey, RoundedCornerShape(6.dp)).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) { Text(if (game.alive) "PAUSED" else "GAME OVER", color = Honey, fontFamily = Marquee, fontSize = 30.sp) if (!game.alive) Text(if (game.score > best) "New best: ${game.score}" else "Score: ${game.score}", color = Color.White, fontSize = 16.sp) OrbitButton(if (game.alive) "Carry on" else "Play again", Modifier.width(200.dp).focusRequester(menuFocus), onClick = { - if (game.alive) paused = false else { game = SnakeGame.start(30, 22, random); paused = false } + if (!game.alive) { game = SnakeGame.start(30, 22, random); before = null; progress = 1f } + paused = false }) OrbitButton("Leave the game", Modifier.width(200.dp), onClick = onLeave) } diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index e39b56c..b611970 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B next.instanceFollowRedirects = false next.connectTimeout = 10_000 next.readTimeout = 20_000 - next.setRequestProperty("User-Agent", "OrbitTV/0.76.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.76.1") next.setRequestProperty("Accept-Encoding", "identity") session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) } if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) { diff --git a/android/app/src/main/res/drawable-nodpi/orbit_arcade_logo.webp b/android/app/src/main/res/drawable-nodpi/orbit_arcade_logo.webp index 6de09c49b00c7eca71418cc4340d01c72b4e526d..8e9a5043ebc525806259378923d31b27847bb1cc 100644 GIT binary patch literal 19058 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

j#TzC@Rx*C@f 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 CjSM~j diff --git a/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt b/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt index a96bc47..d4e1b2d 100644 --- a/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt @@ -62,4 +62,15 @@ class SnakeGameTest { assertFalse(start.food in start.body) assertTrue(start.alive) } + + @Test fun theSnakeGlidesBetweenSteps() { + val before = listOf(SnakeCell(3, 5), SnakeCell(2, 5), SnakeCell(1, 5)) + val moved = game(3 to 5, 2 to 5, 1 to 5).step(random) + val halfway = snakeLine(moved, before, .5f) + assertEquals(3.5f to 5f, halfway.first()) + assertEquals(3f to 5f, halfway[1]) + assertEquals(1.5f to 5f, halfway.last()) + assertEquals(4f to 5f, snakeLine(moved, before, 1f).first()) + assertEquals(4f to 5f, snakeLine(moved, null, .5f).first()) + } } diff --git a/design/arcade/arcade_logo_clear.svg b/design/arcade/arcade_logo_clear.svg new file mode 100644 index 0000000..fa0dac8 --- /dev/null +++ b/design/arcade/arcade_logo_clear.svg @@ -0,0 +1 @@ +ORBITARCADE \ No newline at end of file diff --git a/design/arcade/build-arcade.py b/design/arcade/build-arcade.py index c4fac36..de27dba 100644 --- a/design/arcade/build-arcade.py +++ b/design/arcade/build-arcade.py @@ -44,6 +44,12 @@ def write(name, w, h, body): write('arcade_logo', 1024, 360, '' + stars(1024, 360, 45) + mark(30, 20, .58) + text(366, 108, 'ORBIT', 44, '#F5F7FF', 'Barlow', 'font-weight="600" letter-spacing="16"') + text(362, 262, 'ARCADE', 150, 'url(#honey)', 'Anton', 'letter-spacing="4"') + ''.join(piece(378 + i * 62, 306, 26, c) for i, c in enumerate(PIECES[:5])) + bee(858, 306, .52)) +# The app's copy has no background, so it sits on whatever's behind it. +write('arcade_logo_clear', 1024, 360, mark(30, 20, .58) + text(366, 108, 'ORBIT', 44, '#F5F7FF', 'Barlow', 'font-weight="600" letter-spacing="16"') + + text(362, 262, 'ARCADE', 150, 'url(#honey)', 'Anton', 'letter-spacing="4"') + ''.join(piece(378 + i * 62, 306, 26, c) for i, c in enumerate(PIECES[:5])) + bee(858, 306, .52)) +from PIL import Image +Image.open(HERE / 'arcade_logo_clear.png').convert('RGBA').resize((512, 180), Image.LANCZOS).save(HERE.parents[1] / 'android/app/src/main/res/drawable-nodpi/orbit_arcade_logo.webp', quality=92) + # Icons for the menu, drawn in a box 100 wide around 0,0. def snake(c): return f''