From 32bf2d0992e48d75cde47a78fb168d060861f1c3 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 29 Jul 2026 03:12:37 -0400 Subject: [PATCH] Release LCDPet v1.2 --- app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/assets/audio_credits.txt | 42 + app/src/main/assets/how_to.txt | 785 +++++- .../notfeelingit/lcdpet/DowntimeActivities.kt | 258 +- .../lcdpet/LCDPetNotificationWorker.kt | 266 ++ .../today/notfeelingit/lcdpet/MainActivity.kt | 2272 ++++++++++++++--- .../today/notfeelingit/lcdpet/PetStorage.kt | 267 ++ .../notfeelingit/lcdpet/ui/theme/Theme.kt | 23 +- app/src/main/res/raw/need_urgent.ogg | Bin 0 -> 36658 bytes app/src/main/res/raw/pet_eating.ogg | Bin 0 -> 13318 bytes app/src/main/res/raw/pet_excited.ogg | Bin 0 -> 6784 bytes app/src/main/res/raw/pet_sick.ogg | Bin 0 -> 28291 bytes .../metadata/android/en-US/changelogs/3.txt | 10 + 14 files changed, 3495 insertions(+), 434 deletions(-) create mode 100644 app/src/main/java/today/notfeelingit/lcdpet/LCDPetNotificationWorker.kt create mode 100644 app/src/main/res/raw/need_urgent.ogg create mode 100644 app/src/main/res/raw/pet_eating.ogg create mode 100644 app/src/main/res/raw/pet_excited.ogg create mode 100644 app/src/main/res/raw/pet_sick.ogg create mode 100644 fastlane/metadata/android/en-US/changelogs/3.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f1e45b6..50199d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "today.notfeelingit.lcdpet" minSdk = 26 targetSdk = 36 - versionCode = 2 - versionName = "1.1" + versionCode = 3 + versionName = "1.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -47,6 +47,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation("androidx.datastore:datastore-preferences:1.2.1") + implementation("androidx.work:work-runtime-ktx:2.11.2") testImplementation(libs.junit) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd18db4..5b9e6aa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + grassWidthPx = coordinates.size.width } - .pointerInput(Unit) { + .pointerInput(grassColumns) { detectDragGestures( onDragStart = { offset -> updateGrassFinger(offset.x) @@ -128,19 +147,59 @@ fun WatchTvActivityScreen( // 1 = on, 2 = CRT shutting off, 0 = off val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var appInForeground by remember { + mutableStateOf( + lifecycleOwner.lifecycle.currentState.isAtLeast( + Lifecycle.State.RESUMED + ) + ) + } + var weatherChannelPlayer by remember { + mutableStateOf(null) + } + var staticChannelPlayer by remember { + mutableStateOf(null) + } + + val tvTextFontSize = with(LocalDensity.current) { 14.dp.toSp() } + val tvTextLineHeight = with(LocalDensity.current) { 18.dp.toSp() } + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + appInForeground = true + } + + Lifecycle.Event.ON_PAUSE, + Lifecycle.Event.ON_STOP -> { + appInForeground = false + } + + else -> Unit + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } LaunchedEffect(selectedChannel, tvPowerState) { tvFrame = 0 while (true) { - delay(if (tvPowerState == 2) 120L else 450L) + delay(if (tvPowerState == 2) 145L else 450L) tvFrame += 1 } } LaunchedEffect(tvPowerState) { if (tvPowerState == 2) { - delay(420L) + delay(580L) tvPowerState = 0 } } @@ -158,7 +217,12 @@ fun WatchTvActivityScreen( start() } - while (powerOffPlayer?.isPlaying == true) { + while ( + powerOffPlayer?.isPlaying == true && + lifecycleOwner.lifecycle.currentState.isAtLeast( + Lifecycle.State.RESUMED + ) + ) { delay(50L) } } finally { @@ -167,34 +231,91 @@ fun WatchTvActivityScreen( } } - DisposableEffect(selectedChannel, tvPowerState) { - var player: MediaPlayer? = null - - if (tvPowerState == 1) { - val soundResource = when (selectedChannel) { - TvChannel.Weather -> R.raw.weather_channel_jazz - TvChannel.NoSignal -> R.raw.tv_static_light + DisposableEffect(tvPowerState) { + val weatherPlayer = + if (tvPowerState == 1) { + MediaPlayer.create( + context, + R.raw.weather_channel_jazz + )?.apply { + isLooping = true + setVolume(0f, 0f) + start() + } + } else { + null } - val channelVolume = when (selectedChannel) { - TvChannel.Weather -> 0.75f - TvChannel.NoSignal -> 0.60f + val staticPlayer = + if (tvPowerState == 1) { + MediaPlayer.create( + context, + R.raw.tv_static_light + )?.apply { + isLooping = true + setVolume(0f, 0f) + start() + } + } else { + null } - player = MediaPlayer.create(context, soundResource)?.apply { - isLooping = true - setVolume(channelVolume, channelVolume) - start() - } - } + weatherChannelPlayer = weatherPlayer + staticChannelPlayer = staticPlayer onDispose { - player?.stop() - player?.release() - player = null + if (weatherChannelPlayer === weatherPlayer) { + weatherChannelPlayer = null + } + if (staticChannelPlayer === staticPlayer) { + staticChannelPlayer = null + } + + listOf(weatherPlayer, staticPlayer).forEach { player -> + runCatching { + if (player?.isPlaying == true) { + player.stop() + } + } + player?.release() + } } } + LaunchedEffect( + appInForeground, + tvPowerState, + selectedChannel, + weatherChannelPlayer, + staticChannelPlayer + ) { + val canHearTv = + appInForeground && tvPowerState == 1 + + val weatherVolume = + if (canHearTv && selectedChannel == TvChannel.Weather) { + 0.75f + } else { + 0f + } + + val staticVolume = + if (canHearTv && selectedChannel == TvChannel.NoSignal) { + 0.60f + } else { + 0f + } + + weatherChannelPlayer?.setVolume( + weatherVolume, + weatherVolume + ) + staticChannelPlayer?.setVolume( + staticVolume, + staticVolume + ) + } + Column( modifier = Modifier .fillMaxWidth() @@ -267,25 +388,73 @@ fun WatchTvActivityScreen( .padding(horizontal = 10.dp, vertical = 8.dp), contentAlignment = Alignment.Center ) { - Text( - text = if (tvPowerState == 1) { - tvPreviewText( - channel = selectedChannel, - frame = tvFrame, - weatherLabel = weatherLabel + if ( + tvPowerState == 1 && + selectedChannel == TvChannel.Weather + ) { + val weatherLines = weatherPreviewText(weatherLabel).lines() + val weatherGraphic = weatherLines.joinToString("\n") { + it.take(11) + } + val weatherDetails = weatherLines.joinToString("\n") { + it.drop(15) + } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = weatherGraphic, + fontFamily = FontFamily.Monospace, + fontSize = tvTextFontSize, + lineHeight = tvTextLineHeight, + color = lcdDark, + modifier = Modifier.width(92.dp), + softWrap = false, + maxLines = 3 ) - } else { - tvPowerOffPreviewText( - frame = tvFrame, - shuttingDown = tvPowerState == 2 + + Spacer(modifier = Modifier.width(34.dp)) + + Text( + text = weatherDetails, + fontFamily = FontFamily.Monospace, + fontSize = tvTextFontSize, + lineHeight = tvTextLineHeight, + color = lcdDark, + modifier = Modifier.offset(x = (-10).dp), + softWrap = false, + maxLines = 3 ) - }, - fontFamily = FontFamily.Monospace, - textAlign = if (tvPowerState == 1) TextAlign.Start else TextAlign.Center, - color = if (tvPowerState == 1) lcdDark else Color.White, - modifier = Modifier.fillMaxWidth(), - softWrap = !(tvPowerState == 1 && selectedChannel == TvChannel.NoSignal) - ) + } + } else { + Text( + text = if (tvPowerState == 1) { + tvPreviewText( + channel = selectedChannel, + frame = tvFrame, + weatherLabel = weatherLabel + ) + } else { + tvPowerOffPreviewText( + frame = tvFrame, + shuttingDown = tvPowerState == 2 + ) + }, + fontFamily = FontFamily.Monospace, + fontSize = tvTextFontSize, + lineHeight = tvTextLineHeight, + textAlign = if (tvPowerState == 1) { + TextAlign.Start + } else { + TextAlign.Center + }, + color = if (tvPowerState == 1) lcdDark else Color.White, + modifier = Modifier.fillMaxWidth(), + softWrap = false, + maxLines = 5 + ) + } } } @@ -418,12 +587,19 @@ private fun weatherPreviewText(weatherLabel: String): String { val normalizedWeather = weatherLabel.lowercase() val conditionText = when { + normalizedWeather.contains("snow") -> "SNOWING" normalizedWeather.contains("rain") -> "RAINING" normalizedWeather.contains("cloud") || normalizedWeather.contains("overcast") -> "CLOUDY" else -> "SUNNY" } val iconLines = when (conditionText) { + "SNOWING" -> listOf( + " .--. ", + " (____) ", + " * * * " + ) + "RAINING" -> listOf( " .--. ", " (____) ", diff --git a/app/src/main/java/today/notfeelingit/lcdpet/LCDPetNotificationWorker.kt b/app/src/main/java/today/notfeelingit/lcdpet/LCDPetNotificationWorker.kt new file mode 100644 index 0000000..ea04460 --- /dev/null +++ b/app/src/main/java/today/notfeelingit/lcdpet/LCDPetNotificationWorker.kt @@ -0,0 +1,266 @@ +package today.notfeelingit.lcdpet + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.media.AudioAttributes +import android.net.Uri +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import java.util.concurrent.TimeUnit + +private const val LCDPET_WORKER_CHANNEL_ID = "lcdpet_alerts" +private const val LCDPET_WORKER_URGENT_CHANNEL_ID = "lcdpet_urgent_alerts_v2" + +private const val INPUT_WORK_TYPE = "work_type" +private const val INPUT_NOTIFICATION_ID = "notification_id" +private const val INPUT_NOTIFICATION_TITLE = "notification_title" +private const val INPUT_NOTIFICATION_MESSAGE = "notification_message" +private const val INPUT_URGENT_NEED = "urgent_need" + +private const val WORK_TYPE_NOTIFICATION = "notification" +private const val WORK_TYPE_WANT = "want" + +private const val LCDPET_BACKGROUND_TEST_WORK = "lcdpet_background_test" + +class LCDPetNotificationWorker( + appContext: Context, + workerParams: WorkerParameters +) : CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + val workType = + inputData.getString(INPUT_WORK_TYPE) ?: WORK_TYPE_NOTIFICATION + + if (workType == WORK_TYPE_WANT) { + val createdWant = + applicationContext.createLCDPetWantIfDue() + ?: return Result.success() + + sendLCDPetWorkerNotification( + context = applicationContext, + notificationId = 2001, + title = "LCDPet", + message = wantNotificationText(createdWant) + ) + + return Result.success() + } + + val notificationId = inputData.getInt(INPUT_NOTIFICATION_ID, -1) + val title = inputData.getString(INPUT_NOTIFICATION_TITLE) ?: "LCDPet" + val message = inputData.getString(INPUT_NOTIFICATION_MESSAGE).orEmpty() + val urgentNeed = inputData.getBoolean(INPUT_URGENT_NEED, false) + + if (notificationId < 0 || message.isBlank()) { + return Result.failure() + } + + sendLCDPetWorkerNotification( + context = applicationContext, + notificationId = notificationId, + title = title, + message = message, + urgentNeed = urgentNeed + ) + + return Result.success() + } +} + +fun scheduleLCDPetNotificationWork( + context: Context, + uniqueWorkName: String, + delayMs: Long, + notificationId: Int, + message: String, + title: String = "LCDPet", + urgentNeed: Boolean = false +) { + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(delayMs.coerceAtLeast(0L), TimeUnit.MILLISECONDS) + .setInputData( + workDataOf( + INPUT_WORK_TYPE to WORK_TYPE_NOTIFICATION, + INPUT_NOTIFICATION_ID to notificationId, + INPUT_NOTIFICATION_TITLE to title, + INPUT_NOTIFICATION_MESSAGE to message, + INPUT_URGENT_NEED to urgentNeed + ) + ) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + uniqueWorkName, + ExistingWorkPolicy.REPLACE, + request + ) +} + +fun scheduleLCDPetWantWork( + context: Context, + uniqueWorkName: String, + delayMs: Long +) { + val request = OneTimeWorkRequestBuilder() + .setInitialDelay( + delayMs.coerceAtLeast(0L), + TimeUnit.MILLISECONDS + ) + .setInputData( + workDataOf( + INPUT_WORK_TYPE to WORK_TYPE_WANT + ) + ) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + uniqueWorkName, + ExistingWorkPolicy.REPLACE, + request + ) +} + +fun cancelLCDPetNotificationWork( + context: Context, + uniqueWorkName: String +) { + WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName) +} + +fun scheduleLCDPetBackgroundTest(context: Context) { + scheduleLCDPetNotificationWork( + context = context, + uniqueWorkName = LCDPET_BACKGROUND_TEST_WORK, + delayMs = 30_000L, + notificationId = 1099, + message = "Background worker test: beep boop." + ) +} + +private fun wantNotificationText( + createdWant: LCDPetCreatedWant +): String { + val notificationPetName = + createdWant.petName.ifBlank { "Your pet" } + + return when (createdWant.wantName) { + "Snack" -> "$notificationPetName wants a snack." + "Play" -> "$notificationPetName wants to play." + "Pet" -> "$notificationPetName wants pets." + "Talk" -> "$notificationPetName is looking for you." + "Outside" -> "$notificationPetName wants to go outside." + "Bath" -> "$notificationPetName wants a bath." + else -> "$notificationPetName wants your attention." + } +} + +private fun sendLCDPetWorkerNotification( + context: Context, + notificationId: Int, + title: String, + message: String, + urgentNeed: Boolean = false +) { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (urgentNeed) { + val urgentSoundUri = + Uri.parse( + "android.resource://${context.packageName}/${R.raw.need_urgent}" + ) + + val urgentAudioAttributes = + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + + val urgentChannel = NotificationChannel( + LCDPET_WORKER_URGENT_CHANNEL_ID, + "LCDPet Urgent Needs", + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = "Urgent Hunger, Potty, Bath, and Energy alerts." + setSound(urgentSoundUri, urgentAudioAttributes) + enableVibration(true) + setVibrationPattern(longArrayOf(0L, 250L)) + } + + manager.createNotificationChannel(urgentChannel) + } else { + val channel = NotificationChannel( + LCDPET_WORKER_CHANNEL_ID, + "LCDPet Alerts", + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = "Pet care reminders and LCDPet status alerts." + } + + manager.createNotificationChannel(channel) + } + } + + val launchIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val pendingIntent = PendingIntent.getActivity( + context, + notificationId, + launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val channelId = + if (urgentNeed) { + LCDPET_WORKER_URGENT_CHANNEL_ID + } else { + LCDPET_WORKER_CHANNEL_ID + } + + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + + if (urgentNeed && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val urgentSoundUri = + Uri.parse( + "android.resource://${context.packageName}/${R.raw.need_urgent}" + ) + builder.setSound(urgentSoundUri) + builder.setVibrate(longArrayOf(0L, 250L)) + } + + NotificationManagerCompat + .from(context) + .notify(notificationId, builder.build()) +} diff --git a/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt b/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt index c4bc646..11d3445 100644 --- a/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt +++ b/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt @@ -10,8 +10,10 @@ import android.app.NotificationChannel import android.Manifest import android.content.pm.PackageManager +import android.media.AudioAttributes import android.media.AudioFormat import android.media.AudioRecord +import android.media.MediaPlayer import android.media.MediaRecorder import android.os.Build import android.os.Bundle @@ -21,6 +23,7 @@ import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.border import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -37,17 +40,20 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.Box +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -68,10 +74,108 @@ import kotlin.math.abs import kotlinx.coroutines.launch import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat import androidx.compose.ui.platform.LocalLifecycleOwner import today.notfeelingit.lcdpet.ui.theme.LCDPetTheme private const val LCDPET_NOTIFICATION_CHANNEL_ID = "lcdpet_alerts" +private const val LCDPET_URGENT_NOTIFICATION_CHANNEL_ID = "lcdpet_urgent_alerts_v2" +private const val PET_SOUND_VOLUME = 0.85f + +private fun postLCDPetNotification( + context: Context, + appIsVisible: Boolean, + notificationId: Int, + title: String, + message: String, + urgentNeed: Boolean = false, + allowInForeground: Boolean = false +) { + if (!allowInForeground && appIsVisible) { + if (urgentNeed) { + MediaPlayer.create( + context, + R.raw.need_urgent + )?.apply { + isLooping = false + + setOnCompletionListener { completedPlayer -> + completedPlayer.release() + } + + setOnErrorListener { failedPlayer, _, _ -> + failedPlayer.release() + true + } + + start() + } + } + + return + } + + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + + val launchIntent = Intent( + context, + MainActivity::class.java + ).apply { + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val pendingIntent = PendingIntent.getActivity( + context, + notificationId, + launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT or + PendingIntent.FLAG_IMMUTABLE + ) + + val channelId = + if (urgentNeed) { + LCDPET_URGENT_NOTIFICATION_CHANNEL_ID + } else { + LCDPET_NOTIFICATION_CHANNEL_ID + } + + val builder = + NotificationCompat.Builder(context, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + + if ( + urgentNeed && + Build.VERSION.SDK_INT < Build.VERSION_CODES.O + ) { + val urgentSoundUri = + Uri.parse( + "android.resource://${context.packageName}/" + + "${R.raw.need_urgent}" + ) + + builder.setSound(urgentSoundUri) + builder.setVibrate(longArrayOf(0L, 250L)) + } + + NotificationManagerCompat + .from(context) + .notify(notificationId, builder.build()) +} private data class CareJournalEntry( val category: String, @@ -105,7 +209,7 @@ private val lcdPetTemplates = listOf( topLine = " /^^^\\ \n", facePrefix = "( ", faceSuffix = " )\n", - feetLine = " /|_|\\ " + feetLine = " /| |\\ " ), PetTemplate( id = "bean", @@ -157,8 +261,41 @@ class MainActivity : ComponentActivity() { createLCDPetNotificationChannel() setContent { - LCDPetTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + val appearance by applicationContext.lcdPetAppearanceFlow.collectAsState( + initial = "System" + ) + val systemDarkTheme = isSystemInDarkTheme() + val useDarkTheme = + when (appearance) { + "Light" -> false + "Dark" -> true + else -> systemDarkTheme + } + + LCDPetTheme(darkTheme = useDarkTheme) { + val systemBarColor = MaterialTheme.colorScheme.background + + SideEffect { + window.statusBarColor = systemBarColor.toArgb() + window.navigationBarColor = systemBarColor.toArgb() + + WindowCompat.getInsetsController( + window, + window.decorView + ).apply { + isAppearanceLightStatusBars = !useDarkTheme + isAppearanceLightNavigationBars = !useDarkTheme + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background + ) { innerPadding -> Greeting( name = "LCDPet", modifier = Modifier.padding(innerPadding) @@ -178,8 +315,33 @@ class MainActivity : ComponentActivity() { description = "Pet care reminders and LCDPet status alerts." } - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val urgentSoundUri = + Uri.parse( + "android.resource://$packageName/${R.raw.need_urgent}" + ) + + val urgentAudioAttributes = + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + + val urgentChannel = NotificationChannel( + LCDPET_URGENT_NOTIFICATION_CHANNEL_ID, + "LCDPet Urgent Needs", + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = "Urgent Hunger, Potty, Bath, and Energy alerts." + setSound(urgentSoundUri, urgentAudioAttributes) + enableVibration(true) + setVibrationPattern(longArrayOf(0L, 250L)) + } + + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + notificationManager.createNotificationChannel(urgentChannel) } } } @@ -256,7 +418,14 @@ fun LCDMenuButton( Text( text = text, fontWeight = FontWeight.Bold, - color = if (enabled) Color.White else lcdDark, + color = + if (enabled && lcdDark == Color.White) { + Color.Black + } else if (enabled) { + Color.White + } else { + lcdDark + }, textAlign = TextAlign.Center ) } @@ -298,7 +467,7 @@ fun LCDHoldSubButton( lcdDark: Color, lcdFaded: Color, onPress: () -> Unit, - onRelease: () -> Unit, + onRelease: (releasedNormally: Boolean) -> Unit, modifier: Modifier = Modifier, fillWidthFraction: Float = 0.86f ) { @@ -311,10 +480,12 @@ fun LCDHoldSubButton( onPress = { if (enabled) { onPress() + var releasedNormally = false + try { - tryAwaitRelease() + releasedNormally = tryAwaitRelease() } finally { - onRelease() + onRelease(releasedNormally) } } } @@ -352,14 +523,36 @@ fun LCDSubMenuFrame( @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { val lcdBackground = Color(0xFFC4C7B0) - val lcdDark = Color(0xFF343434) - + val lcdFrameDark = Color(0xFF343434) val sickGreen = Color(0xFF2F7D32) - val lcdFaded = lcdDark.copy(alpha = 0.30f) val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val saveScope = rememberCoroutineScope() + val appearance by context.lcdPetAppearanceFlow.collectAsState( + initial = "System" + ) + val petHomeColor by context.lcdPetHomeColorFlow.collectAsState( + initial = "LCD" + ) + + val petHomeBackground = when (petHomeColor) { + "R" -> Color(0xFFD98C8C) + "G" -> Color(0xFF8FBF8F) + "B" -> Color(0xFF8FAFD6) + "Y" -> Color(0xFFD8C66A) + "P" -> Color(0xFF9B7BC1) + "K" -> Color.Black + else -> lcdBackground + } + + val lcdDark = + if (petHomeColor == "K") Color.White else lcdFrameDark + val lcdFaded = lcdDark.copy(alpha = 0.30f) + + val petFrameForeground = lcdFrameDark + val petFrameFaded = petFrameForeground.copy(alpha = 0.30f) + val notificationPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), onResult = {} @@ -448,8 +641,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { var roughWakeDarkFlash by remember { mutableStateOf(false) } var roughWakeNervousHold by remember { mutableStateOf(false) } var restoredAutoSleep by remember { mutableStateOf(false) } + var restoredManualNap by remember { mutableStateOf(false) } var autoSleepWakeAllowed by remember { mutableStateOf(false) } var autoSleepStartedAtMs by remember { mutableStateOf(0L) } + var outsideEnergyNextTickAtMs by remember { mutableStateOf(0L) } var isTradingInPet by remember { mutableStateOf(false) } var isFindingNewPet by remember { mutableStateOf(false) } var tradeInProgress by remember { mutableIntStateOf(0) } @@ -457,9 +652,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { var tradeInRemainingMs by remember { mutableStateOf(0L) } var tradeInConfirm by remember { mutableStateOf(false) } var isBathing by remember { mutableStateOf(false) } + var isFeeding by remember { mutableStateOf(false) } + var feedingStartHunger by remember { mutableIntStateOf(0) } var isTalking by remember { mutableStateOf(false) } var microphoneRecorder by remember { mutableStateOf(null) } var microphoneLevel by remember { mutableIntStateOf(0) } + var talkHeardSound by remember { mutableStateOf(false) } var bathBondAtStart by remember { mutableIntStateOf(0) } var bathWantMatched by remember { mutableStateOf(false) } var careMenuOpen by remember { mutableStateOf(false) } @@ -477,6 +675,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { var currentWant by remember { mutableStateOf("") } var currentWantStartedAtMs by remember { mutableStateOf(0L) } + var nextWantAtMs by remember { mutableStateOf(0L) } + var suppressBackgroundWantOnNextStop by remember { + mutableStateOf(false) + } var hungerNeedNotificationOpen by remember { mutableStateOf(false) } var pottyNeedNotificationOpen by remember { mutableStateOf(false) } var dirtinessNeedNotificationOpen by remember { mutableStateOf(false) } @@ -491,6 +693,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { var happyOutsideTrip by remember { mutableStateOf(false) } var petAgeDays by remember { mutableIntStateOf(0) } + var nextAgeAtMs by remember { mutableStateOf(0L) } var careScore by remember { mutableIntStateOf(0) } var neglectScore by remember { mutableIntStateOf(0) } @@ -510,6 +713,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { val pottyTickMs = if (testingMode) 30_000L else 3L * hourMs val dirtyHealthTickMs = if (testingMode) 60_000L else 12L * hourMs val activityMoodMs = if (testingMode) 2_000L else 2_000L + val feedStepMs = if (testingMode) 450L else 10_000L val propAnimationTickMs = if (testingMode) 180L else 180L val petDayMs = if (testingMode) 60_000L else 24L * hourMs val weatherShiftOptionsMs = if (testingMode) { @@ -544,6 +748,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { val tradeInProgressBlocks = 10 var bondReady by remember { mutableStateOf(true) } + var nextWeatherAtMs by remember { mutableStateOf(0L) } + var bondReadyAtMs by remember { mutableStateOf(0L) } + var careCycleNextTickAtMs by remember { mutableStateOf(0L) } + var rainHappinessNextTickAtMs by remember { mutableStateOf(0L) } + var pottyNextTickAtMs by remember { mutableStateOf(0L) } + var dirtyHealthNextTickAtMs by remember { mutableStateOf(0L) } var saveLoaded by remember { mutableStateOf(false) } fun encodeCareJournalEntries(entries: List): String { @@ -558,38 +768,25 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } - fun sendLCDPetNotification(notificationId: Int, title: String, message: String) { - if ( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - ContextCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - return - } - - val launchIntent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - } - - val pendingIntent = PendingIntent.getActivity( - context, - notificationId, - launchIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + fun sendLCDPetNotification( + notificationId: Int, + title: String, + message: String, + urgentNeed: Boolean = false, + allowInForeground: Boolean = false + ) { + postLCDPetNotification( + context = context, + appIsVisible = + lifecycleOwner.lifecycle.currentState.isAtLeast( + Lifecycle.State.STARTED + ), + notificationId = notificationId, + title = title, + message = message, + urgentNeed = urgentNeed, + allowInForeground = allowInForeground ) - - val notification = NotificationCompat.Builder(context, LCDPET_NOTIFICATION_CHANNEL_ID) - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentTitle(title) - .setContentText(message) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .build() - - NotificationManagerCompat.from(context).notify(notificationId, notification) } fun wantNotificationText(wantName: String): String { @@ -618,7 +815,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { sendLCDPetNotification( notificationId = notificationId, title = "LCDPet", - message = message + message = message, + urgentNeed = true ) } @@ -626,7 +824,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { sendLCDPetNotification( notificationId = 1001, title = "LCDPet", - message = "Test alert: your pet says beep boop." + message = "Test alert: your pet says beep boop.", + allowInForeground = true ) } @@ -674,6 +873,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { bond = save.bond petAgeDays = save.petAgeDays + nextAgeAtMs = save.nextAgeAtMs careScore = save.careScore neglectScore = save.neglectScore hunger = save.hunger @@ -683,6 +883,18 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness = save.dirtiness potty = save.potty boredom = save.boredom + starvingCount = save.starvingCount.coerceAtLeast(0) + recoveryCount = save.recoveryCount.coerceAtLeast(0) + sickCount = save.sickCount.coerceAtLeast(0) + playEnergyCount = save.playEnergyCount.coerceAtLeast(0) + sameActionStreak = save.sameActionStreak.coerceAtLeast(0) + boredomTriggerAt = + save.boredomTriggerAt.takeIf { it > 0 } + ?: (3..13).random() + boredomPenaltyCount = + save.boredomPenaltyCount.coerceAtLeast(0) + ultraTrollTriggered = save.ultraTrollTriggered + lastActionName = save.lastActionName location = save.location weather = save.weather status = save.status @@ -694,59 +906,238 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = save.tradeInProgress.coerceIn(0, tradeInProgressBlocks) tradeInStartedAtMs = save.tradeInStartedAtMs currentWant = save.currentWant + currentWantStartedAtMs = save.currentWantStartedAtMs + nextWantAtMs = save.nextWantAtMs petName = save.petName pendingPetName = save.petName petTemplateId = save.petTemplateId.ifBlank { "classic" } pendingPetTemplateId = petTemplateId isSleeping = save.isSleeping sleepMode = save.sleepMode + restoredManualNap = + save.isSleeping && save.sleepMode == "Nap" roughWakeAlert = save.roughWakeAlert autoSleepStartedAtMs = save.autoSleepStartedAtMs + outsideEnergyNextTickAtMs = save.outsideEnergyNextTickAtMs + nextWeatherAtMs = save.nextWeatherAtMs.coerceAtLeast(0L) + + val restoredBondReadyAtMs = + save.bondReadyAtMs.coerceAtLeast(0L) + + if (restoredBondReadyAtMs > System.currentTimeMillis()) { + bondReady = false + bondReadyAtMs = restoredBondReadyAtMs + } else { + bondReady = true + bondReadyAtMs = 0L + } + + careCycleNextTickAtMs = + save.careCycleNextTickAtMs.coerceAtLeast(0L) + rainHappinessNextTickAtMs = + save.rainHappinessNextTickAtMs.coerceAtLeast(0L) + pottyNextTickAtMs = + save.pottyNextTickAtMs.coerceAtLeast(0L) + dirtyHealthNextTickAtMs = + save.dirtyHealthNextTickAtMs.coerceAtLeast(0L) careJournalEntries = decodeCareJournalEntries(save.careJournalRaw) autoSleepWakeAllowed = false val nowMs = System.currentTimeMillis() - val elapsedAwayMs = (nowMs - save.lastSavedAtMs).coerceAtLeast(0L) + val firstOutsideEnergyTickAtMs = + if (save.outsideEnergyNextTickAtMs > 0L) { + save.outsideEnergyNextTickAtMs + } else { + save.lastSavedAtMs + offlineOutsideEnergyDrainTickMs + } if ( !save.isSleeping && save.location == "Outside" && !save.isTradingInPet && - !save.isFindingNewPet && - elapsedAwayMs >= offlineOutsideEnergyDrainTickMs + !save.isFindingNewPet ) { - val energyLostAway = (elapsedAwayMs / offlineOutsideEnergyDrainTickMs).toInt() + when { + save.energy <= 0 -> { + energy = 0 + isSleeping = true + sleepMode = "Auto" + autoSleepStartedAtMs = save.lastSavedAtMs + outsideEnergyNextTickAtMs = 0L + restoredAutoSleep = true + } - if (energyLostAway >= save.energy) { - val timeUntilSleepMs = save.energy.toLong() * offlineOutsideEnergyDrainTickMs - energy = 0 - isSleeping = true - sleepMode = "Auto" - autoSleepStartedAtMs = save.lastSavedAtMs + timeUntilSleepMs - restoredAutoSleep = true - } else { - energy = (save.energy - energyLostAway).coerceAtLeast(0) - restoredAutoSleep = false + nowMs >= firstOutsideEnergyTickAtMs -> { + val dueTicks = + 1 + ((nowMs - firstOutsideEnergyTickAtMs) / + offlineOutsideEnergyDrainTickMs).toInt() + + if (dueTicks >= save.energy) { + energy = 0 + isSleeping = true + sleepMode = "Auto" + autoSleepStartedAtMs = + firstOutsideEnergyTickAtMs + + ((save.energy - 1L) * + offlineOutsideEnergyDrainTickMs) + outsideEnergyNextTickAtMs = 0L + restoredAutoSleep = true + } else { + energy = (save.energy - dueTicks).coerceAtLeast(0) + outsideEnergyNextTickAtMs = 0L + restoredAutoSleep = false + } + } + + else -> { + outsideEnergyNextTickAtMs = 0L + restoredAutoSleep = false + } } } else { - restoredAutoSleep = save.isSleeping && save.sleepMode == "Auto" + outsideEnergyNextTickAtMs = 0L + restoredAutoSleep = + save.isSleeping && save.sleepMode == "Auto" + } + + val hasActiveSavedPet = + save.petName.isNotBlank() && + save.petTemplateId.isNotBlank() && + !save.isTradingInPet && + !save.isFindingNewPet + + if (hasActiveSavedPet) { + val savedNextAgeAtMs = save.nextAgeAtMs + + when { + savedNextAgeAtMs <= 0L -> { + nextAgeAtMs = nowMs + petDayMs + } + + nowMs >= savedNextAgeAtMs -> { + val dueAgeDays = + 1L + ((nowMs - savedNextAgeAtMs) / petDayMs) + + petAgeDays = + (save.petAgeDays.toLong() + dueAgeDays) + .coerceAtMost(Int.MAX_VALUE.toLong()) + .toInt() + + nextAgeAtMs = + savedNextAgeAtMs + (dueAgeDays * petDayMs) + } + + else -> { + nextAgeAtMs = savedNextAgeAtMs + } + } + } else { + nextAgeAtMs = 0L + } + + if (nextAgeAtMs != save.nextAgeAtMs) { + saveScope.launch { + context.setLCDPetNextAgeAtMs(nextAgeAtMs) + } } tradeInConfirm = false + + // Do not repost an already-active Need warning when the activity + // is recreated after its notification is tapped or cold-opened + // with an overdue saved state. + hungerNeedNotificationOpen = + hunger >= 10 && starvingCount >= 2 + + pottyNeedNotificationOpen = + save.location == "Inside" && save.potty >= 9 + + dirtinessNeedNotificationOpen = + save.dirtiness >= 9 && + save.status !in listOf( + "Sick", + "Weak", + "Recovering", + "RecoverWeak" + ) + + energyNeedNotificationOpen = + energy <= 1 && !isSleeping + saveLoaded = true } } - val hungerBar = "▓".repeat(hunger) + "▒".repeat(10 - hunger) + LaunchedEffect( + saveLoaded, + petName, + petTemplateId, + isTradingInPet, + isFindingNewPet, + nextAgeAtMs + ) { + val hasActivePet = + saveLoaded && + petName.isNotBlank() && + petTemplateId.isNotBlank() && + !isTradingInPet && + !isFindingNewPet + + if (!hasActivePet) { + return@LaunchedEffect + } + + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (nextAgeAtMs > 0L) { + nextAgeAtMs + } else { + nowMs + petDayMs + } + + if (nextAgeAtMs != dueAtMs) { + nextAgeAtMs = dueAtMs + context.setLCDPetNextAgeAtMs(dueAtMs) + return@LaunchedEffect + } + + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + val tickNowMs = System.currentTimeMillis() + val dueAgeDays = + 1L + ((tickNowMs - dueAtMs).coerceAtLeast(0L) / petDayMs) + + petAgeDays = + (petAgeDays.toLong() + dueAgeDays) + .coerceAtMost(Int.MAX_VALUE.toLong()) + .toInt() + + nextAgeAtMs = dueAtMs + (dueAgeDays * petDayMs) + context.setLCDPetNextAgeAtMs(nextAgeAtMs) + } + + fun lcdMeterBar(value: Int) = buildAnnotatedString { + val activeBlocks = value.coerceIn(0, 10) + + withStyle(SpanStyle(color = lcdDark)) { + append("▓".repeat(activeBlocks)) + } + + withStyle(SpanStyle(color = lcdFaded)) { + append("▓".repeat(10 - activeBlocks)) + } + } + + val hungerBar = lcdMeterBar(hunger) val healthBar = "\uD83D\uDDA4".repeat(health) + "\uD83E\uDE76".repeat(10 - health) - val happinessBar = "▓".repeat(happiness) + "▒".repeat(10 - happiness) - val energyBar = "▓".repeat(energy) + "▒".repeat(10 - energy) + val happinessBar = lcdMeterBar(happiness) + val energyBar = lcdMeterBar(energy) val bondBlocks = bond / 10 - val bondBar = "▓".repeat(bondBlocks) + "▒".repeat(10 - bondBlocks) + val bondBar = lcdMeterBar(bondBlocks) val bondPercent = bond - val dirtinessBar = "▓".repeat(dirtiness) + "▒".repeat(10 - dirtiness) - val pottyBar = "▓".repeat(potty) + "▒".repeat(10 - potty) - val boredomBar = "▓".repeat(boredom) + "▒".repeat(10 - boredom) + val dirtinessBar = lcdMeterBar(dirtiness) + val pottyBar = lcdMeterBar(potty) + val boredomBar = lcdMeterBar(boredom) fun statAgeText(days: Int): String { fun amount(value: Int, single: String, plural: String): String { @@ -878,8 +1269,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } val recoveryNeeded = (3 + sickCount - if (happiness >= 8) 1 else 0).coerceAtLeast(1) - val isOutsideGoodWeather = location == "Outside" && weather != "Rain" + val isOutsideGoodWeather = + location == "Outside" && weather in listOf("Sunny", "Cloudy") val isOutsideRain = location == "Outside" && weather == "Rain" + val isOutsideSnow = location == "Outside" && weather == "Snow" val displayStatus = when { isFindingNewPet -> "Trading" @@ -895,6 +1288,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { // Rain is scary until full trust. isOutsideRain && bond < 100 -> "Nervous" + // Snow trips stay calm and brief because the player does not want to linger. + isOutsideSnow -> "Calm" + // Temporary moods, like briefly enjoying good weather, show before fallback mood. activityMood.isNotEmpty() -> activityMood @@ -935,7 +1331,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { append(selectedPetTemplate.feetLine) } - val petColor = if (status == "Sick") sickGreen else lcdDark + val petColor = + if (status == "Sick") sickGreen else petFrameForeground val alertText = when { // Locked event moments. @@ -976,6 +1373,11 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { // Immediate player-action warnings. location == "Outside" && weather == "Rain" && bond < 100 && !happyOutsideTrip -> "Bring inside!" + isOutsideSnow -> if (potty >= 3) { + "\"Maybe a quick potty, then back in?\"" + } else { + "\"It's cold, let's go back in\"" + } // Wants beat low-priority mood chatter. wantText.isNotEmpty() -> wantText @@ -1003,7 +1405,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { withStyle( SpanStyle( - color = if (isDark) lcdDark else lcdFaded, + color = + if (isDark) petFrameForeground else petFrameFaded, fontWeight = FontWeight.Bold ) ) { @@ -1018,7 +1421,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { withStyle( SpanStyle( - color = if (isActive) lcdDark else lcdFaded, + color = + if (isActive) petFrameForeground else petFrameFaded, fontWeight = if (isActive) { FontWeight.Bold } else { @@ -1040,8 +1444,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { fun nextWeather(currentWeather: String): String { return when (currentWeather) { "Sunny" -> "Cloudy" - "Cloudy" -> listOf("Sunny", "Rain").random() + "Cloudy" -> listOf("Sunny", "Rain", "Snow").random() "Rain" -> "Cloudy" + "Snow" -> "Cloudy" else -> "Sunny" } } @@ -1059,6 +1464,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { if (!force) { bondReady = false + bondReadyAtMs = + System.currentTimeMillis() + bondCooldownMs } } } @@ -1080,6 +1487,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { if (outsideWantMatched) { currentWant = "" + currentWantStartedAtMs = 0L happyOutsideTrip = true } @@ -1105,7 +1513,6 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } else { location = "Inside" - weather = "Sunny" happyOutsideTrip = false if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) { @@ -1114,11 +1521,32 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } + fun playPetSound(soundResource: Int) { + val player = MediaPlayer.create(context, soundResource) ?: return + + player.apply { + isLooping = false + setVolume(PET_SOUND_VOLUME, PET_SOUND_VOLUME) + + setOnCompletionListener { completedPlayer -> + completedPlayer.release() + } + + setOnErrorListener { failedPlayer, _, _ -> + failedPlayer.release() + true + } + + start() + } + } + fun damageHealth(): Int { val newHealth = (health - 1).coerceAtLeast(0) health = newHealth if (newHealth == 0) { + careCycleNextTickAtMs = 0L status = "Weak" } @@ -1224,13 +1652,17 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } fun feedPet() { + if (isFeeding) { + return + } + framePropAnimation = "" framePropStyle = 0 frameProp = "[___]" if (completeWant("Snack")) { - // Wanted snack is a tiny treat, not a full meal. - // It should not overfeed, fill potty, or create a new care problem. + playPetSound(R.raw.pet_eating) + // A requested snack is a tiny treat, not a full meal. happiness = (happiness + 1).coerceAtMost(10) energy = (energy + 1).coerceAtMost(10) starvingCount = 0 @@ -1242,53 +1674,39 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { return } - if (hunger <= 0) { - if (!overfeedNeedWindowOpen) { - logNeedJournal("Overfed", met = false) - overfeedNeedWindowOpen = true - } - - overfeedCount += 1 - - if (overfeedCount == 3) { - neglectScore = (neglectScore + 1).coerceAtMost(999) - weakCause = "Overfed" - - if (damageHealth() > 0) { - status = "Sick" - } - } - } else { - hunger -= 1 - logNeedJournal("Feed", met = true) - feedPottyCount += 1 - - if (feedPottyCount >= 3) { - potty = (potty + 1).coerceAtMost(10) - feedPottyCount = 0 - } - - starvingCount = 0 - hungerNeedWindowOpen = false - overfeedNeedWindowOpen = false - careScore = (careScore + 1).coerceAtMost(999) - buildBond() - goodFeedCount += 1 - - if (goodFeedCount >= 2) { - happiness = (happiness + 1).coerceAtMost(10) - goodFeedCount = 0 - } - - if (status == "Sick") { - status = "Recovering" - recoveryCount = 0 - } + if (hunger <= 2) { + return } + + // Full meals are exclusive care time: no active or pending Wants. + currentWant = "" + currentWantStartedAtMs = 0L + nextWantAtMs = 0L + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_background_want" + ) + androidx.core.app.NotificationManagerCompat + .from(context) + .cancel(2001) + + saveScope.launch { + context.clearLCDPetWants() + } + + feedingStartHunger = hunger + isFeeding = true + playPetSound(R.raw.pet_eating) + activityMood = "Eating" + framePropAnimation = "Feed" + framePropStyle = 0 + frameProp = "[###]" } fun playPet() { if (energy > 0) { + playPetSound(R.raw.pet_excited) val wantMatched = completeWant("Play") if (!wantMatched) { @@ -1338,6 +1756,33 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } + fun registerHeardTalk() { + if (!isTalking || talkHeardSound) { + return + } + + // TALK succeeds when the microphone actually detects sound. + // Gesture release, sliding away, or lifecycle interruption only + // controls recorder cleanup and must not decide whether the pet heard it. + talkHeardSound = true + + val wantMatched = completeWant("Talk") + + if (!wantMatched) { + recordAction("Talk") + } + + careScore = (careScore + 1).coerceAtMost(999) + happiness = (happiness + 1).coerceAtMost(10) + + if (wantMatched) { + // Wanted talk is requested attention, not repetitive pestering. + buildBond(multiplier = 5, force = true) + } else { + buildBond() + } + } + @Suppress("MissingPermission") fun beginTalk() { if ( @@ -1349,6 +1794,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { framePropAnimation.isEmpty() ) { isTalking = true + talkHeardSound = false activityMood = if (bond >= 100) "Happy" else "Calm" framePropAnimation = "" framePropStyle = 0 @@ -1375,6 +1821,11 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { if (recorder == null || recorder.state != AudioRecord.STATE_INITIALIZED) { recorder?.release() + isTalking = false + microphoneRecorder = null + microphoneLevel = 0 + talkHeardSound = false + frameProp = "" return } @@ -1415,6 +1866,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { 1 -> "¢==-)" else -> "¢==-" } + + if (level > 0) { + registerHeardTalk() + } } } } @@ -1425,7 +1880,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } fun endTalk() { - if (isTalking) { + if (isTalking || microphoneRecorder != null) { isTalking = false microphoneRecorder?.let { recorder -> @@ -1439,28 +1894,14 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { microphoneRecorder = null microphoneLevel = 0 + talkHeardSound = false frameProp = "" - - val wantMatched = completeWant("Talk") - - if (!wantMatched) { - recordAction("Talk") - } - - careScore = (careScore + 1).coerceAtMost(999) - happiness = (happiness + 1).coerceAtMost(10) - - if (wantMatched) { - // Wanted talk is requested attention, not repetitive pestering. - buildBond(multiplier = 5, force = true) - } else { - buildBond() - } } } fun walkPet() { if (energy > 0) { + playPetSound(R.raw.pet_excited) activityMood = "Excited" startFrameAnimation("Footprints") careScore = (careScore + 1).coerceAtMost(999) @@ -1473,7 +1914,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { fun napPet() { roughWakeAlert = false - autoSleepStartedAtMs = 0L + + // For a manual Nap, this field stores the fixed wake deadline. + // Choosing it here prevents reopening the app from rolling a new duration. + autoSleepStartedAtMs = + System.currentTimeMillis() + sleepOptionsMs.random() + sleepMode = "Nap" isSleeping = true activityMood = "Asleep" @@ -1486,6 +1932,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { choosePottyFace() startFrameAnimation("PottyClean") careScore = (careScore + 1).coerceAtMost(999) + pottyNextTickAtMs = 0L potty = 0 hunger = (hunger + 1).coerceAtMost(10) @@ -1518,6 +1965,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { fun resetPetAfterTradeIn() { location = "Inside" weather = newPetWeatherOptions.random() + nextWeatherAtMs = 0L status = "Nervous" hunger = 5 health = 10 @@ -1554,6 +2002,23 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { isSleeping = false sleepMode = "" autoSleepStartedAtMs = 0L + restoredAutoSleep = false + autoSleepWakeAllowed = false + outsideEnergyNextTickAtMs = 0L + careCycleNextTickAtMs = 0L + rainHappinessNextTickAtMs = 0L + pottyNextTickAtMs = 0L + dirtyHealthNextTickAtMs = 0L + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_outside_energy_warning" + ) + + saveScope.launch { + context.setLCDPetOutsideEnergyNextTickAtMs(0L) + } + roughWakeAlert = false roughWakeDarkFlash = false roughWakeNervousHold = false @@ -1576,9 +2041,14 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { pendingPetTemplateId = "classic" happyOutsideTrip = false petAgeDays = 0 + nextAgeAtMs = 0L + saveScope.launch { + context.setLCDPetNextAgeAtMs(0L) + } careScore = 0 neglectScore = 0 bondReady = true + bondReadyAtMs = 0L tradeInConfirm = false isTradingInPet = false isFindingNewPet = false @@ -1591,6 +2061,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { careMenuOpen = false socializeMenuOpen = false currentWant = "" + currentWantStartedAtMs = 0L frameProp = "" framePropAnimation = "" framePropStyle = 0 @@ -1619,6 +2090,15 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness = dirtiness, potty = potty, boredom = boredom, + starvingCount = starvingCount, + recoveryCount = recoveryCount, + sickCount = sickCount, + playEnergyCount = playEnergyCount, + sameActionStreak = sameActionStreak, + boredomTriggerAt = boredomTriggerAt, + boredomPenaltyCount = boredomPenaltyCount, + ultraTrollTriggered = ultraTrollTriggered, + lastActionName = lastActionName, location = location, weather = weather, status = status, @@ -1630,12 +2110,20 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = 0, tradeInStartedAtMs = tradeInStartedAtMs, currentWant = currentWant, + currentWantStartedAtMs = currentWantStartedAtMs, + nextWantAtMs = nextWantAtMs, petName = petName, petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, autoSleepStartedAtMs = autoSleepStartedAtMs, + nextWeatherAtMs = nextWeatherAtMs, + bondReadyAtMs = bondReadyAtMs, + careCycleNextTickAtMs = careCycleNextTickAtMs, + rainHappinessNextTickAtMs = rainHappinessNextTickAtMs, + pottyNextTickAtMs = pottyNextTickAtMs, + dirtyHealthNextTickAtMs = dirtyHealthNextTickAtMs, careJournalRaw = encodeCareJournalEntries(careJournalEntries) ) } @@ -1663,6 +2151,15 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness = dirtiness, potty = potty, boredom = boredom, + starvingCount = starvingCount, + recoveryCount = recoveryCount, + sickCount = sickCount, + playEnergyCount = playEnergyCount, + sameActionStreak = sameActionStreak, + boredomTriggerAt = boredomTriggerAt, + boredomPenaltyCount = boredomPenaltyCount, + ultraTrollTriggered = ultraTrollTriggered, + lastActionName = lastActionName, location = location, weather = weather, status = status, @@ -1674,12 +2171,20 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = 0, tradeInStartedAtMs = 0L, currentWant = currentWant, + currentWantStartedAtMs = currentWantStartedAtMs, + nextWantAtMs = nextWantAtMs, petName = petName, petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, autoSleepStartedAtMs = autoSleepStartedAtMs, + nextWeatherAtMs = nextWeatherAtMs, + bondReadyAtMs = bondReadyAtMs, + careCycleNextTickAtMs = careCycleNextTickAtMs, + rainHappinessNextTickAtMs = rainHappinessNextTickAtMs, + pottyNextTickAtMs = pottyNextTickAtMs, + dirtyHealthNextTickAtMs = dirtyHealthNextTickAtMs, careJournalRaw = encodeCareJournalEntries(careJournalEntries) ) } @@ -1693,6 +2198,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { location = "Inside" weather = "Sunny" + nextWeatherAtMs = 0L status = if (bond < 30) "Nervous" else "Calm" hunger = 5 health = 10 @@ -1732,6 +2238,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { roughWakeDarkFlash = false roughWakeNervousHold = false isBathing = false + isFeeding = false + feedingStartHunger = 0 isTalking = false bathBondAtStart = 0 bathWantMatched = false @@ -1739,8 +2247,46 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { socializeMenuOpen = false statsMenuOpen = false currentWant = "" + currentWantStartedAtMs = 0L + nextWantAtMs = 0L + outsideEnergyNextTickAtMs = 0L + careCycleNextTickAtMs = 0L + rainHappinessNextTickAtMs = 0L + pottyNextTickAtMs = 0L + dirtyHealthNextTickAtMs = 0L + suppressBackgroundWantOnNextStop = false happyOutsideTrip = false bondReady = true + bondReadyAtMs = 0L + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_background_want" + ) + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_outside_energy_warning" + ) + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_background_test" + ) + + androidx.core.app.NotificationManagerCompat + .from(context) + .apply { + cancel(1099) + cancel(2001) + cancel(3001) + cancel(3002) + cancel(3003) + cancel(3004) + } + + saveScope.launch { + context.clearLCDPetWants() + context.setLCDPetOutsideEnergyNextTickAtMs(0L) + } } fun wakeFromAutoSleep() { if (isSleeping && sleepMode == "Auto") { @@ -1753,7 +2299,6 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { if (sleptOutside) { location = "Inside" - weather = "Sunny" bond = (bond - 1).coerceAtLeast(0) } @@ -1789,24 +2334,233 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } - DisposableEffect(lifecycleOwner, isSleeping, sleepMode, autoSleepWakeAllowed, restoredAutoSleep) { + val currentNeedsPetName by rememberUpdatedState(needsPetName) + val currentSaveLoaded by rememberUpdatedState(saveLoaded) + val currentLocation by rememberUpdatedState(location) + val currentEnergy by rememberUpdatedState(energy) + val currentPetName by rememberUpdatedState(petName) + val currentWantForLifecycle by rememberUpdatedState(currentWant) + val currentStatusForLifecycle by rememberUpdatedState(status) + val currentIsSleeping by rememberUpdatedState(isSleeping) + val currentSleepMode by rememberUpdatedState(sleepMode) + val currentIsBathing by rememberUpdatedState(isBathing) + val currentIsFeeding by rememberUpdatedState(isFeeding) + val currentIsTradingInPet by rememberUpdatedState(isTradingInPet) + val currentIsFindingNewPet by rememberUpdatedState(isFindingNewPet) + val currentOutsideEnergyNextTickAtMs by rememberUpdatedState( + outsideEnergyNextTickAtMs + ) + val currentNextWantAtMs by rememberUpdatedState(nextWantAtMs) + val currentSuppressBackgroundWant by rememberUpdatedState( + suppressBackgroundWantOnNextStop + ) + val currentAutoSleepWakeAllowed by rememberUpdatedState( + autoSleepWakeAllowed + ) + val currentRestoredAutoSleep by rememberUpdatedState(restoredAutoSleep) + + DisposableEffect(lifecycleOwner) { + val outsideEnergyWorkName = "lcdpet_outside_energy_warning" + val backgroundWantWorkName = "lcdpet_background_want" + val observer = LifecycleEventObserver { _, event -> when (event) { - Lifecycle.Event.ON_PAUSE, - Lifecycle.Event.ON_STOP -> { - if (isSleeping && sleepMode == "Auto") { + Lifecycle.Event.ON_PAUSE -> { + endTalk() + + if (currentIsSleeping && currentSleepMode == "Auto") { autoSleepWakeAllowed = true } } + Lifecycle.Event.ON_STOP -> { + endTalk() + + val stopNowMs = System.currentTimeMillis() + if (currentIsSleeping && currentSleepMode == "Auto") { + autoSleepWakeAllowed = true + } + + val shouldTrackOutsideEnergy = + currentSaveLoaded && + currentLocation == "Outside" && + !currentIsSleeping && + !currentIsTradingInPet && + !currentIsFindingNewPet && + !currentNeedsPetName + + if (shouldTrackOutsideEnergy) { + val nowMs = stopNowMs + val nextTickAtMs = + if (currentOutsideEnergyNextTickAtMs > nowMs) { + currentOutsideEnergyNextTickAtMs + } else { + nowMs + offlineOutsideEnergyDrainTickMs + } + + outsideEnergyNextTickAtMs = nextTickAtMs + + saveScope.launch { + context.setLCDPetOutsideEnergyNextTickAtMs(nextTickAtMs) + } + + val warningDelayMs = + if (currentEnergy <= 1) { + 0L + } else { + (nextTickAtMs - nowMs).coerceAtLeast(0L) + + ((currentEnergy - 2L) * + offlineOutsideEnergyDrainTickMs) + } + + val notificationPetName = + currentPetName.ifBlank { "Your pet" } + + scheduleLCDPetNotificationWork( + context = context, + uniqueWorkName = outsideEnergyWorkName, + delayMs = warningDelayMs, + notificationId = 3004, + message = + "$notificationPetName is exhausted. Nap soon.", + urgentNeed = true + ) + } else { + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = outsideEnergyWorkName + ) + + outsideEnergyNextTickAtMs = 0L + + saveScope.launch { + context.setLCDPetOutsideEnergyNextTickAtMs(0L) + } + } + + val suppressWantForThisStop = + currentSuppressBackgroundWant + + val shouldScheduleBackgroundWant = + currentSaveLoaded && + !suppressWantForThisStop && + currentWantForLifecycle.isEmpty() && + currentStatusForLifecycle !in setOf("Sick", "Weak") && + !currentIsSleeping && + !currentIsBathing && + !currentIsFeeding && + !currentIsTradingInPet && + !currentIsFindingNewPet && + !currentNeedsPetName + + if (shouldScheduleBackgroundWant) { + val dueAtMs = + if (currentNextWantAtMs > 0L) { + currentNextWantAtMs + } else { + (stopNowMs + wantCooldownOptionsMs.random()).also { + nextWantAtMs = it + + saveScope.launch { + context.setLCDPetNextWantAtMs(it) + } + } + } + + scheduleLCDPetWantWork( + context = context, + uniqueWorkName = backgroundWantWorkName, + delayMs = (dueAtMs - stopNowMs).coerceAtLeast(0L) + ) + } else { + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = backgroundWantWorkName + ) + } + } + Lifecycle.Event.ON_RESUME -> { + val resumeNowMs = System.currentTimeMillis() + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = backgroundWantWorkName + ) + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = outsideEnergyWorkName + ) + + val shouldApplyOutsideEnergy = + currentSaveLoaded && + currentLocation == "Outside" && + !currentIsSleeping && + !currentIsTradingInPet && + !currentIsFindingNewPet && + !currentNeedsPetName && + currentOutsideEnergyNextTickAtMs > 0L + + if (shouldApplyOutsideEnergy) { + val nowMs = resumeNowMs + + if (nowMs >= currentOutsideEnergyNextTickAtMs) { + val dueTicks = + 1 + ((nowMs - currentOutsideEnergyNextTickAtMs) / + offlineOutsideEnergyDrainTickMs).toInt() + + val energyBeforeCatchUp = currentEnergy + val energyAfterCatchUp = + (energyBeforeCatchUp - dueTicks) + .coerceAtLeast(0) + + // Acknowledge the worker alert before changing Energy. + // This prevents the foreground watcher from observing + // Energy 1 before the warning-open flag becomes true. + if (energyAfterCatchUp <= 1) { + energyNeedNotificationOpen = true + } + + if (dueTicks >= energyBeforeCatchUp) { + energy = 0 + isSleeping = true + sleepMode = "Auto" + autoSleepStartedAtMs = + currentOutsideEnergyNextTickAtMs + + ((energyBeforeCatchUp - 1L) + .coerceAtLeast(0L) * + offlineOutsideEnergyDrainTickMs) + outsideEnergyNextTickAtMs = 0L + autoSleepWakeAllowed = true + restoredAutoSleep = true + } else { + energy = energyAfterCatchUp + } + } + } + + outsideEnergyNextTickAtMs = 0L + + saveScope.launch { + context.setLCDPetOutsideEnergyNextTickAtMs(0L) + } + if ( isSleeping && sleepMode == "Auto" && - (autoSleepWakeAllowed || restoredAutoSleep) + (currentAutoSleepWakeAllowed || currentRestoredAutoSleep) ) { restoredAutoSleep = true } + + if (currentSuppressBackgroundWant) { + nextWantAtMs = 0L + saveScope.launch { + context.setLCDPetNextWantAtMs(0L) + } + suppressBackgroundWantOnNextStop = false + } } else -> Unit @@ -1816,6 +2570,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) + endTalk() } } @@ -1836,6 +2591,58 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } + LaunchedEffect(saveLoaded, restoredManualNap) { + if ( + !saveLoaded || + !restoredManualNap || + !isSleeping || + sleepMode != "Nap" + ) { + return@LaunchedEffect + } + + val wakeAtMs = autoSleepStartedAtMs + val napStillActive = + wakeAtMs <= 0L || System.currentTimeMillis() < wakeAtMs + + if (!napStillActive) { + // The original Nap finished while LCDPet was closed. + // Release the gate so the normal Nap completion effect can finish it. + restoredManualNap = false + return@LaunchedEffect + } + + // LCDPet was reopened before the manual Nap was finished. + roughWakeDarkFlash = true + activityMood = "Asleep" + frameProp = "" + framePropAnimation = "" + framePropStyle = 0 + + delay(if (testingMode) 1_500L else 1_200L) + + if ( + restoredManualNap && + isSleeping && + sleepMode == "Nap" + ) { + isSleeping = false + sleepMode = "" + autoSleepStartedAtMs = 0L + restoredManualNap = false + roughWakeDarkFlash = false + roughWakeNervousHold = false + + status = "Nervous" + activityMood = "RoughWake" + roughWakeAlert = true + frameProp = "!?!" + framePropAnimation = "" + framePropStyle = 0 + happiness = (happiness - 1).coerceAtLeast(0) + } + } + LaunchedEffect( bond, petAgeDays, @@ -1848,6 +2655,15 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness, potty, boredom, + starvingCount, + recoveryCount, + sickCount, + playEnergyCount, + sameActionStreak, + boredomTriggerAt, + boredomPenaltyCount, + ultraTrollTriggered, + lastActionName, location, weather, status, @@ -1858,11 +2674,19 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { isFindingNewPet, tradeInProgress, currentWant, + currentWantStartedAtMs, + nextWantAtMs, petName, isSleeping, sleepMode, roughWakeAlert, autoSleepStartedAtMs, + nextWeatherAtMs, + bondReadyAtMs, + careCycleNextTickAtMs, + rainHappinessNextTickAtMs, + pottyNextTickAtMs, + dirtyHealthNextTickAtMs, careJournalEntries, saveLoaded ) { @@ -1879,6 +2703,15 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness = dirtiness, potty = potty, boredom = boredom, + starvingCount = starvingCount, + recoveryCount = recoveryCount, + sickCount = sickCount, + playEnergyCount = playEnergyCount, + sameActionStreak = sameActionStreak, + boredomTriggerAt = boredomTriggerAt, + boredomPenaltyCount = boredomPenaltyCount, + ultraTrollTriggered = ultraTrollTriggered, + lastActionName = lastActionName, location = location, weather = weather, status = status, @@ -1890,12 +2723,20 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = tradeInProgress, tradeInStartedAtMs = tradeInStartedAtMs, currentWant = currentWant, + currentWantStartedAtMs = currentWantStartedAtMs, + nextWantAtMs = nextWantAtMs, petName = petName, petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, autoSleepStartedAtMs = autoSleepStartedAtMs, + nextWeatherAtMs = nextWeatherAtMs, + bondReadyAtMs = bondReadyAtMs, + careCycleNextTickAtMs = careCycleNextTickAtMs, + rainHappinessNextTickAtMs = rainHappinessNextTickAtMs, + pottyNextTickAtMs = pottyNextTickAtMs, + dirtyHealthNextTickAtMs = dirtyHealthNextTickAtMs, careJournalRaw = encodeCareJournalEntries(careJournalEntries) ) } @@ -1954,6 +2795,15 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness = dirtiness, potty = potty, boredom = boredom, + starvingCount = starvingCount, + recoveryCount = recoveryCount, + sickCount = sickCount, + playEnergyCount = playEnergyCount, + sameActionStreak = sameActionStreak, + boredomTriggerAt = boredomTriggerAt, + boredomPenaltyCount = boredomPenaltyCount, + ultraTrollTriggered = ultraTrollTriggered, + lastActionName = lastActionName, location = location, weather = weather, status = status, @@ -1965,12 +2815,20 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = tradeInProgressBlocks, tradeInStartedAtMs = 0L, currentWant = currentWant, + currentWantStartedAtMs = currentWantStartedAtMs, + nextWantAtMs = nextWantAtMs, petName = petName, petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, autoSleepStartedAtMs = autoSleepStartedAtMs, + nextWeatherAtMs = nextWeatherAtMs, + bondReadyAtMs = bondReadyAtMs, + careCycleNextTickAtMs = careCycleNextTickAtMs, + rainHappinessNextTickAtMs = rainHappinessNextTickAtMs, + pottyNextTickAtMs = pottyNextTickAtMs, + dirtyHealthNextTickAtMs = dirtyHealthNextTickAtMs, careJournalRaw = encodeCareJournalEntries(careJournalEntries) ) } @@ -2005,46 +2863,181 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } - LaunchedEffect(currentWant, location, isSleeping, isBathing, isTradingInPet, isFindingNewPet, needsPetName) { - if (isTradingInPet || isFindingNewPet || needsPetName) { + LaunchedEffect(status, saveLoaded) { + if (saveLoaded && status in setOf("Sick", "Weak")) { + currentWant = "" + currentWantStartedAtMs = 0L + nextWantAtMs = 0L + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_background_want" + ) + + context.clearLCDPetWants() + } + } + + LaunchedEffect( + currentWant, + location, + status, + isSleeping, + isBathing, + isFeeding, + suppressBackgroundWantOnNextStop, + isTradingInPet, + isFindingNewPet, + needsPetName, + saveLoaded + ) { + if ( + !saveLoaded || + suppressBackgroundWantOnNextStop || + status in setOf("Sick", "Weak") || + isFeeding || + isTradingInPet || + isFindingNewPet || + needsPetName + ) { return@LaunchedEffect } - if (currentWant.isEmpty() && !isSleeping && !isBathing) { - delay(wantCooldownOptionsMs.random()) - - if (currentWant.isEmpty() && !isSleeping && !isBathing) { - val possibleWants = if (location == "Inside") { - listOf("Snack", "Play", "Pet", "Talk", "Outside") + if (bond >= 100) listOf("Bath") else emptyList() + if ( + currentWant.isEmpty() && + !isSleeping && + !isBathing && + !isFeeding && + !suppressBackgroundWantOnNextStop + ) { + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (nextWantAtMs > 0L) { + nextWantAtMs } else { - listOf("Play", "Pet", "Talk") + (nowMs + wantCooldownOptionsMs.random()).also { + nextWantAtMs = it + saveScope.launch { + context.setLCDPetNextWantAtMs(it) + } + } } - currentWant = possibleWants.random() - currentWantStartedAtMs = System.currentTimeMillis() - sendWantNotification(currentWant) + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + if ( + currentWant.isEmpty() && + !isSleeping && + !isBathing && + !isFeeding && + !suppressBackgroundWantOnNextStop && + status !in setOf("Sick", "Weak") && + !isTradingInPet && + !isFindingNewPet && + !needsPetName + ) { + val claimNowMs = System.currentTimeMillis() + val createdWant = + context.createLCDPetWantIfDue(claimNowMs) + + if (createdWant != null) { + currentWant = createdWant.wantName + currentWantStartedAtMs = claimNowMs + nextWantAtMs = 0L + sendWantNotification(createdWant.wantName) + } } } else if (currentWant.isNotEmpty()) { - delay(wantDurationMs) + if (nextWantAtMs != 0L) { + nextWantAtMs = 0L + saveScope.launch { + context.setLCDPetNextWantAtMs(0L) + } + } - if (currentWant.isNotEmpty()) { - logWantJournal(currentWant, met = false) + val expiringWant = currentWant + val nowMs = System.currentTimeMillis() + val startedAtMs = + if (currentWantStartedAtMs > 0L) { + currentWantStartedAtMs + } else { + nowMs.also { currentWantStartedAtMs = it } + } + + val elapsedMs = (nowMs - startedAtMs).coerceAtLeast(0L) + val remainingMs = (wantDurationMs - elapsedMs).coerceAtLeast(0L) + + delay(remainingMs) + + if (currentWant == expiringWant) { + logWantJournal(expiringWant, met = false) currentWant = "" currentWantStartedAtMs = 0L } } } - LaunchedEffect(weather, isTradingInPet, isFindingNewPet, needsPetName) { - if (!isTradingInPet && !isFindingNewPet && !needsPetName) { - delay(weatherShiftOptionsMs.random()) + LaunchedEffect( + weather, + isTradingInPet, + isFindingNewPet, + needsPetName, + saveLoaded + ) { + if ( + !saveLoaded || + isTradingInPet || + isFindingNewPet || + needsPetName + ) { + return@LaunchedEffect + } + + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (nextWeatherAtMs > 0L) { + nextWeatherAtMs + } else { + nowMs + weatherShiftOptionsMs.random() + } + + if (nextWeatherAtMs != dueAtMs) { + nextWeatherAtMs = dueAtMs + } + + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + if ( + !isTradingInPet && + !isFindingNewPet && + !needsPetName + ) { + nextWeatherAtMs = 0L weather = nextWeather(weather) } } - LaunchedEffect(bondReady) { + LaunchedEffect(bondReady, saveLoaded) { + if (!saveLoaded || bondReady) { + return@LaunchedEffect + } + + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (bondReadyAtMs > 0L) { + bondReadyAtMs + } else { + nowMs + bondCooldownMs + } + + if (bondReadyAtMs != dueAtMs) { + bondReadyAtMs = dueAtMs + } + + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + if (!bondReady) { - delay(bondCooldownMs) + bondReadyAtMs = 0L bondReady = true } } @@ -2056,19 +3049,22 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { return@LaunchedEffect } - if ( - hunger >= 10 && - starvingCount >= 2 && - status !in listOf("Sick", "Weak", "Recovering", "RecoverWeak") && - !hungerNeedNotificationOpen - ) { + val hungerWarningThresholdOpen = + hunger >= 10 && starvingCount >= 2 + + if (!hungerWarningThresholdOpen) { + hungerNeedNotificationOpen = false + } else if (status in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) { + // Sickness already communicates the unresolved condition. Keep + // the warning consumed until Hunger actually drops below its + // threshold so status changes cannot re-arm the same alert. + hungerNeedNotificationOpen = true + } else if (!hungerNeedNotificationOpen) { hungerNeedNotificationOpen = true sendNeedNotification( notificationId = 3001, message = "$notificationPetName is really hungry. Feed soon." ) - } else if (hunger < 10 || status in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) { - hungerNeedNotificationOpen = false } } @@ -2130,15 +3126,73 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } - LaunchedEffect(hunger, starvingCount, status, recoveryCount, sickCount, happiness, isSleeping, isTradingInPet, isFindingNewPet, needsPetName) { - if (isSleeping || isTradingInPet || isFindingNewPet || needsPetName) { + LaunchedEffect( + hunger, + starvingCount, + status, + recoveryCount, + sickCount, + happiness, + isSleeping, + isTradingInPet, + isFindingNewPet, + needsPetName, + saveLoaded + ) { + if ( + !saveLoaded || + isSleeping || + isTradingInPet || + isFindingNewPet || + needsPetName + ) { + if (careCycleNextTickAtMs != 0L) { + careCycleNextTickAtMs = 0L + } return@LaunchedEffect } - if (status == "Sick") { - delay(sickTickMs) + val tickIntervalMs = + when (status) { + "Sick" -> sickTickMs + "Weak" -> weakRecoveryCheckMs + "RecoverWeak" -> recoverWeakMs + "Recovering" -> recoveringTickMs + else -> hungerTickMs + } + + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (careCycleNextTickAtMs > 0L) { + careCycleNextTickAtMs + } else { + nowMs + tickIntervalMs + } + + if (careCycleNextTickAtMs != dueAtMs) { + careCycleNextTickAtMs = dueAtMs + } + + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + if ( + isSleeping || + isTradingInPet || + isFindingNewPet || + needsPetName + ) { + careCycleNextTickAtMs = 0L + return@LaunchedEffect + } + + // The completed tick consumes this deadline. Any resulting state + // change starts a fresh interval for the new/current care branch. + careCycleNextTickAtMs = 0L + + if (status == "Sick") { sickCount += 1 damageHealth() + if (hunger < 10) { hunger += 1 } @@ -2148,15 +3202,11 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { recoveryCount = 0 } } else if (status == "Weak") { - delay(weakRecoveryCheckMs) - if (location == "Inside" && energy > 0 && hunger < 10) { status = "RecoverWeak" health = 1 } } else if (status == "RecoverWeak") { - delay(recoverWeakMs) - if (location == "Inside" && hunger < 10) { status = "Calm" recoveryCount = 0 @@ -2166,8 +3216,6 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { weakCause = "" } } else if (status == "Recovering") { - delay(recoveringTickMs) - recoveryCount += 1 if (recoveryCount >= recoveryNeeded && hunger < 10) { @@ -2179,8 +3227,6 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { weakCause = "" } } else { - delay(hungerTickMs) - if (hunger < 10) { hunger += 1 overfeedCount = 0 @@ -2202,27 +3248,117 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { val newHealth = (health - 1).coerceAtLeast(0) health = newHealth status = if (newHealth == 0) "Weak" else "Sick" + + if (status == "Sick") { + playPetSound(R.raw.pet_sick) + } } } } } - LaunchedEffect(location, weather, happiness, isTradingInPet, isFindingNewPet, needsPetName) { - if (isTradingInPet || isFindingNewPet || needsPetName) { + LaunchedEffect( + location, + weather, + happiness, + happyOutsideTrip, + isTradingInPet, + isFindingNewPet, + needsPetName, + saveLoaded + ) { + val rainPenaltyActive = + saveLoaded && + !isTradingInPet && + !isFindingNewPet && + !needsPetName && + location == "Outside" && + weather == "Rain" && + happiness > 0 && + !happyOutsideTrip + + if (!rainPenaltyActive) { + if (rainHappinessNextTickAtMs != 0L) { + rainHappinessNextTickAtMs = 0L + } return@LaunchedEffect } - if (location == "Outside" && weather == "Rain" && happiness > 0 && !happyOutsideTrip) { - delay(rainHappinessTickMs) + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (rainHappinessNextTickAtMs > 0L) { + rainHappinessNextTickAtMs + } else { + nowMs + rainHappinessTickMs + } + + if (rainHappinessNextTickAtMs != dueAtMs) { + rainHappinessNextTickAtMs = dueAtMs + } + + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + if ( + location == "Outside" && + weather == "Rain" && + happiness > 0 && + !happyOutsideTrip && + !isTradingInPet && + !isFindingNewPet && + !needsPetName + ) { + rainHappinessNextTickAtMs = 0L happiness = (happiness - 1).coerceAtLeast(0) } } - LaunchedEffect(location, potty, isSleeping, isTradingInPet, isFindingNewPet, needsPetName) { - if (isSleeping || isTradingInPet || isFindingNewPet || needsPetName) { + + LaunchedEffect( + location, + potty, + isSleeping, + isTradingInPet, + isFindingNewPet, + needsPetName, + saveLoaded + ) { + if ( + !saveLoaded || + isSleeping || + isTradingInPet || + isFindingNewPet || + needsPetName + ) { + if (pottyNextTickAtMs != 0L) { + pottyNextTickAtMs = 0L + } return@LaunchedEffect } - delay(pottyTickMs) + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (pottyNextTickAtMs > 0L) { + pottyNextTickAtMs + } else { + nowMs + pottyTickMs + } + + if (pottyNextTickAtMs != dueAtMs) { + pottyNextTickAtMs = dueAtMs + } + + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + if ( + isSleeping || + isTradingInPet || + isFindingNewPet || + needsPetName + ) { + pottyNextTickAtMs = 0L + return@LaunchedEffect + } + + pottyNextTickAtMs = 0L if (potty < 10) { potty = (potty + 1).coerceAtMost(10) @@ -2242,29 +3378,138 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } - LaunchedEffect(dirtiness, status, isSleeping, isTradingInPet, isFindingNewPet, needsPetName) { - if (isSleeping || isTradingInPet || isFindingNewPet || needsPetName) { + LaunchedEffect( + dirtiness, + status, + isSleeping, + isTradingInPet, + isFindingNewPet, + needsPetName, + saveLoaded + ) { + val dirtyHealthActive = + saveLoaded && + !isSleeping && + !isTradingInPet && + !isFindingNewPet && + !needsPetName && + dirtiness >= 10 && + status !in listOf( + "Sick", + "Weak", + "Recovering", + "RecoverWeak" + ) + + if (!dirtyHealthActive) { + if (dirtyHealthNextTickAtMs != 0L) { + dirtyHealthNextTickAtMs = 0L + } return@LaunchedEffect } - if (dirtiness >= 10 && status !in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) { - delay(dirtyHealthTickMs) + val nowMs = System.currentTimeMillis() + val dueAtMs = + if (dirtyHealthNextTickAtMs > 0L) { + dirtyHealthNextTickAtMs + } else { + nowMs + dirtyHealthTickMs + } - if (dirtiness >= 10 && status !in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) { - logNeedJournal("Bath", met = false) - neglectScore = (neglectScore + 1).coerceAtMost(999) - weakCause = "Dirty" - val newHealth = damageHealth() + if (dirtyHealthNextTickAtMs != dueAtMs) { + dirtyHealthNextTickAtMs = dueAtMs + } - if (newHealth > 0) { - status = "Sick" - } + delay((dueAtMs - nowMs).coerceAtLeast(0L)) + + if ( + dirtiness >= 10 && + status !in listOf( + "Sick", + "Weak", + "Recovering", + "RecoverWeak" + ) && + !isSleeping && + !isTradingInPet && + !isFindingNewPet && + !needsPetName + ) { + dirtyHealthNextTickAtMs = 0L + logNeedJournal("Bath", met = false) + neglectScore = (neglectScore + 1).coerceAtMost(999) + weakCause = "Dirty" + val newHealth = damageHealth() + + if (newHealth > 0) { + status = "Sick" + playPetSound(R.raw.pet_sick) } } } - LaunchedEffect(activityMood, isBathing) { - if (activityMood.isNotEmpty() && !isBathing) { + LaunchedEffect(isFeeding) { + if (isFeeding) { + val mealTarget = 2 + val mealBars = + (feedingStartHunger - mealTarget).coerceAtLeast(0) + + repeat(mealBars) { step -> + delay(feedStepMs) + + if (!isFeeding) { + return@LaunchedEffect + } + + hunger = (hunger - 1).coerceAtLeast(mealTarget) + + val barsRemaining = mealBars - step - 1 + frameProp = when { + barsRemaining >= 6 -> "[###]" + barsRemaining >= 3 -> "[## ]" + barsRemaining >= 1 -> "[# ]" + else -> "[___]" + } + } + + val energyGain = + ((mealBars + 2) / 3).coerceIn(1, 3) + val pottyGain = + (mealBars / 3).coerceIn(0, 2) + + energy = (energy + energyGain).coerceAtMost(10) + potty = (potty + pottyGain).coerceAtMost(10) + happiness = + (happiness + if (mealBars >= 3) 1 else 0) + .coerceAtMost(10) + + logNeedJournal("Feed", met = true) + starvingCount = 0 + overfeedCount = 0 + hungerNeedWindowOpen = false + overfeedNeedWindowOpen = false + careScore = (careScore + 1).coerceAtMost(999) + buildBond() + + if (status == "Sick") { + careCycleNextTickAtMs = 0L + status = "Recovering" + recoveryCount = 0 + } + + delay(if (testingMode) 450L else 1_000L) + + frameProp = "" + framePropAnimation = "" + framePropStyle = 0 + activityMood = "" + isFeeding = false + feedingStartHunger = 0 + } + } + + LaunchedEffect(activityMood, isBathing, isFeeding) { + if (activityMood.isNotEmpty() && !isBathing && !isFeeding) { delay(activityMoodMs) activityMood = "" } @@ -2497,10 +3742,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } if (dirtiness < dirtinessBeforeBath) { + dirtyHealthNextTickAtMs = 0L careScore = (careScore + 1).coerceAtMost(999) } if (weakCause == "Dirty" && status == "Sick" && dirtiness < 10) { + careCycleNextTickAtMs = 0L status = "Recovering" recoveryCount = 0 } @@ -2567,6 +3814,15 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { dirtiness = dirtiness, potty = potty, boredom = boredom, + starvingCount = starvingCount, + recoveryCount = recoveryCount, + sickCount = sickCount, + playEnergyCount = playEnergyCount, + sameActionStreak = sameActionStreak, + boredomTriggerAt = boredomTriggerAt, + boredomPenaltyCount = boredomPenaltyCount, + ultraTrollTriggered = ultraTrollTriggered, + lastActionName = lastActionName, location = location, weather = weather, status = status, @@ -2578,12 +3834,20 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = tradeInProgress, tradeInStartedAtMs = tradeInStartedAtMs, currentWant = currentWant, + currentWantStartedAtMs = currentWantStartedAtMs, + nextWantAtMs = nextWantAtMs, petName = petName, petTemplateId = petTemplateId, isSleeping = true, sleepMode = "Auto", roughWakeAlert = roughWakeAlert, autoSleepStartedAtMs = autoSleepStartedAtMs, + nextWeatherAtMs = nextWeatherAtMs, + bondReadyAtMs = bondReadyAtMs, + careCycleNextTickAtMs = careCycleNextTickAtMs, + rainHappinessNextTickAtMs = rainHappinessNextTickAtMs, + pottyNextTickAtMs = pottyNextTickAtMs, + dirtyHealthNextTickAtMs = dirtyHealthNextTickAtMs, careJournalRaw = encodeCareJournalEntries(careJournalEntries) ) } @@ -2591,16 +3855,38 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } - LaunchedEffect(isSleeping, sleepMode) { - if (!isSleeping || sleepMode != "Nap") { + LaunchedEffect( + isSleeping, + sleepMode, + saveLoaded, + restoredManualNap + ) { + if ( + !saveLoaded || + !isSleeping || + sleepMode != "Nap" || + restoredManualNap + ) { return@LaunchedEffect } isTalking = false - val sleepTime = sleepOptionsMs.random() + val nowMs = System.currentTimeMillis() + + val wakeAtMs = + if (autoSleepStartedAtMs > 0L) { + autoSleepStartedAtMs + } else { + // Compatibility for an older saved manual Nap with no deadline. + (nowMs + sleepOptionsMs.random()).also { + autoSleepStartedAtMs = it + } + } + + val remainingSleepMs = (wakeAtMs - nowMs).coerceAtLeast(0L) val sleepSteps = 5 - val stepDelay = sleepTime / sleepSteps + val stepDelay = remainingSleepMs / sleepSteps val startHealth = health val startHappiness = happiness @@ -2631,6 +3917,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { energy = 10 if (status in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) { + careCycleNextTickAtMs = 0L status = "Calm" weakCause = "" overfeedCount = 0 @@ -2643,6 +3930,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { isSleeping = false sleepMode = "" + autoSleepStartedAtMs = 0L activityMood = "" if (framePropAnimation == "Nap") { @@ -2652,19 +3940,38 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } } val screenScrollState = rememberScrollState() + val configuration = LocalConfiguration.current + val widthScale = + (configuration.screenWidthDp / 400f) + .coerceIn(0.80f, 1f) + val heightScale = + (configuration.screenHeightDp / 780f) + .coerceIn(0.74f, 1f) + val layoutScale = minOf(widthScale, heightScale) + + val screenPadding = (16f * layoutScale).dp + val petFrameMinHeight = (235f * layoutScale).dp + val petFrameOuterPadding = (10f * layoutScale).dp + val petFrameInnerPadding = (12f * layoutScale).dp + val meterTopPadding = (16f * layoutScale).dp + val meterSpacing = (6f * layoutScale).dp + val controlsTopSpacing = (12f * layoutScale).dp + val actionSpacing = (8f * layoutScale).dp CompositionLocalProvider(LocalContentColor provides lcdDark) { Box( modifier = modifier .fillMaxSize() - .background(lcdBackground) + .background(petHomeBackground) ) { + val howToTopPadding = (28f * layoutScale * layoutScale).dp + Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(screenScrollState) - .padding(16.dp) - ) { + modifier = Modifier + .fillMaxWidth() + .verticalScroll(screenScrollState) + .padding(screenPadding) + ) { if (!needsPetName) { Row( modifier = Modifier.fillMaxWidth(), @@ -2676,7 +3983,21 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { ) { Text( text = displayPetName, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, + modifier = Modifier.clickable( + enabled = + !isTradingInPet && + !isFindingNewPet + ) { + infoPanelType = "Renovation" + infoPanelOpen = true + statsMenuOpen = false + careJournalOpen = false + testWantMenuOpen = false + testBondMenuOpen = false + careMenuOpen = false + socializeMenuOpen = false + } ) Text( @@ -2705,16 +4026,18 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "$location | $weather", fontWeight = FontWeight.Bold, - modifier = Modifier.clickable { + modifier = Modifier.clickable( + enabled = downtimeMode == DowntimeMode.None + ) { toggleLocation() } ) Text( - text = "Credits / Licenses", + text = "Extras", fontWeight = FontWeight.Bold, modifier = Modifier.clickable { - infoPanelType = "Credits" + infoPanelType = "Extras" infoPanelOpen = true statsMenuOpen = false careJournalOpen = false @@ -3017,6 +4340,16 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } ) + LCDSubButton( + text = "TEST BACKGROUND 30S", + enabled = true, + lcdDark = lcdDark, + lcdFaded = lcdFaded, + onClick = { + scheduleLCDPetBackgroundTest(context) + } + ) + LCDSubButton( text = "TEST HUNGER WARNING", enabled = !isFindingNewPet && !isTradingInPet && !isSleeping, @@ -3053,12 +4386,40 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { ) LCDSubButton( - text = "TEST ENERGY WARNING", + text = "TEST BG ENERGY 30S", enabled = !isFindingNewPet && !isTradingInPet && !isSleeping, lcdDark = lcdDark, lcdFaded = lcdFaded, onClick = { - energy = 1 + // Isolate the real 30-second Energy worker test + // from both foreground and background Wants. + suppressBackgroundWantOnNextStop = true + currentWant = "" + currentWantStartedAtMs = 0L + nextWantAtMs = 0L + + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_background_want" + ) + cancelLCDPetNotificationWork( + context = context, + uniqueWorkName = "lcdpet_background_test" + ) + androidx.core.app.NotificationManagerCompat + .from(context) + .apply { + cancel(1099) + cancel(2001) + } + + saveScope.launch { + context.clearLCDPetWants() + } + + location = "Outside" + outsideEnergyNextTickAtMs = 0L + energy = 2 energyNeedNotificationOpen = false } ) @@ -3187,15 +4548,41 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { onClick = { forceTestWant("Bath") } ) + LCDSubButton( + text = "TEST SNOW", + enabled = !isSleeping && + !isBathing && + !isTalking && + framePropAnimation.isEmpty(), + lcdDark = lcdDark, + lcdFaded = lcdFaded, + onClick = { + weather = "Snow" + location = "Inside" + happyOutsideTrip = false + activityMood = "" + downtimeMode = DowntimeMode.None + careMenuOpen = false + socializeMenuOpen = false + testWantMenuOpen = false + statsMenuOpen = false + } + ) + LCDSubButton( text = "TEST TOUCH GRASS", - enabled = !isSleeping && !isBathing && framePropAnimation.isEmpty(), + enabled = !isSleeping && + !isBathing && + !isTalking && + framePropAnimation.isEmpty(), lcdDark = lcdDark, lcdFaded = lcdFaded, onClick = { downtimeMode = DowntimeMode.TouchGrass location = "Outside" happyOutsideTrip = false + careMenuOpen = false + socializeMenuOpen = false testWantMenuOpen = false statsMenuOpen = false } @@ -3203,7 +4590,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { LCDSubButton( text = "TEST WATCH TV", - enabled = !isSleeping && !isBathing && framePropAnimation.isEmpty(), + enabled = !isSleeping && + !isBathing && + !isTalking && + framePropAnimation.isEmpty(), lcdDark = lcdDark, lcdFaded = lcdFaded, onClick = { @@ -3211,6 +4601,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { selectedTvChannel = TvChannel.Weather location = "Inside" happyOutsideTrip = false + careMenuOpen = false + socializeMenuOpen = false testWantMenuOpen = false statsMenuOpen = false } @@ -3258,6 +4650,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } choosePottyFace() startFrameAnimation("PottyAccident") + pottyNextTickAtMs = 0L potty = 0 dirtiness = (dirtiness + 3).coerceAtMost(10) testWantMenuOpen = false @@ -3280,6 +4673,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { pottyStain = "" choosePottyFace() startFrameAnimation("PottyClean") + pottyNextTickAtMs = 0L potty = 0 testWantMenuOpen = false statsMenuOpen = false @@ -3293,6 +4687,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { lcdFaded = lcdFaded, onClick = { currentWant = "" + currentWantStartedAtMs = 0L testWantMenuOpen = false } ) @@ -3483,18 +4878,22 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { Box( modifier = Modifier .fillMaxWidth() - .heightIn(min = 235.dp) - .padding(top = 10.dp, bottom = 10.dp) - .border(2.dp, lcdDark), + .heightIn(min = petFrameMinHeight) + .padding(top = petFrameOuterPadding, bottom = petFrameOuterPadding) + .background(lcdBackground) + .border(2.dp, petFrameForeground), contentAlignment = Alignment.Center ) { Column( modifier = Modifier .fillMaxSize() - .padding(top = 12.dp, bottom = 12.dp), + .padding(top = petFrameInnerPadding, bottom = petFrameInnerPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { + val downtimeActive = + testingMode && downtimeMode != DowntimeMode.None + val downtimeTitle = when { testingMode && downtimeMode == DowntimeMode.TouchGrass -> "TOUCH GRASS" testingMode && downtimeMode == DowntimeMode.WatchTv -> "WATCH TV" @@ -3505,22 +4904,33 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = downtimeTitle, fontWeight = FontWeight.Bold, - color = lcdDark + color = petFrameForeground ) } else { - Text(text = healthBar) + Text( + text = healthBar, + color = petFrameForeground + ) } - Spacer(modifier = Modifier.height(10.dp)) + Spacer( + modifier = Modifier.height( + if (downtimeActive) { + (4f * layoutScale).dp + } else { + (10f * layoutScale).dp + } + ) + ) if (testingMode && downtimeMode == DowntimeMode.TouchGrass) { TouchGrassActivityScreen( - lcdDark = lcdDark, + lcdDark = petFrameForeground, onGrassTap = {} ) } else if (testingMode && downtimeMode == DowntimeMode.WatchTv) { WatchTvActivityScreen( - lcdDark = lcdDark, + lcdDark = petFrameForeground, selectedChannel = selectedTvChannel, weatherLabel = weather, onChannelSelected = { selectedTvChannel = it } @@ -3532,42 +4942,57 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { lineHeight = 24.sp, fontFamily = FontFamily.Monospace, textAlign = TextAlign.Center, - color = if (displayStatus == "Recovering") sickGreen else petColor + color = + if (petHomeColor == "K") { + petFrameForeground + } else if (displayStatus == "Recovering") { + sickGreen + } else { + petColor + } ) } - Spacer(modifier = Modifier.height(8.dp)) + if (!downtimeActive) { + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = framePropText, - modifier = Modifier.width(160.dp), - fontSize = 20.sp, - lineHeight = 20.sp, - fontFamily = FontFamily.Monospace, - textAlign = TextAlign.Center, - color = lcdDark, - softWrap = false, - maxLines = 1 - ) + Text( + text = framePropText, + modifier = Modifier.width(160.dp), + fontSize = 20.sp, + lineHeight = 20.sp, + fontFamily = FontFamily.Monospace, + textAlign = TextAlign.Center, + color = petFrameForeground, + softWrap = false, + maxLines = 1 + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = when { - alertText.isNotEmpty() -> alertText - wantText.isNotEmpty() -> wantText - else -> "Status: $displayStatus" - }, - fontWeight = if (alertText.isNotEmpty() || wantText.isNotEmpty()) FontWeight.Bold else FontWeight.Normal, - textAlign = TextAlign.Center - ) + Text( + text = when { + alertText.isNotEmpty() -> alertText + wantText.isNotEmpty() -> wantText + else -> "Status: $displayStatus" + }, + fontWeight = if ( + alertText.isNotEmpty() || wantText.isNotEmpty() + ) { + FontWeight.Bold + } else { + FontWeight.Normal + }, + color = petFrameForeground, + textAlign = TextAlign.Center + ) + } } - if ((isSleeping && location == "Inside") || roughWakeDarkFlash) { Box( modifier = Modifier - .fillMaxSize() + .matchParentSize() .background(Color.Gray.copy(alpha = 0.38f)) ) } @@ -3582,7 +5007,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "FINDING PET...", fontWeight = FontWeight.Bold, - color = lcdDark + color = petFrameForeground ) } } @@ -3595,7 +5020,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { modifier = Modifier .width(androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp.dp) .height(androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp.dp) - .background(lcdBackground) + .background(petHomeBackground) .border(2.dp, lcdDark) .padding(10.dp) ) { @@ -3607,10 +5032,12 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = if (infoPanelType == "HowTo") { - "HOW TO" - } else { - "CREDITS / LICENSES" + text = when (infoPanelType) { + "HowTo" -> "HOW TO" + "Extras" -> "EXTRAS" + "Appearance" -> "APPEARANCE" + "Renovation" -> "HOME RENOVATION" + else -> "CREDITS / LICENSES" }, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, @@ -3634,6 +5061,233 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } ) } + } else if (infoPanelType == "Extras") { + Text( + text = "Credits / Licenses", + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .clickable { + infoPanelType = "Credits" + } + .padding(vertical = 10.dp) + ) + + Text( + text = "Appearance: $appearance", + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .clickable { + infoPanelType = "Appearance" + } + .padding(vertical = 10.dp) + ) + } else if (infoPanelType == "Appearance") { + Text( + text = "Choose how LCDPet looks.", + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = "System follows the device setting. Light and Dark stay fixed until changed.", + fontSize = 11.sp, + lineHeight = 13.sp, + fontFamily = FontFamily.Monospace, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + listOf("System", "Light", "Dark").forEach { option -> + val selected = appearance == option + + Text( + text = + if (selected) { + "[X] ${option.uppercase()}" + } else { + "[ ] ${option.uppercase()}" + }, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .border( + width = if (selected) 2.dp else 1.dp, + color = lcdDark + ) + .clickable { + saveScope.launch { + context.setLCDPetAppearance(option) + } + } + .padding( + horizontal = 12.dp, + vertical = 10.dp + ) + ) + + Spacer(modifier = Modifier.height(6.dp)) + } + } else if (infoPanelType == "Renovation") { + Text( + text = "Choose a color for $displayPetName's home.", + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = "Tap a color to preview and save it.", + fontSize = 11.sp, + lineHeight = 13.sp, + fontFamily = FontFamily.Monospace, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .background(petHomeBackground) + .border(2.dp, lcdDark) + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = + Arrangement.spacedBy(8.dp) + ) { + Text( + text = "BACKGROUND PREVIEW", + color = lcdDark, + fontSize = 11.sp, + lineHeight = 13.sp, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold + ) + + Box( + modifier = Modifier + .fillMaxWidth(0.78f) + .background(lcdBackground) + .border( + 2.dp, + petFrameForeground + ) + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = + Alignment.CenterHorizontally + ) { + Text( + text = displayPetName, + color = petFrameForeground, + fontWeight = FontWeight.Bold + ) + + Text( + text = petArt, + color = petFrameForeground, + fontSize = 20.sp, + lineHeight = 20.sp, + fontFamily = + FontFamily.Monospace, + textAlign = TextAlign.Center + ) + } + } + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + val renovationOptions = listOf( + Triple("LCD", "LCD", lcdBackground), + Triple("R", "RED", Color(0xFFD98C8C)), + Triple("G", "GREEN", Color(0xFF8FBF8F)), + Triple("B", "BLUE", Color(0xFF8FAFD6)), + Triple("Y", "YELLOW", Color(0xFFD8C66A)), + Triple("P", "PURPLE", Color(0xFF9B7BC1)), + Triple("K", "BLACK", Color.Black) + ) + + renovationOptions.chunked(2).forEach { rowOptions -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = + Arrangement.spacedBy(6.dp) + ) { + rowOptions.forEach { option -> + val code = option.first + val optionBackground = option.third + val selected = petHomeColor == code + val optionForeground = + if (code == "K") { + Color.White + } else { + lcdDark + } + + Box( + modifier = Modifier + .weight(1f) + .background(optionBackground) + .border( + width = + if (selected) { + 3.dp + } else { + 1.dp + }, + color = optionForeground + ) + .clickable { + saveScope.launch { + context.setLCDPetHomeColor( + code + ) + } + } + .padding( + horizontal = 4.dp, + vertical = 10.dp + ), + contentAlignment = + Alignment.Center + ) { + Text( + text = + if (selected) { + "[X] $code" + } else { + "[ ] $code" + }, + color = optionForeground, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + } + + if (rowOptions.size == 1) { + Spacer(modifier = Modifier.weight(1f)) + } + } + + Spacer(modifier = Modifier.height(6.dp)) + } } else { creditsText.lines().forEach { creditLine -> if (creditLine.startsWith("Sound page: ")) { @@ -3710,7 +5364,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { text = "CLOSE", fontWeight = FontWeight.Bold, modifier = Modifier - .background(lcdBackground) + .background(petHomeBackground) .border(1.dp, lcdDark) .clickable { infoPanelOpen = false @@ -3722,7 +5376,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { text = "MOVE", fontWeight = FontWeight.Bold, modifier = Modifier - .background(lcdBackground) + .background(petHomeBackground) .border(1.dp, lcdDark) .clickable { infoPanelButtonCorner = when (infoPanelButtonCorner) { @@ -3739,7 +5393,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { text = "MOVE", fontWeight = FontWeight.Bold, modifier = Modifier - .background(lcdBackground) + .background(petHomeBackground) .border(1.dp, lcdDark) .clickable { infoPanelButtonCorner = when (infoPanelButtonCorner) { @@ -3756,7 +5410,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { text = "CLOSE", fontWeight = FontWeight.Bold, modifier = Modifier - .background(lcdBackground) + .background(petHomeBackground) .border(1.dp, lcdDark) .clickable { infoPanelOpen = false @@ -3771,88 +5425,137 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } Column( - modifier = Modifier.padding(top = 16.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) + modifier = Modifier.padding(top = meterTopPadding), + verticalArrangement = Arrangement.spacedBy(meterSpacing) ) { + val meterLabelWidth = 120.dp + val meterBarPadding = 8.dp + Row { Text( text = "Happiness", - modifier = Modifier.width(112.dp), + modifier = Modifier.width(meterLabelWidth), textAlign = TextAlign.End, maxLines = 1, softWrap = false ) - Text(text = " $happinessBar") + Text( + text = happinessBar, + modifier = Modifier.padding(start = meterBarPadding), + fontFamily = FontFamily.Monospace, + maxLines = 1, + softWrap = false + ) } Row { Text( text = "Energy", - modifier = Modifier.width(112.dp), + modifier = Modifier.width(meterLabelWidth), textAlign = TextAlign.End, maxLines = 1, softWrap = false ) - Text(text = " $energyBar") + Text( + text = energyBar, + modifier = Modifier.padding(start = meterBarPadding), + fontFamily = FontFamily.Monospace, + maxLines = 1, + softWrap = false + ) } Row { Text( text = "Hunger", - modifier = Modifier.width(112.dp), + modifier = Modifier.width(meterLabelWidth), textAlign = TextAlign.End, maxLines = 1, softWrap = false ) - Text(text = " $hungerBar") + Text( + text = hungerBar, + modifier = Modifier.padding(start = meterBarPadding), + fontFamily = FontFamily.Monospace, + maxLines = 1, + softWrap = false + ) } Row { Text( text = "Dirtiness", - modifier = Modifier.width(112.dp), + modifier = Modifier.width(meterLabelWidth), textAlign = TextAlign.End, maxLines = 1, softWrap = false ) - Text(text = " $dirtinessBar") + Text( + text = dirtinessBar, + modifier = Modifier.padding(start = meterBarPadding), + fontFamily = FontFamily.Monospace, + maxLines = 1, + softWrap = false + ) } Row { Text( text = "Potty", - modifier = Modifier.width(112.dp), + modifier = Modifier.width(meterLabelWidth), textAlign = TextAlign.End, maxLines = 1, softWrap = false ) - Text(text = " $pottyBar") + Text( + text = pottyBar, + modifier = Modifier.padding(start = meterBarPadding), + fontFamily = FontFamily.Monospace, + maxLines = 1, + softWrap = false + ) } Row { Text( text = "Boredom", - modifier = Modifier.width(112.dp), + modifier = Modifier.width(meterLabelWidth), textAlign = TextAlign.End, maxLines = 1, softWrap = false ) - Text(text = " $boredomBar") + Text( + text = boredomBar, + modifier = Modifier.padding(start = meterBarPadding), + fontFamily = FontFamily.Monospace, + maxLines = 1, + softWrap = false + ) } } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(controlsTopSpacing)) Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(actionSpacing) ) { - val actionInProgress = isTradingInPet || isFindingNewPet || isSleeping || isBathing || framePropAnimation.isNotEmpty() || + val actionInProgress = + (testingMode && downtimeMode != DowntimeMode.None) || + isTradingInPet || + isFindingNewPet || + isSleeping || + isBathing || + isFeeding || + framePropAnimation.isNotEmpty() || (frameProp.isNotEmpty() && !isTalking) val canAct = !actionInProgress val canStartAction = canAct && !isTalking val canBathe = canStartAction && location == "Inside" && (dirtiness > 0 || currentWant == "Bath") - val canFeed = canStartAction && location == "Inside" && + val canFeed = + canStartAction && + location == "Inside" && + (currentWant == "Snack" || hunger >= 3) && !(status in listOf( "Sick", "Weak", @@ -3868,7 +5571,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { val canNap = canStartAction && location == "Inside" && energy < 10 val canPotty = canStartAction && location == "Outside" && potty > 0 - val canWalk = canStartAction && location == "Outside" && energy > 0 && status !in listOf( + val canWalk = canStartAction && location == "Outside" && weather != "Snow" && energy > 0 && status !in listOf( "Sick", "Recovering", "Weak", @@ -3990,7 +5693,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { ) } }, - onRelease = { + onRelease = { _ -> endTalk() } ) @@ -4025,7 +5728,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding(top = 28.dp, bottom = 4.dp) + .padding(top = howToTopPadding, bottom = 4.dp) .clickable { infoPanelType = "HowTo" infoPanelOpen = true @@ -4039,6 +5742,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { ) } } + } } } diff --git a/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt b/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt index 29a6baf..03a2988 100644 --- a/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt +++ b/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt @@ -15,6 +15,7 @@ val Context.lcdPetDataStore by preferencesDataStore(name = "lcdpet_save") data class LCDPetRecordSave( val bond: Int = 0, val petAgeDays: Int = 0, + val nextAgeAtMs: Long = 0L, val careScore: Int = 0, val neglectScore: Int = 0, val hunger: Int = 5, @@ -24,6 +25,15 @@ data class LCDPetRecordSave( val dirtiness: Int = 0, val potty: Int = 0, val boredom: Int = 0, + val starvingCount: Int = 0, + val recoveryCount: Int = 0, + val sickCount: Int = 0, + val playEnergyCount: Int = 0, + val sameActionStreak: Int = 0, + val boredomTriggerAt: Int = 0, + val boredomPenaltyCount: Int = 0, + val ultraTrollTriggered: Boolean = false, + val lastActionName: String = "", val location: String = "Inside", val weather: String = "Sunny", val status: String = "Nervous", @@ -35,19 +45,34 @@ data class LCDPetRecordSave( val tradeInProgress: Int = 0, val tradeInStartedAtMs: Long = 0L, val currentWant: String = "", + val currentWantStartedAtMs: Long = 0L, + val nextWantAtMs: Long = 0L, val petName: String = "", val petTemplateId: String = "classic", val isSleeping: Boolean = false, val sleepMode: String = "", val roughWakeAlert: Boolean = false, val autoSleepStartedAtMs: Long = 0L, + val outsideEnergyNextTickAtMs: Long = 0L, + val nextWeatherAtMs: Long = 0L, + val bondReadyAtMs: Long = 0L, + val careCycleNextTickAtMs: Long = 0L, + val rainHappinessNextTickAtMs: Long = 0L, + val pottyNextTickAtMs: Long = 0L, + val dirtyHealthNextTickAtMs: Long = 0L, val careJournalRaw: String = "", val lastSavedAtMs: Long = System.currentTimeMillis() ) +data class LCDPetCreatedWant( + val wantName: String, + val petName: String +) + private object LCDPetSaveKeys { val BOND = intPreferencesKey("bond") val PET_AGE_DAYS = intPreferencesKey("pet_age_days") + val NEXT_AGE_AT_MS = longPreferencesKey("next_age_at_ms") val CARE_SCORE = intPreferencesKey("care_score") val NEGLECT_SCORE = intPreferencesKey("neglect_score") val HUNGER = intPreferencesKey("hunger") @@ -57,6 +82,17 @@ private object LCDPetSaveKeys { val DIRTINESS = intPreferencesKey("dirtiness") val POTTY = intPreferencesKey("potty") val BOREDOM = intPreferencesKey("boredom") + val STARVING_COUNT = intPreferencesKey("starving_count") + val RECOVERY_COUNT = intPreferencesKey("recovery_count") + val SICK_COUNT = intPreferencesKey("sick_count") + val PLAY_ENERGY_COUNT = intPreferencesKey("play_energy_count") + val SAME_ACTION_STREAK = intPreferencesKey("same_action_streak") + val BOREDOM_TRIGGER_AT = intPreferencesKey("boredom_trigger_at") + val BOREDOM_PENALTY_COUNT = + intPreferencesKey("boredom_penalty_count") + val ULTRA_TROLL_TRIGGERED = + booleanPreferencesKey("ultra_troll_triggered") + val LAST_ACTION_NAME = stringPreferencesKey("last_action_name") val LOCATION = stringPreferencesKey("location") val WEATHER = stringPreferencesKey("weather") val STATUS = stringPreferencesKey("status") @@ -68,13 +104,32 @@ private object LCDPetSaveKeys { val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress") val TRADE_IN_STARTED_AT_MS = longPreferencesKey("trade_in_started_at_ms") val CURRENT_WANT = stringPreferencesKey("current_want") + val CURRENT_WANT_STARTED_AT_MS = + longPreferencesKey("current_want_started_at_ms") + val NEXT_WANT_AT_MS = longPreferencesKey("next_want_at_ms") val PET_NAME = stringPreferencesKey("pet_name") val PET_TEMPLATE_ID = stringPreferencesKey("pet_template_id") val IS_SLEEPING = booleanPreferencesKey("is_sleeping") val SLEEP_MODE = stringPreferencesKey("sleep_mode") val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert") val AUTO_SLEEP_STARTED_AT_MS = longPreferencesKey("auto_sleep_started_at_ms") + val OUTSIDE_ENERGY_NEXT_TICK_AT_MS = + longPreferencesKey("outside_energy_next_tick_at_ms") + val NEXT_WEATHER_AT_MS = + longPreferencesKey("next_weather_at_ms") + val BOND_READY_AT_MS = + longPreferencesKey("bond_ready_at_ms") + val CARE_CYCLE_NEXT_TICK_AT_MS = + longPreferencesKey("care_cycle_next_tick_at_ms") + val RAIN_HAPPINESS_NEXT_TICK_AT_MS = + longPreferencesKey("rain_happiness_next_tick_at_ms") + val POTTY_NEXT_TICK_AT_MS = + longPreferencesKey("potty_next_tick_at_ms") + val DIRTY_HEALTH_NEXT_TICK_AT_MS = + longPreferencesKey("dirty_health_next_tick_at_ms") val CARE_JOURNAL = stringPreferencesKey("care_journal") + val APPEARANCE = stringPreferencesKey("appearance") + val PET_HOME_COLOR = stringPreferencesKey("pet_home_color") val LAST_SAVED_AT_MS = longPreferencesKey("last_saved_at_ms") } @@ -83,6 +138,8 @@ val Context.lcdPetRecordSaveFlow: Flow LCDPetRecordSave( bond = preferences[LCDPetSaveKeys.BOND] ?: 0, petAgeDays = preferences[LCDPetSaveKeys.PET_AGE_DAYS] ?: 0, + nextAgeAtMs = + preferences[LCDPetSaveKeys.NEXT_AGE_AT_MS] ?: 0L, careScore = preferences[LCDPetSaveKeys.CARE_SCORE] ?: 0, neglectScore = preferences[LCDPetSaveKeys.NEGLECT_SCORE] ?: 0, hunger = preferences[LCDPetSaveKeys.HUNGER] ?: 5, @@ -92,6 +149,24 @@ val Context.lcdPetRecordSaveFlow: Flow dirtiness = preferences[LCDPetSaveKeys.DIRTINESS] ?: 0, potty = preferences[LCDPetSaveKeys.POTTY] ?: 0, boredom = preferences[LCDPetSaveKeys.BOREDOM] ?: 0, + starvingCount = + preferences[LCDPetSaveKeys.STARVING_COUNT] ?: 0, + recoveryCount = + preferences[LCDPetSaveKeys.RECOVERY_COUNT] ?: 0, + sickCount = + preferences[LCDPetSaveKeys.SICK_COUNT] ?: 0, + playEnergyCount = + preferences[LCDPetSaveKeys.PLAY_ENERGY_COUNT] ?: 0, + sameActionStreak = + preferences[LCDPetSaveKeys.SAME_ACTION_STREAK] ?: 0, + boredomTriggerAt = + preferences[LCDPetSaveKeys.BOREDOM_TRIGGER_AT] ?: 0, + boredomPenaltyCount = + preferences[LCDPetSaveKeys.BOREDOM_PENALTY_COUNT] ?: 0, + ultraTrollTriggered = + preferences[LCDPetSaveKeys.ULTRA_TROLL_TRIGGERED] ?: false, + lastActionName = + preferences[LCDPetSaveKeys.LAST_ACTION_NAME] ?: "", location = preferences[LCDPetSaveKeys.LOCATION] ?: "Inside", weather = preferences[LCDPetSaveKeys.WEATHER] ?: "Sunny", status = preferences[LCDPetSaveKeys.STATUS] ?: "Nervous", @@ -103,17 +178,73 @@ val Context.lcdPetRecordSaveFlow: Flow tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0, tradeInStartedAtMs = preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] ?: 0L, currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "", + currentWantStartedAtMs = + preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] ?: 0L, + nextWantAtMs = + preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] ?: 0L, petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "", petTemplateId = preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] ?: "classic", isSleeping = preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false, sleepMode = preferences[LCDPetSaveKeys.SLEEP_MODE] ?: "", roughWakeAlert = preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] ?: false, autoSleepStartedAtMs = preferences[LCDPetSaveKeys.AUTO_SLEEP_STARTED_AT_MS] ?: 0L, + outsideEnergyNextTickAtMs = + preferences[LCDPetSaveKeys.OUTSIDE_ENERGY_NEXT_TICK_AT_MS] ?: 0L, + nextWeatherAtMs = + preferences[LCDPetSaveKeys.NEXT_WEATHER_AT_MS] ?: 0L, + bondReadyAtMs = + preferences[LCDPetSaveKeys.BOND_READY_AT_MS] ?: 0L, + careCycleNextTickAtMs = + preferences[LCDPetSaveKeys.CARE_CYCLE_NEXT_TICK_AT_MS] ?: 0L, + rainHappinessNextTickAtMs = + preferences[LCDPetSaveKeys.RAIN_HAPPINESS_NEXT_TICK_AT_MS] ?: 0L, + pottyNextTickAtMs = + preferences[LCDPetSaveKeys.POTTY_NEXT_TICK_AT_MS] ?: 0L, + dirtyHealthNextTickAtMs = + preferences[LCDPetSaveKeys.DIRTY_HEALTH_NEXT_TICK_AT_MS] ?: 0L, careJournalRaw = preferences[LCDPetSaveKeys.CARE_JOURNAL] ?: "", lastSavedAtMs = preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] ?: System.currentTimeMillis() ) } +val Context.lcdPetAppearanceFlow: Flow + get() = lcdPetDataStore.data.map { preferences -> + preferences[LCDPetSaveKeys.APPEARANCE] ?: "System" + } + +suspend fun Context.setLCDPetAppearance(appearance: String) { + val savedAppearance = + appearance.takeIf { it in setOf("System", "Light", "Dark") } + ?: "System" + + lcdPetDataStore.edit { preferences -> + preferences[LCDPetSaveKeys.APPEARANCE] = savedAppearance + } +} + +val Context.lcdPetHomeColorFlow: Flow + get() = lcdPetDataStore.data.map { preferences -> + preferences[LCDPetSaveKeys.PET_HOME_COLOR] ?: "LCD" + } + +suspend fun Context.setLCDPetHomeColor(homeColor: String) { + val savedHomeColor = + homeColor.takeIf { + it in setOf("LCD", "R", "G", "B", "Y", "P", "K") + } ?: "LCD" + + lcdPetDataStore.edit { preferences -> + preferences[LCDPetSaveKeys.PET_HOME_COLOR] = savedHomeColor + } +} + +suspend fun Context.setLCDPetNextAgeAtMs(nextAgeAtMs: Long) { + lcdPetDataStore.edit { preferences -> + preferences[LCDPetSaveKeys.NEXT_AGE_AT_MS] = + nextAgeAtMs.coerceAtLeast(0L) + } +} + suspend fun Context.saveLCDPetStats( bond: Int, petAgeDays: Int, @@ -126,6 +257,15 @@ suspend fun Context.saveLCDPetStats( dirtiness: Int, potty: Int, boredom: Int, + starvingCount: Int, + recoveryCount: Int, + sickCount: Int, + playEnergyCount: Int, + sameActionStreak: Int, + boredomTriggerAt: Int, + boredomPenaltyCount: Int, + ultraTrollTriggered: Boolean, + lastActionName: String, location: String, weather: String, status: String, @@ -143,6 +283,14 @@ suspend fun Context.saveLCDPetStats( sleepMode: String, roughWakeAlert: Boolean, autoSleepStartedAtMs: Long, + currentWantStartedAtMs: Long = 0L, + nextWantAtMs: Long = 0L, + nextWeatherAtMs: Long, + bondReadyAtMs: Long, + careCycleNextTickAtMs: Long, + rainHappinessNextTickAtMs: Long, + pottyNextTickAtMs: Long, + dirtyHealthNextTickAtMs: Long, careJournalRaw: String? = null ) { lcdPetDataStore.edit { preferences -> @@ -157,6 +305,24 @@ suspend fun Context.saveLCDPetStats( preferences[LCDPetSaveKeys.DIRTINESS] = dirtiness preferences[LCDPetSaveKeys.POTTY] = potty preferences[LCDPetSaveKeys.BOREDOM] = boredom + preferences[LCDPetSaveKeys.STARVING_COUNT] = + starvingCount.coerceAtLeast(0) + preferences[LCDPetSaveKeys.RECOVERY_COUNT] = + recoveryCount.coerceAtLeast(0) + preferences[LCDPetSaveKeys.SICK_COUNT] = + sickCount.coerceAtLeast(0) + preferences[LCDPetSaveKeys.PLAY_ENERGY_COUNT] = + playEnergyCount.coerceAtLeast(0) + preferences[LCDPetSaveKeys.SAME_ACTION_STREAK] = + sameActionStreak.coerceAtLeast(0) + preferences[LCDPetSaveKeys.BOREDOM_TRIGGER_AT] = + boredomTriggerAt.coerceAtLeast(0) + preferences[LCDPetSaveKeys.BOREDOM_PENALTY_COUNT] = + boredomPenaltyCount.coerceAtLeast(0) + preferences[LCDPetSaveKeys.ULTRA_TROLL_TRIGGERED] = + ultraTrollTriggered + preferences[LCDPetSaveKeys.LAST_ACTION_NAME] = + lastActionName preferences[LCDPetSaveKeys.LOCATION] = location preferences[LCDPetSaveKeys.WEATHER] = weather preferences[LCDPetSaveKeys.STATUS] = status @@ -168,6 +334,22 @@ suspend fun Context.saveLCDPetStats( preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] = tradeInStartedAtMs preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant + preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = + currentWantStartedAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = + nextWantAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.NEXT_WEATHER_AT_MS] = + nextWeatherAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.BOND_READY_AT_MS] = + bondReadyAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.CARE_CYCLE_NEXT_TICK_AT_MS] = + careCycleNextTickAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.RAIN_HAPPINESS_NEXT_TICK_AT_MS] = + rainHappinessNextTickAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.POTTY_NEXT_TICK_AT_MS] = + pottyNextTickAtMs.coerceAtLeast(0L) + preferences[LCDPetSaveKeys.DIRTY_HEALTH_NEXT_TICK_AT_MS] = + dirtyHealthNextTickAtMs.coerceAtLeast(0L) preferences[LCDPetSaveKeys.PET_NAME] = petName preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] = petTemplateId preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping @@ -180,3 +362,88 @@ suspend fun Context.saveLCDPetStats( preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = System.currentTimeMillis() } } + +suspend fun Context.createLCDPetWantIfDue( + nowMs: Long = System.currentTimeMillis() +): LCDPetCreatedWant? { + var createdWant: LCDPetCreatedWant? = null + + lcdPetDataStore.edit { preferences -> + val currentWant = + preferences[LCDPetSaveKeys.CURRENT_WANT].orEmpty() + val nextWantAtMs = + preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] ?: 0L + val petName = + preferences[LCDPetSaveKeys.PET_NAME].orEmpty() + val location = + preferences[LCDPetSaveKeys.LOCATION] ?: "Inside" + val bond = + preferences[LCDPetSaveKeys.BOND] ?: 0 + val status = + preferences[LCDPetSaveKeys.STATUS] ?: "Nervous" + val isSleeping = + preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false + val isTradingInPet = + preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] ?: false + val isFindingNewPet = + preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] ?: false + + val canCreateWant = + currentWant.isBlank() && + nextWantAtMs > 0L && + nowMs >= nextWantAtMs && + petName.isNotBlank() && + status !in setOf("Sick", "Weak") && + !isSleeping && + !isTradingInPet && + !isFindingNewPet + + if (!canCreateWant) { + return@edit + } + + val possibleWants = + if (location == "Inside") { + listOf("Snack", "Play", "Pet", "Talk", "Outside") + + if (bond >= 100) listOf("Bath") else emptyList() + } else { + listOf("Play", "Pet", "Talk") + } + + val wantName = possibleWants.random() + + preferences[LCDPetSaveKeys.CURRENT_WANT] = wantName + preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = nowMs + preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = 0L + preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = nowMs + + createdWant = LCDPetCreatedWant( + wantName = wantName, + petName = petName + ) + } + + return createdWant +} + +suspend fun Context.clearLCDPetWants() { + lcdPetDataStore.edit { preferences -> + preferences[LCDPetSaveKeys.CURRENT_WANT] = "" + preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = 0L + preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = 0L + } +} + +suspend fun Context.setLCDPetNextWantAtMs(nextWantAtMs: Long) { + lcdPetDataStore.edit { preferences -> + preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = + nextWantAtMs.coerceAtLeast(0L) + } +} + +suspend fun Context.setLCDPetOutsideEnergyNextTickAtMs(nextTickAtMs: Long) { + lcdPetDataStore.edit { preferences -> + preferences[LCDPetSaveKeys.OUTSIDE_ENERGY_NEXT_TICK_AT_MS] = + nextTickAtMs.coerceAtLeast(0L) + } +} diff --git a/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt b/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt index fec268b..a33f2a9 100644 --- a/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt +++ b/app/src/main/java/today/notfeelingit/lcdpet/ui/theme/Theme.kt @@ -1,6 +1,8 @@ package today.notfeelingit.lcdpet.ui.theme +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @@ -8,25 +10,24 @@ private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40 +) - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 ) @Composable fun LCDPetTheme( + darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { MaterialTheme( - colorScheme = LightColorScheme, + colorScheme = + if (darkTheme) DarkColorScheme + else LightColorScheme, typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/res/raw/need_urgent.ogg b/app/src/main/res/raw/need_urgent.ogg new file mode 100644 index 0000000000000000000000000000000000000000..746147f62326198fe1a0bffe9473449a0e2a0c36 GIT binary patch literal 36658 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�uN?@QSYF%sJrT0Tgdjj^+qRtv z_!4NhP%;MM@sxxsGX$x(gbluQ*deO z4^yZRH(_{qSp4b8$)_WxwI1hxM)J}&86n~h!$E!9Tn3sy)q;?H>mwL5HRJzvX6n#3!gSgxrxXF2e6{6aF6-Kw%@BxvMa@iykcj?77F8h+@ z+q>~-7?0E4nYfTxvM#iZAt#tKNC+;}*6>l;U!ZR%KFg^m&eRoCKBB$3bV~q{o7&2T zy)Vqe`b-}27|BK~2@+0XmaGhj{+Lz{3*v3-Om1F;IbTRh&Lsw&0B!7VtRO%EBW<;| zcTg-^s)>>m^NMDJRbE7s__4c(%EOcv{;w}W3C0_qrOHY0bR_60sf`;}n!23GNm_*m8c>xbFc^xtPxK(?lPq!Q zPArPSH{l{cqn5=F_U{JMG$qi(=ZvaZ*w_t`Ju#hWG!y7*0m{C8_cwe-Dg&PBPe?9M zP?r!`twUnMSk`zba3_~Ea0lgD4hCw3?3rsd_f}pen$EqNccL+IFdvbg4s^SE9AD8C z=VOo$ZRNi{jazIU*XUM`VJ1u%-Cd56A*7CnJn6p$u9+vzu@yDZIK%y9*|f2Nt{2QQ zuhG-^t)|9KzmnEl5*DR zi;b>#-+`-1>)pabxTSnZ{{9mfC|RGpx<;?ren&~XvrnDs>mdpZ0(=7lwbWl8e#V7)VO4lszUSk`j2w{ ziLk(K|^RS_`4oer}EmAqt;Bmp{a94Fu>14@&F817_$Ol!i##RCx%Y|W#Q{21&${>2lv{hANb8(7sG+ zpL83ml=++Igp-%2+NF06in5s3j{{!sA$5FLZN?6ANzXkY%EHPurv@F*9ebC4uenjk zh(w=jmYhKo?Yc^mU8`qcmQHWvBZ>Zj%E4xFd}gr7LkP9BF^FQ6wui>mhqa6AbL{0f zU*cta=%mZ968xeJ%syP1RcwEe09zCzTH5AEedH?hc*72hzcBj2CPvzQjS>Emowq2H z3qOnMjr6-vtl9j`+a2Ucbo&s>{q-B3X3zeo>csZ)om7GgOuIKVb6mYEcy;hq3EDqH z85aMU(4**_6-#kg36si_rroRf+Xn-ce@%0148O4cw{01}E|TE4C!5DTs)fg%Zwix- zF0DCo2oNcHLvb_N%t5<_tH)gaxJ1v8e9a3n8%u zUXpOcutQrM-uk;eDo>dm7AyJx?bDiM^(^d>o05H5bh0r!>f3r-9xpdcvv;|y#(lrL zziC{+$eePibKGY(VoI=y#};;Fgnw()_(^)0{c0gcxg9&}y41Y6o4K-lcslcLg;?)6t|ukt$z`T^ zhlewv^DCMYftmF14+C0DP)Hp8DzQ@ z6z5ko%-uH_RFd5|azxD7$)nA)%zrH@>=bHf*NDumGuP)8M&|iBQoB}zM9(Q65$uaP zp{ELZy$3XS-szx?di_>OKF8Dpu^{fJ+;aT?s*nOwh|-Y_SM*?RHh zI5u2~W`N_xM*f~9q;J1Une+X}l|0aVm@eg=ZSHIvJtW0LUmdBZW?BP&MN$nhQ|D-D OIS^K_{r7Lg5dRO+vG