Add changing space backdrops to Snake (0.76.3)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Orbit TV 0.76.3: Snake backdrops
|
||||
|
||||
Includes 0.76.2, which was never installed. Snake's field now has four dim, still space backdrops: deep space, a nebula, a ringed planet and the edge of a world. A new one fades in over a second and a half every ten bites, then they go round again. They're kept dark so the snake and food stand out.
|
||||
|
||||
Checked: build, lint and all unit tests pass. Not checked until installed: how they look on the TV, which is Paul's call.
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 82
|
||||
versionName = "0.76.2-home-tab"
|
||||
versionCode = 83
|
||||
versionName = "0.76.3-snake-backdrops"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -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.2\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.76.3\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -2,6 +2,8 @@ package tv.orbit.classic
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@@ -19,9 +21,13 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
@@ -92,6 +98,49 @@ private fun hexagon(center: Offset, radius: Float) = Path().apply {
|
||||
close()
|
||||
}
|
||||
|
||||
internal const val SNAKE_BACKDROPS = 4
|
||||
|
||||
/** A new backdrop every ten bites, going round. */
|
||||
internal fun snakeBackdrop(score: Int) = (score / 100) % SNAKE_BACKDROPS
|
||||
|
||||
/** Dim and still, so the snake and the food are always what stands out. */
|
||||
@Composable
|
||||
private fun SnakeBackdrop(scene: Int, modifier: Modifier) {
|
||||
Canvas(modifier) {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val tops = listOf(Color(0xFF040A1E), Color(0xFF0A0620), Color(0xFF03101F), Color(0xFF0B0818))
|
||||
val bottoms = listOf(Color(0xFF071632), Color(0xFF160A30), Color(0xFF061C33), Color(0xFF1A0E1E))
|
||||
drawRect(Brush.verticalGradient(listOf(tops[scene], bottoms[scene])))
|
||||
val stars = java.util.Random(7L + scene)
|
||||
repeat(90) {
|
||||
drawCircle(Color.White.copy(alpha = .12f + stars.nextFloat() * .28f), .8f + stars.nextFloat() * 1.4f, Offset(stars.nextFloat() * w, stars.nextFloat() * h))
|
||||
}
|
||||
when (scene) {
|
||||
// A nebula, soft and off to one side.
|
||||
1 -> {
|
||||
drawCircle(Brush.radialGradient(listOf(Color(0x33A050FF), Color.Transparent), Offset(w * .72f, h * .32f), h * .62f), h * .62f, Offset(w * .72f, h * .32f))
|
||||
drawCircle(Brush.radialGradient(listOf(Color(0x22FF5AA0), Color.Transparent), Offset(w * .3f, h * .7f), h * .5f), h * .5f, Offset(w * .3f, h * .7f))
|
||||
}
|
||||
// A ringed planet in the corner.
|
||||
2 -> {
|
||||
val planet = Offset(w * .84f, h * .8f)
|
||||
val radius = h * .26f
|
||||
drawCircle(Brush.radialGradient(listOf(Color(0x553A64C8), Color(0x331A2A66)), planet - Offset(radius * .4f, radius * .4f), radius * 1.4f), radius, planet)
|
||||
withTransform({ rotate(-14f, planet) }) {
|
||||
drawOval(Color(0x33FFD84A), Offset(planet.x - radius * 1.6f, planet.y - radius * .32f), Size(radius * 3.2f, radius * .64f), style = Stroke(2.5f))
|
||||
}
|
||||
}
|
||||
// The edge of a world, along the bottom.
|
||||
3 -> {
|
||||
val center = Offset(w * .5f, h * 2.1f)
|
||||
drawCircle(Brush.radialGradient(listOf(Color(0x44FF8A3D), Color.Transparent), center, h * 1.3f), h * 1.3f, center)
|
||||
drawCircle(Color(0xFF0B0818), h * 1.18f, center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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<SnakeCell>?, progress: Float): List<Pair<Float, Float>> {
|
||||
val cells = game.body.map { it.x.toFloat() to it.y.toFloat() }.toMutableList()
|
||||
@@ -175,6 +224,7 @@ 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) {
|
||||
Crossfade(snakeBackdrop(game.score), animationSpec = tween(1500), label = "backdrop") { scene -> SnakeBackdrop(scene, Modifier.fillMaxSize()) }
|
||||
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)
|
||||
|
||||
@@ -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.2")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.76.3")
|
||||
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()) {
|
||||
|
||||
@@ -74,4 +74,12 @@ class SnakeGameTest {
|
||||
assertEquals(4f to 5f, snakeLine(moved, before, 1f).first())
|
||||
assertEquals(4f to 5f, snakeLine(moved, null, .5f).first())
|
||||
}
|
||||
|
||||
@Test fun theBackdropChangesEveryTenBitesAndGoesRound() {
|
||||
assertEquals(0, snakeBackdrop(0))
|
||||
assertEquals(0, snakeBackdrop(90))
|
||||
assertEquals(1, snakeBackdrop(100))
|
||||
assertEquals(3, snakeBackdrop(300))
|
||||
assertEquals(0, snakeBackdrop(400))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user