5746 lines
208 KiB
Kotlin
5746 lines
208 KiB
Kotlin
package today.notfeelingit.lcdpet
|
||
import androidx.core.app.NotificationManagerCompat
|
||
import androidx.core.app.NotificationCompat
|
||
import android.app.PendingIntent
|
||
import android.net.Uri
|
||
import android.content.Intent
|
||
import android.content.Context
|
||
import android.app.NotificationManager
|
||
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
|
||
import androidx.activity.ComponentActivity
|
||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||
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
|
||
import androidx.compose.foundation.gestures.detectTapGestures
|
||
|
||
import androidx.compose.foundation.clickable
|
||
import androidx.compose.foundation.layout.Arrangement
|
||
import androidx.compose.foundation.layout.Column
|
||
import androidx.compose.foundation.layout.Row
|
||
import androidx.compose.foundation.layout.RowScope
|
||
import androidx.compose.foundation.layout.Spacer
|
||
import androidx.compose.foundation.layout.fillMaxSize
|
||
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
|
||
import androidx.compose.ui.text.font.FontFamily
|
||
import androidx.compose.ui.text.withStyle
|
||
import androidx.compose.ui.text.TextStyle
|
||
import androidx.compose.ui.text.font.FontWeight
|
||
import androidx.compose.ui.text.style.TextAlign
|
||
import androidx.compose.ui.tooling.preview.Preview
|
||
import androidx.compose.ui.unit.dp
|
||
import androidx.compose.ui.unit.sp
|
||
import androidx.lifecycle.Lifecycle
|
||
import androidx.lifecycle.LifecycleEventObserver
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.delay
|
||
import kotlinx.coroutines.withContext
|
||
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,
|
||
val action: String,
|
||
val met: Boolean,
|
||
val responseMs: Long?,
|
||
val happenedAtMs: Long
|
||
)
|
||
|
||
private data class PetTemplate(
|
||
val id: String,
|
||
val label: String,
|
||
val topLine: String,
|
||
val facePrefix: String,
|
||
val faceSuffix: String,
|
||
val feetLine: String
|
||
)
|
||
|
||
private val lcdPetTemplates = listOf(
|
||
PetTemplate(
|
||
id = "classic",
|
||
label = "Classic",
|
||
topLine = " /---\\ \n",
|
||
facePrefix = "( ",
|
||
faceSuffix = " )\n",
|
||
feetLine = " /| |\\ "
|
||
),
|
||
PetTemplate(
|
||
id = "scruffy",
|
||
label = "Scruffy",
|
||
topLine = " /^^^\\ \n",
|
||
facePrefix = "( ",
|
||
faceSuffix = " )\n",
|
||
feetLine = " /| |\\ "
|
||
),
|
||
PetTemplate(
|
||
id = "bean",
|
||
label = "Round Bean",
|
||
topLine = " .---. \n",
|
||
facePrefix = "( ",
|
||
faceSuffix = " )\n",
|
||
feetLine = " /|\\ "
|
||
),
|
||
PetTemplate(
|
||
id = "pocketpup",
|
||
label = "Pocket Pup",
|
||
topLine = " /\\_/\\ \n",
|
||
facePrefix = "( ",
|
||
faceSuffix = " )\n",
|
||
feetLine = " /|\\ "
|
||
),
|
||
PetTemplate(
|
||
id = "batty",
|
||
label = "Batty",
|
||
topLine = "/\\---/\\\n",
|
||
facePrefix = "( ",
|
||
faceSuffix = " )\n",
|
||
feetLine = " /|\\ "
|
||
),
|
||
PetTemplate(
|
||
id = "goblin",
|
||
label = "Goblin",
|
||
topLine = " /\\_/\\ \n",
|
||
facePrefix = "( ",
|
||
faceSuffix = " )\n",
|
||
feetLine = " /| |\\ "
|
||
)
|
||
)
|
||
|
||
private fun lcdPetTemplateById(id: String): PetTemplate {
|
||
return lcdPetTemplates.firstOrNull { it.id == id } ?: lcdPetTemplates.first()
|
||
}
|
||
|
||
private fun renderPetTemplateText(template: PetTemplate, expression: String): String {
|
||
return template.topLine + template.facePrefix + expression + template.faceSuffix + template.feetLine
|
||
}
|
||
|
||
|
||
class MainActivity : ComponentActivity() {
|
||
override fun onCreate(savedInstanceState: Bundle?) {
|
||
super.onCreate(savedInstanceState)
|
||
enableEdgeToEdge()
|
||
createLCDPetNotificationChannel()
|
||
|
||
setContent {
|
||
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)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun createLCDPetNotificationChannel() {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
val channel = NotificationChannel(
|
||
LCDPET_NOTIFICATION_CHANNEL_ID,
|
||
"LCDPet Alerts",
|
||
NotificationManager.IMPORTANCE_DEFAULT
|
||
).apply {
|
||
description = "Pet care reminders and LCDPet status alerts."
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
@Composable
|
||
fun ActionLabel(
|
||
text: String,
|
||
enabled: Boolean,
|
||
lcdDark: Color,
|
||
lcdFaded: Color,
|
||
onClick: () -> Unit
|
||
) {
|
||
Text(
|
||
text = text,
|
||
fontWeight = FontWeight.Bold,
|
||
color = if (enabled) lcdDark else lcdFaded,
|
||
modifier = if (enabled) {
|
||
Modifier.clickable {
|
||
onClick()
|
||
}
|
||
} else {
|
||
Modifier
|
||
}
|
||
)
|
||
}
|
||
|
||
@Composable
|
||
fun LCDActionButton(
|
||
text: String,
|
||
enabled: Boolean,
|
||
lcdDark: Color,
|
||
lcdFaded: Color,
|
||
onClick: () -> Unit
|
||
) {
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.border(2.dp, if (enabled) lcdDark else lcdFaded)
|
||
.clickable(enabled = enabled) {
|
||
onClick()
|
||
}
|
||
.padding(vertical = 8.dp),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text(
|
||
text = text,
|
||
fontWeight = FontWeight.Bold,
|
||
color = if (enabled) lcdDark else lcdFaded,
|
||
textAlign = TextAlign.Center
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun LCDMenuButton(
|
||
text: String,
|
||
enabled: Boolean,
|
||
lcdDark: Color,
|
||
lcdFaded: Color,
|
||
onClick: () -> Unit
|
||
) {
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(if (enabled) lcdDark else lcdFaded)
|
||
.border(2.dp, if (enabled) lcdDark else lcdFaded)
|
||
.clickable(enabled = enabled) {
|
||
onClick()
|
||
}
|
||
.padding(vertical = 10.dp),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text(
|
||
text = text,
|
||
fontWeight = FontWeight.Bold,
|
||
color =
|
||
if (enabled && lcdDark == Color.White) {
|
||
Color.Black
|
||
} else if (enabled) {
|
||
Color.White
|
||
} else {
|
||
lcdDark
|
||
},
|
||
textAlign = TextAlign.Center
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun LCDSubButton(
|
||
text: String,
|
||
enabled: Boolean,
|
||
lcdDark: Color,
|
||
lcdFaded: Color,
|
||
onClick: () -> Unit,
|
||
modifier: Modifier = Modifier,
|
||
fillWidthFraction: Float = 0.86f
|
||
) {
|
||
Box(
|
||
modifier = modifier
|
||
.fillMaxWidth(fillWidthFraction)
|
||
.border(2.dp, if (enabled) lcdDark else lcdFaded)
|
||
.clickable(enabled = enabled) {
|
||
onClick()
|
||
}
|
||
.padding(vertical = 8.dp),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text(
|
||
text = text,
|
||
fontWeight = FontWeight.Bold,
|
||
color = if (enabled) lcdDark else lcdFaded,
|
||
textAlign = TextAlign.Center
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun LCDHoldSubButton(
|
||
text: String,
|
||
enabled: Boolean,
|
||
lcdDark: Color,
|
||
lcdFaded: Color,
|
||
onPress: () -> Unit,
|
||
onRelease: (releasedNormally: Boolean) -> Unit,
|
||
modifier: Modifier = Modifier,
|
||
fillWidthFraction: Float = 0.86f
|
||
) {
|
||
Box(
|
||
modifier = modifier
|
||
.fillMaxWidth(fillWidthFraction)
|
||
.border(2.dp, if (enabled) lcdDark else lcdFaded)
|
||
.pointerInput(enabled) {
|
||
detectTapGestures(
|
||
onPress = {
|
||
if (enabled) {
|
||
onPress()
|
||
var releasedNormally = false
|
||
|
||
try {
|
||
releasedNormally = tryAwaitRelease()
|
||
} finally {
|
||
onRelease(releasedNormally)
|
||
}
|
||
}
|
||
}
|
||
)
|
||
}
|
||
.padding(vertical = 8.dp),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text(
|
||
text = text,
|
||
fontWeight = FontWeight.Bold,
|
||
color = if (enabled) lcdDark else lcdFaded,
|
||
textAlign = TextAlign.Center
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun LCDSubMenuFrame(
|
||
lcdDark: Color,
|
||
content: @Composable RowScope.() -> Unit
|
||
) {
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.border(2.dp, lcdDark)
|
||
.padding(6.dp),
|
||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
content()
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||
val lcdBackground = Color(0xFFC4C7B0)
|
||
val lcdFrameDark = Color(0xFF343434)
|
||
val sickGreen = Color(0xFF2F7D32)
|
||
|
||
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 = {}
|
||
)
|
||
|
||
var microphonePermissionGranted by remember {
|
||
mutableStateOf(
|
||
ContextCompat.checkSelfPermission(
|
||
context,
|
||
Manifest.permission.RECORD_AUDIO
|
||
) == PackageManager.PERMISSION_GRANTED
|
||
)
|
||
}
|
||
|
||
val microphonePermissionLauncher = rememberLauncherForActivityResult(
|
||
contract = ActivityResultContracts.RequestPermission(),
|
||
onResult = { granted ->
|
||
microphonePermissionGranted = granted
|
||
}
|
||
)
|
||
|
||
LaunchedEffect(Unit) {
|
||
if (
|
||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||
ContextCompat.checkSelfPermission(
|
||
context,
|
||
Manifest.permission.POST_NOTIFICATIONS
|
||
) != PackageManager.PERMISSION_GRANTED
|
||
) {
|
||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||
}
|
||
}
|
||
val creditsText = remember(context) {
|
||
runCatching {
|
||
context.assets.open("audio_credits.txt").bufferedReader().use { it.readText() }
|
||
}.getOrElse {
|
||
"Credits unavailable."
|
||
}
|
||
}
|
||
|
||
val howToText = remember(context) {
|
||
runCatching {
|
||
context.assets.open("how_to.txt").bufferedReader().use { it.readText() }
|
||
}.getOrElse {
|
||
"HOW TO content could not be loaded."
|
||
}
|
||
}
|
||
val newPetWeatherOptions = listOf("Sunny", "Cloudy", "Rain", "Snow")
|
||
var location by remember { mutableStateOf("Inside") }
|
||
var weather by remember { mutableStateOf(newPetWeatherOptions.random()) }
|
||
var status by remember { mutableStateOf("Nervous") }
|
||
|
||
var hunger by remember { mutableIntStateOf(5) }
|
||
var health by remember { mutableIntStateOf(10) }
|
||
var happiness by remember { mutableIntStateOf(5) }
|
||
var energy by remember { mutableIntStateOf(5) }
|
||
var bond by remember { mutableIntStateOf(0) }
|
||
var dirtiness by remember { mutableIntStateOf(0) }
|
||
var potty by remember { mutableIntStateOf(0) }
|
||
var boredom by remember { mutableIntStateOf(0) }
|
||
|
||
var overfeedCount by remember { mutableIntStateOf(0) }
|
||
var starvingCount by remember { mutableIntStateOf(0) }
|
||
var overfeedNeedWindowOpen by remember { mutableStateOf(false) }
|
||
var hungerNeedWindowOpen by remember { mutableStateOf(false) }
|
||
var recoveryCount by remember { mutableIntStateOf(0) }
|
||
var sickCount by remember { mutableIntStateOf(0) }
|
||
var goodFeedCount by remember { mutableIntStateOf(0) }
|
||
var feedPottyCount by remember { mutableIntStateOf(0) }
|
||
var playEnergyCount by remember { mutableIntStateOf(0) }
|
||
var sameActionStreak by remember { mutableIntStateOf(0) }
|
||
var boredomTriggerAt by remember { mutableIntStateOf((3..13).random()) }
|
||
var boredomPenaltyCount by remember { mutableIntStateOf(0) }
|
||
var ultraTrollTriggered by remember { mutableStateOf(false) }
|
||
var lastActionName by remember { mutableStateOf("") }
|
||
var weakCause by remember { mutableStateOf("") }
|
||
var activityMood by remember { mutableStateOf("") }
|
||
var frameProp by remember { mutableStateOf("") }
|
||
var framePropAnimation by remember { mutableStateOf("") }
|
||
var framePropAnimationId by remember { mutableIntStateOf(0) }
|
||
var framePropStyle by remember { mutableIntStateOf(0) }
|
||
var pottyStain by remember { mutableStateOf("") }
|
||
var isSleeping by remember { mutableStateOf(false) }
|
||
var sleepMode by remember { mutableStateOf("") }
|
||
var roughWakeAlert by remember { mutableStateOf(false) }
|
||
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) }
|
||
var tradeInStartedAtMs by remember { mutableStateOf(0L) }
|
||
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<AudioRecord?>(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) }
|
||
var socializeMenuOpen by remember { mutableStateOf(false) }
|
||
var statsMenuOpen by remember { mutableStateOf(false) }
|
||
var careJournalOpen by remember { mutableStateOf(false) }
|
||
var infoPanelOpen by remember { mutableStateOf(false) }
|
||
var infoPanelType by remember { mutableStateOf("Credits") }
|
||
var infoPanelButtonCorner by remember { mutableStateOf("BottomRight") }
|
||
var careJournalTab by remember { mutableStateOf("All") }
|
||
var testWantMenuOpen by remember { mutableStateOf(false) }
|
||
var testBondMenuOpen by remember { mutableStateOf(false) }
|
||
var downtimeMode by remember { mutableStateOf(DowntimeMode.None) }
|
||
var selectedTvChannel by remember { mutableStateOf(TvChannel.Weather) }
|
||
|
||
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) }
|
||
var energyNeedNotificationOpen by remember { mutableStateOf(false) }
|
||
var careJournalEntries by remember { mutableStateOf<List<CareJournalEntry>>(emptyList()) }
|
||
var pottyFaceOverride by remember { mutableStateOf("") }
|
||
var pottyFaceBag by remember { mutableStateOf(emptyList<String>()) }
|
||
var petName by remember { mutableStateOf("") }
|
||
var pendingPetName by remember { mutableStateOf("") }
|
||
var petTemplateId by remember { mutableStateOf("classic") }
|
||
var pendingPetTemplateId by remember { mutableStateOf("classic") }
|
||
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) }
|
||
|
||
val testingMode = (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||
|
||
val secondMs = 1_000L
|
||
val minuteMs = 60L * secondMs
|
||
val hourMs = 60L * minuteMs
|
||
|
||
val bondCooldownMs = if (testingMode) 60_000L else 6L * hourMs
|
||
val sickTickMs = if (testingMode) 12_000L else 30L * minuteMs
|
||
val weakRecoveryCheckMs = if (testingMode) 25_000L else 2L * hourMs
|
||
val recoverWeakMs = if (testingMode) 25_000L else 2L * hourMs
|
||
val recoveringTickMs = if (testingMode) 5_000L else 10L * minuteMs
|
||
val hungerTickMs = if (testingMode) 20_000L else 2L * hourMs
|
||
val rainHappinessTickMs = if (testingMode) 20_000L else 1L * hourMs
|
||
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) {
|
||
listOf(45_000L, 60_000L, 90_000L)
|
||
} else {
|
||
listOf(4L * hourMs, 6L * hourMs, 9L * hourMs, 12L * hourMs)
|
||
}
|
||
val wantCooldownOptionsMs = if (testingMode) {
|
||
listOf(45_000L, 60_000L, 90_000L)
|
||
} else {
|
||
listOf(8L * hourMs, 12L * hourMs, 18L * hourMs, 24L * hourMs)
|
||
}
|
||
val wantDurationMs = if (testingMode) 20_000L else 10L * minuteMs
|
||
|
||
val lowBondBathMs = if (testingMode) 2_000L else 2L * minuteMs
|
||
val mediumBondBathMs = if (testingMode) 4_000L else 4L * minuteMs
|
||
val trustedBathMs = if (testingMode) 6_000L else 6L * minuteMs
|
||
|
||
val sleepOptionsMs = if (testingMode) {
|
||
listOf(8_000L, 11_000L, 14_000L, 18_000L)
|
||
} else {
|
||
listOf(2L * hourMs, 3L * hourMs, 4L * hourMs, 5L * hourMs)
|
||
}
|
||
|
||
val autoSleepDelayMs = if (testingMode) 15_000L else 30L * minuteMs
|
||
val roughSleepEnergyTickMs = if (testingMode) 30_000L else 60L * 60L * 1000L
|
||
val offlineOutsideEnergyDrainTickMs = roughSleepEnergyTickMs
|
||
val roughWakeAlertMs = if (testingMode) 4_000L else 30_000L
|
||
|
||
val tradeInWaitMs = if (testingMode) 5_000L else 2L * hourMs
|
||
val findingNewPetMs = if (testingMode) 2_000L else 5_000L
|
||
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<CareJournalEntry>): String {
|
||
return entries.take(50).joinToString("\n") { entry ->
|
||
listOf(
|
||
entry.category,
|
||
entry.action,
|
||
entry.met.toString(),
|
||
entry.responseMs?.toString().orEmpty(),
|
||
entry.happenedAtMs.toString()
|
||
).joinToString("|")
|
||
}
|
||
}
|
||
|
||
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
|
||
)
|
||
}
|
||
|
||
fun wantNotificationText(wantName: String): String {
|
||
val notificationPetName = petName.ifBlank { "Your pet" }
|
||
|
||
return when (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."
|
||
}
|
||
}
|
||
|
||
fun sendWantNotification(wantName: String) {
|
||
sendLCDPetNotification(
|
||
notificationId = 2001,
|
||
title = "LCDPet",
|
||
message = wantNotificationText(wantName)
|
||
)
|
||
}
|
||
|
||
fun sendNeedNotification(notificationId: Int, message: String) {
|
||
sendLCDPetNotification(
|
||
notificationId = notificationId,
|
||
title = "LCDPet",
|
||
message = message,
|
||
urgentNeed = true
|
||
)
|
||
}
|
||
|
||
fun sendTestNotification() {
|
||
sendLCDPetNotification(
|
||
notificationId = 1001,
|
||
title = "LCDPet",
|
||
message = "Test alert: your pet says beep boop.",
|
||
allowInForeground = true
|
||
)
|
||
}
|
||
|
||
fun decodeCareJournalEntries(raw: String): List<CareJournalEntry> {
|
||
return raw.lines()
|
||
.mapNotNull { line ->
|
||
val parts = line.split("|")
|
||
|
||
if (parts.size != 5) {
|
||
return@mapNotNull null
|
||
}
|
||
|
||
val met = when (parts[2]) {
|
||
"true" -> true
|
||
"false" -> false
|
||
else -> return@mapNotNull null
|
||
}
|
||
|
||
val responseMs = if (parts[3].isBlank()) {
|
||
null
|
||
} else {
|
||
parts[3].toLongOrNull()
|
||
}
|
||
|
||
val happenedAtMs = parts[4].toLongOrNull() ?: return@mapNotNull null
|
||
|
||
CareJournalEntry(
|
||
category = parts[0],
|
||
action = parts[1],
|
||
met = met,
|
||
responseMs = responseMs,
|
||
happenedAtMs = happenedAtMs
|
||
)
|
||
}
|
||
.take(50)
|
||
}
|
||
|
||
LaunchedEffect(Unit) {
|
||
context.lcdPetRecordSaveFlow.collect { save ->
|
||
// Only hydrate state from storage on the first load.
|
||
// After that, saves come from this screen and should not re-trigger restored auto-sleep.
|
||
if (saveLoaded) {
|
||
return@collect
|
||
}
|
||
|
||
bond = save.bond
|
||
petAgeDays = save.petAgeDays
|
||
nextAgeAtMs = save.nextAgeAtMs
|
||
careScore = save.careScore
|
||
neglectScore = save.neglectScore
|
||
hunger = save.hunger
|
||
health = save.health
|
||
happiness = save.happiness
|
||
energy = save.energy
|
||
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
|
||
weakCause = save.weakCause
|
||
pottyStain = save.pottyStain
|
||
happyOutsideTrip = save.happyOutsideTrip
|
||
isTradingInPet = save.isTradingInPet
|
||
isFindingNewPet = save.isFindingNewPet
|
||
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 firstOutsideEnergyTickAtMs =
|
||
if (save.outsideEnergyNextTickAtMs > 0L) {
|
||
save.outsideEnergyNextTickAtMs
|
||
} else {
|
||
save.lastSavedAtMs + offlineOutsideEnergyDrainTickMs
|
||
}
|
||
|
||
if (
|
||
!save.isSleeping &&
|
||
save.location == "Outside" &&
|
||
!save.isTradingInPet &&
|
||
!save.isFindingNewPet
|
||
) {
|
||
when {
|
||
save.energy <= 0 -> {
|
||
energy = 0
|
||
isSleeping = true
|
||
sleepMode = "Auto"
|
||
autoSleepStartedAtMs = save.lastSavedAtMs
|
||
outsideEnergyNextTickAtMs = 0L
|
||
restoredAutoSleep = true
|
||
}
|
||
|
||
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 {
|
||
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
|
||
}
|
||
}
|
||
|
||
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 = lcdMeterBar(happiness)
|
||
val energyBar = lcdMeterBar(energy)
|
||
val bondBlocks = bond / 10
|
||
val bondBar = lcdMeterBar(bondBlocks)
|
||
val bondPercent = bond
|
||
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 {
|
||
return "$value ${if (value == 1) single else plural}"
|
||
}
|
||
|
||
return when {
|
||
days < 7 -> amount(days, "day", "days")
|
||
days < 30 -> amount((days / 7).coerceAtLeast(1), "week", "weeks")
|
||
days < 365 -> amount((days / 30).coerceAtLeast(1), "month", "months")
|
||
else -> amount((days / 365).coerceAtLeast(1), "year", "years")
|
||
}
|
||
}
|
||
|
||
val petAgeText = statAgeText(petAgeDays)
|
||
val displayPetName = petName.ifBlank { "Pet" }
|
||
val needsPetName = saveLoaded && (petName.isBlank() || petTemplateId.isBlank()) && !isTradingInPet && !isFindingNewPet
|
||
val trimmedPendingPetName = pendingPetName.trim()
|
||
val normalizedPendingPetName = trimmedPendingPetName.lowercase()
|
||
val isNameValidationActive = !testingMode
|
||
val reservedPetNames = setOf("patch", "petch")
|
||
val isReservedPetName = isNameValidationActive && normalizedPendingPetName in reservedPetNames
|
||
val hasValidPetNameLetters = trimmedPendingPetName.isNotEmpty() && trimmedPendingPetName.all { it.isLetter() }
|
||
val canKeepPetName = hasValidPetNameLetters && !isReservedPetName
|
||
val pendingPetTemplate = lcdPetTemplateById(pendingPetTemplateId)
|
||
val selectedPetTemplate = lcdPetTemplateById(petTemplateId)
|
||
|
||
val bondRelationship = when {
|
||
bond >= 100 -> "BFF"
|
||
bond >= 80 -> "Best Friend"
|
||
bond >= 50 -> "Pal"
|
||
bond >= 25 -> "Settled"
|
||
else -> "New Pet"
|
||
}
|
||
|
||
|
||
|
||
val tradeInProgressBar = "▓".repeat(tradeInProgress) + "▒".repeat(tradeInProgressBlocks - tradeInProgress)
|
||
|
||
fun tradeCountdownText(remainingMs: Long): String {
|
||
val totalSeconds = ((remainingMs + 999L) / 1_000L).coerceAtLeast(0L)
|
||
val hours = totalSeconds / 3_600L
|
||
val minutes = (totalSeconds % 3_600L) / 60L
|
||
val seconds = totalSeconds % 60L
|
||
|
||
return when {
|
||
hours > 0L -> "${hours}h ${minutes}m"
|
||
minutes > 0L -> "${minutes}m ${seconds}s"
|
||
else -> "${seconds}s"
|
||
}
|
||
}
|
||
|
||
fun addCareJournalEntry(
|
||
category: String,
|
||
action: String,
|
||
met: Boolean,
|
||
responseMs: Long? = null
|
||
) {
|
||
val nowMs = System.currentTimeMillis()
|
||
val entry = CareJournalEntry(
|
||
category = category,
|
||
action = action,
|
||
met = met,
|
||
responseMs = responseMs,
|
||
happenedAtMs = nowMs
|
||
)
|
||
|
||
careJournalEntries = (listOf(entry) + careJournalEntries).take(50)
|
||
}
|
||
|
||
fun logNeedJournal(action: String, met: Boolean) {
|
||
addCareJournalEntry(
|
||
category = "Need",
|
||
action = action,
|
||
met = met,
|
||
responseMs = null
|
||
)
|
||
}
|
||
|
||
fun logWantJournal(action: String, met: Boolean) {
|
||
val nowMs = System.currentTimeMillis()
|
||
val responseMs = if (met && currentWantStartedAtMs > 0L) {
|
||
(nowMs - currentWantStartedAtMs).coerceAtLeast(0L)
|
||
} else {
|
||
null
|
||
}
|
||
|
||
addCareJournalEntry(
|
||
category = "Want",
|
||
action = action,
|
||
met = met,
|
||
responseMs = responseMs
|
||
)
|
||
}
|
||
|
||
fun journalResponseText(responseMs: Long?): String {
|
||
if (responseMs == null) {
|
||
return "N/A"
|
||
}
|
||
|
||
return when {
|
||
responseMs < minuteMs -> "${(responseMs / secondMs).coerceAtLeast(1L)}s"
|
||
responseMs < hourMs -> "${(responseMs / minuteMs).coerceAtLeast(1L)}m"
|
||
else -> "${(responseMs / hourMs).coerceAtLeast(1L)}h"
|
||
}
|
||
}
|
||
|
||
fun journalDateText(happenedAtMs: Long): String {
|
||
val ageMs = (System.currentTimeMillis() - happenedAtMs).coerceAtLeast(0L)
|
||
|
||
return when {
|
||
ageMs < secondMs -> "now"
|
||
ageMs < minuteMs -> "${(ageMs / secondMs).coerceAtLeast(1L)}s ago"
|
||
ageMs < hourMs -> "${(ageMs / minuteMs).coerceAtLeast(1L)}m ago"
|
||
ageMs < 24L * hourMs -> "${(ageMs / hourMs).coerceAtLeast(1L)}h ago"
|
||
ageMs < 7L * 24L * hourMs -> "${(ageMs / (24L * hourMs)).coerceAtLeast(1L)}d ago"
|
||
else -> "${(ageMs / (7L * 24L * hourMs)).coerceAtLeast(1L)}w ago"
|
||
}
|
||
}
|
||
|
||
val wantText = when (currentWant) {
|
||
"Snack" -> "$displayPetName wants a Snack!"
|
||
"Play" -> "$displayPetName wants to Play!"
|
||
"Pet" -> "$displayPetName wants Pets!"
|
||
"Talk" -> "$displayPetName is looking for you!"
|
||
"Outside" -> "$displayPetName wants to go Outside!"
|
||
"Bath" -> "$displayPetName wants a Bath!"
|
||
else -> ""
|
||
}
|
||
|
||
val recoveryNeeded = (3 + sickCount - if (happiness >= 8) 1 else 0).coerceAtLeast(1)
|
||
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"
|
||
status in listOf("Sick", "Weak", "Recovering", "RecoverWeak") -> status
|
||
roughWakeAlert -> "RoughWake"
|
||
roughWakeNervousHold -> "Nervous"
|
||
isSleeping -> "Asleep"
|
||
status == "Nervous" -> "Nervous"
|
||
|
||
// If the pet asked to go outside, the whole outing is exciting.
|
||
location == "Outside" && happyOutsideTrip -> "Happy"
|
||
|
||
// 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
|
||
|
||
// Outside trust tiers.
|
||
isOutsideGoodWeather && bond < 40 -> "Nervous"
|
||
isOutsideGoodWeather && bond < 90 -> "Calm"
|
||
isOutsideGoodWeather -> "Happy"
|
||
|
||
// Inside trust tiers: low bond should not look fully happy yet.
|
||
happiness <= 3 -> "Sad"
|
||
bond < 30 -> "Nervous"
|
||
bond < 80 -> "Calm"
|
||
else -> "Happy"
|
||
}
|
||
val petExpression = pottyFaceOverride.ifBlank {
|
||
when (displayStatus) {
|
||
"Calm" -> "o_o"
|
||
"Excited" -> "^o^"
|
||
"Asleep" -> "-.-"
|
||
"Sad" -> ";_;"
|
||
"Nervous" -> "o_O"
|
||
"Sick" -> ">_<"
|
||
"Weak" -> "-_-"
|
||
"Recovering" -> "o_<"
|
||
"RecoverWeak" -> "o_-"
|
||
"Trading" -> "..."
|
||
else -> "^_^"
|
||
}
|
||
}
|
||
|
||
val petArt = buildAnnotatedString {
|
||
append(selectedPetTemplate.topLine)
|
||
append(selectedPetTemplate.facePrefix)
|
||
withStyle(SpanStyle(fontWeight = if (pottyFaceOverride.isNotEmpty()) FontWeight.Bold else FontWeight.Normal)) {
|
||
append(petExpression)
|
||
}
|
||
append(selectedPetTemplate.faceSuffix)
|
||
append(selectedPetTemplate.feetLine)
|
||
}
|
||
|
||
val petColor =
|
||
if (status == "Sick") sickGreen else petFrameForeground
|
||
|
||
val alertText = when {
|
||
// Locked event moments.
|
||
roughWakeAlert -> "Rough wake!"
|
||
roughWakeNervousHold -> "Nervous"
|
||
isFindingNewPet -> "Finding new pet..."
|
||
isTradingInPet -> "Trading pet..."
|
||
|
||
// Consequence alerts: show the cause when sickness came from a missed need.
|
||
weakCause == "Starving" &&
|
||
hunger >= 9 &&
|
||
status in listOf("Sick", "Weak") ->
|
||
"$displayPetName is hungry and sick!"
|
||
|
||
weakCause == "Overfed" &&
|
||
hunger <= 1 &&
|
||
status in listOf("Sick", "Weak") ->
|
||
"$displayPetName is stuffed and sick!"
|
||
|
||
weakCause == "Dirty" &&
|
||
dirtiness >= 10 &&
|
||
status in listOf("Sick", "Weak") ->
|
||
"$displayPetName needs a bath and is sick!"
|
||
|
||
// Need warnings.
|
||
energy <= 0 && !isSleeping -> "$displayPetName needs a nap!"
|
||
energy == 1 && !isSleeping -> "$displayPetName is exhausted!"
|
||
hunger >= 9 -> "$displayPetName is hungry!"
|
||
hunger <= 1 -> "$displayPetName is stuffed!"
|
||
dirtiness >= 9 -> "$displayPetName needs a bath!"
|
||
location == "Inside" && potty >= 9 -> "$displayPetName has to potty!"
|
||
pottyStain.isNotEmpty() -> "$displayPetName had an accident!"
|
||
|
||
// Health/status consequences.
|
||
status == "Weak" -> "$displayPetName is weak!"
|
||
status == "Sick" -> "$displayPetName is sick!"
|
||
status in listOf("Recovering", "RecoverWeak") -> "$displayPetName is recovering!"
|
||
|
||
// 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
|
||
|
||
// Low-priority mood alerts.
|
||
boredom >= 7 -> "$displayPetName is bored!"
|
||
else -> ""
|
||
}
|
||
|
||
val framePropDisplay = when {
|
||
frameProp.isNotEmpty() -> frameProp
|
||
pottyStain.isNotEmpty() -> pottyStain
|
||
else -> " "
|
||
}
|
||
|
||
val framePropText = buildAnnotatedString {
|
||
when {
|
||
framePropAnimation == "Nap" && framePropDisplay == "ZzZ" -> {
|
||
framePropDisplay.forEachIndexed { index, character ->
|
||
val isDark = if (framePropStyle % 2 == 0) {
|
||
index % 2 == 0
|
||
} else {
|
||
index % 2 != 0
|
||
}
|
||
|
||
withStyle(
|
||
SpanStyle(
|
||
color =
|
||
if (isDark) petFrameForeground else petFrameFaded,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
) {
|
||
append(character)
|
||
}
|
||
}
|
||
}
|
||
|
||
framePropAnimation == "Bath" && framePropDisplay == "≈≈≈" -> {
|
||
framePropDisplay.forEachIndexed { index, character ->
|
||
val isActive = index == framePropStyle % 3
|
||
|
||
withStyle(
|
||
SpanStyle(
|
||
color =
|
||
if (isActive) petFrameForeground else petFrameFaded,
|
||
fontWeight = if (isActive) {
|
||
FontWeight.Bold
|
||
} else {
|
||
FontWeight.Normal
|
||
}
|
||
)
|
||
) {
|
||
append(character)
|
||
}
|
||
}
|
||
}
|
||
|
||
else -> {
|
||
append(framePropDisplay)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun nextWeather(currentWeather: String): String {
|
||
return when (currentWeather) {
|
||
"Sunny" -> "Cloudy"
|
||
"Cloudy" -> listOf("Sunny", "Rain", "Snow").random()
|
||
"Rain" -> "Cloudy"
|
||
"Snow" -> "Cloudy"
|
||
else -> "Sunny"
|
||
}
|
||
}
|
||
|
||
fun buildBond(multiplier: Int = 1, force: Boolean = false) {
|
||
if ((bondReady || force) && bond < 100) {
|
||
val bondGain = when {
|
||
bond <= 20 -> 2
|
||
bond <= 50 -> 3
|
||
bond <= 80 -> 5
|
||
else -> 7
|
||
}
|
||
|
||
bond = (bond + (bondGain * multiplier)).coerceAtMost(100)
|
||
|
||
if (!force) {
|
||
bondReady = false
|
||
bondReadyAtMs =
|
||
System.currentTimeMillis() + bondCooldownMs
|
||
}
|
||
}
|
||
}
|
||
|
||
fun toggleLocation() {
|
||
if (isTradingInPet || isFindingNewPet || isSleeping || isBathing || isTalking || framePropAnimation.isNotEmpty() || frameProp.isNotEmpty()) {
|
||
return
|
||
}
|
||
if (location == "Inside" && status in listOf("Weak", "RecoverWeak")) {
|
||
return
|
||
}
|
||
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
statsMenuOpen = false
|
||
|
||
if (location == "Inside") {
|
||
val outsideWantMatched = currentWant == "Outside"
|
||
|
||
if (outsideWantMatched) {
|
||
currentWant = ""
|
||
currentWantStartedAtMs = 0L
|
||
happyOutsideTrip = true
|
||
}
|
||
|
||
location = "Outside"
|
||
|
||
val newWeather = weather
|
||
|
||
if (outsideWantMatched) {
|
||
happiness = 10
|
||
activityMood = "Happy"
|
||
buildBond(force = true)
|
||
} else {
|
||
dirtiness = if (newWeather == "Rain") 10 else (dirtiness + 1).coerceAtMost(10)
|
||
happiness = if (newWeather == "Rain") 8 else 10
|
||
}
|
||
|
||
if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) {
|
||
status = if (newWeather == "Rain" && !outsideWantMatched) "Nervous" else "Calm"
|
||
|
||
if (newWeather != "Rain" || outsideWantMatched) {
|
||
activityMood = "Happy"
|
||
}
|
||
}
|
||
} else {
|
||
location = "Inside"
|
||
happyOutsideTrip = false
|
||
|
||
if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) {
|
||
status = "Calm"
|
||
}
|
||
}
|
||
}
|
||
|
||
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"
|
||
}
|
||
|
||
return newHealth
|
||
}
|
||
|
||
fun startFrameAnimation(animationName: String) {
|
||
framePropAnimation = animationName
|
||
framePropAnimationId += 1
|
||
}
|
||
|
||
fun completeWant(wantName: String): Boolean {
|
||
if (currentWant == wantName) {
|
||
logWantJournal(wantName, met = true)
|
||
currentWant = ""
|
||
currentWantStartedAtMs = 0L
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
fun choosePottyFace() {
|
||
val pottyFaces = listOf(
|
||
"¬.¬",
|
||
"=.=",
|
||
"•.•",
|
||
">.>",
|
||
"¬_¬",
|
||
"=_=",
|
||
"•_•",
|
||
">_>",
|
||
)
|
||
|
||
if (pottyFaceBag.isEmpty()) {
|
||
pottyFaceBag = pottyFaces.shuffled()
|
||
}
|
||
|
||
pottyFaceOverride = pottyFaceBag.first()
|
||
pottyFaceBag = pottyFaceBag.drop(1)
|
||
}
|
||
|
||
fun forceTestWant(wantName: String) {
|
||
location = "Inside"
|
||
weather = "Sunny"
|
||
happyOutsideTrip = false
|
||
|
||
if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) {
|
||
status = "Calm"
|
||
}
|
||
|
||
currentWant = wantName
|
||
currentWantStartedAtMs = System.currentTimeMillis()
|
||
sendWantNotification(wantName)
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
statsMenuOpen = false
|
||
testWantMenuOpen = false
|
||
testBondMenuOpen = false
|
||
}
|
||
|
||
fun applyBoredomPenalty() {
|
||
val boredomPenaltyEvery = when {
|
||
boredom < 4 -> 4
|
||
boredom < 7 -> 3
|
||
boredom < 9 -> 2
|
||
else -> 1
|
||
}
|
||
|
||
boredomPenaltyCount += 1
|
||
|
||
if (boredomPenaltyCount >= boredomPenaltyEvery) {
|
||
boredomPenaltyCount = 0
|
||
|
||
val statLoss = if (boredom >= 9) 2 else 1
|
||
happiness = (happiness - statLoss).coerceAtLeast(0)
|
||
energy = (energy - statLoss).coerceAtLeast(0)
|
||
}
|
||
}
|
||
|
||
fun recordAction(actionName: String) {
|
||
if (lastActionName == actionName) {
|
||
sameActionStreak += 1
|
||
|
||
if (sameActionStreak >= boredomTriggerAt) {
|
||
boredom = (boredom + 1).coerceAtMost(10)
|
||
applyBoredomPenalty()
|
||
boredomTriggerAt = sameActionStreak + (3..13).random()
|
||
}
|
||
|
||
if (sameActionStreak >= 25 && !ultraTrollTriggered) {
|
||
bond = (bond - 5).coerceAtLeast(0)
|
||
ultraTrollTriggered = true
|
||
}
|
||
} else {
|
||
lastActionName = actionName
|
||
sameActionStreak = 1
|
||
boredomTriggerAt = (3..13).random()
|
||
ultraTrollTriggered = false
|
||
boredom = (boredom - 1).coerceAtLeast(0)
|
||
boredomPenaltyCount = (boredomPenaltyCount - 1).coerceAtLeast(0)
|
||
}
|
||
}
|
||
|
||
fun feedPet() {
|
||
if (isFeeding) {
|
||
return
|
||
}
|
||
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
frameProp = "[___]"
|
||
|
||
if (completeWant("Snack")) {
|
||
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
|
||
overfeedCount = 0
|
||
hungerNeedWindowOpen = false
|
||
overfeedNeedWindowOpen = false
|
||
careScore = (careScore + 1).coerceAtMost(999)
|
||
buildBond(force = true)
|
||
return
|
||
}
|
||
|
||
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) {
|
||
recordAction("Play")
|
||
}
|
||
|
||
activityMood = "Excited"
|
||
startFrameAnimation("Ball")
|
||
careScore = (careScore + 1).coerceAtMost(999)
|
||
|
||
if (wantMatched) {
|
||
// Wanted play is an invited burst of fun.
|
||
// It should not drain hunger, energy, or build boredom.
|
||
happiness = (happiness + 2).coerceAtMost(10)
|
||
playEnergyCount = 0
|
||
buildBond(multiplier = 2, force = true)
|
||
} else {
|
||
happiness = (happiness + 1).coerceAtMost(10)
|
||
hunger = (hunger + 1).coerceAtMost(10)
|
||
playEnergyCount += 1
|
||
|
||
if (playEnergyCount >= 2) {
|
||
energy = (energy - 1).coerceAtLeast(0)
|
||
playEnergyCount = 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun petPet() {
|
||
val wantMatched = completeWant("Pet")
|
||
|
||
if (!wantMatched) {
|
||
recordAction("Pet")
|
||
}
|
||
|
||
activityMood = if (bond >= 100) "Happy" else "Calm"
|
||
startFrameAnimation("Hand")
|
||
careScore = (careScore + 1).coerceAtMost(999)
|
||
happiness = (happiness + 1).coerceAtMost(10)
|
||
|
||
if (wantMatched) {
|
||
// Wanted pets are requested attention, not repetitive pestering.
|
||
buildBond(multiplier = 2, force = true)
|
||
} else {
|
||
buildBond()
|
||
}
|
||
}
|
||
|
||
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 (
|
||
microphonePermissionGranted &&
|
||
!isTradingInPet &&
|
||
!isFindingNewPet &&
|
||
!isSleeping &&
|
||
!isBathing &&
|
||
framePropAnimation.isEmpty()
|
||
) {
|
||
isTalking = true
|
||
talkHeardSound = false
|
||
activityMood = if (bond >= 100) "Happy" else "Calm"
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
frameProp = "¢==-"
|
||
microphoneLevel = 0
|
||
|
||
val sampleRate = 16_000
|
||
val minimumBufferSize = AudioRecord.getMinBufferSize(
|
||
sampleRate,
|
||
AudioFormat.CHANNEL_IN_MONO,
|
||
AudioFormat.ENCODING_PCM_16BIT
|
||
)
|
||
val bufferSize = minimumBufferSize.coerceAtLeast(2_048)
|
||
|
||
val recorder = runCatching {
|
||
AudioRecord(
|
||
MediaRecorder.AudioSource.MIC,
|
||
sampleRate,
|
||
AudioFormat.CHANNEL_IN_MONO,
|
||
AudioFormat.ENCODING_PCM_16BIT,
|
||
bufferSize
|
||
)
|
||
}.getOrNull()
|
||
|
||
if (recorder == null || recorder.state != AudioRecord.STATE_INITIALIZED) {
|
||
recorder?.release()
|
||
isTalking = false
|
||
microphoneRecorder = null
|
||
microphoneLevel = 0
|
||
talkHeardSound = false
|
||
frameProp = ""
|
||
return
|
||
}
|
||
|
||
microphoneRecorder = recorder
|
||
recorder.startRecording()
|
||
|
||
saveScope.launch {
|
||
withContext(Dispatchers.IO) {
|
||
val audioBuffer = ShortArray(bufferSize)
|
||
|
||
while (recorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
|
||
val samplesRead = recorder.read(
|
||
audioBuffer,
|
||
0,
|
||
audioBuffer.size
|
||
)
|
||
|
||
if (samplesRead > 0) {
|
||
var peak = 0
|
||
|
||
for (index in 0 until samplesRead) {
|
||
peak = maxOf(peak, abs(audioBuffer[index].toInt()))
|
||
}
|
||
|
||
val level = when {
|
||
peak >= 12_000 -> 3
|
||
peak >= 5_000 -> 2
|
||
peak >= 1_500 -> 1
|
||
else -> 0
|
||
}
|
||
|
||
withContext(Dispatchers.Main) {
|
||
if (isTalking && microphoneRecorder === recorder) {
|
||
microphoneLevel = level
|
||
frameProp = when (level) {
|
||
3 -> "¢==-)))"
|
||
2 -> "¢==-))"
|
||
1 -> "¢==-)"
|
||
else -> "¢==-"
|
||
}
|
||
|
||
if (level > 0) {
|
||
registerHeardTalk()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun endTalk() {
|
||
if (isTalking || microphoneRecorder != null) {
|
||
isTalking = false
|
||
|
||
microphoneRecorder?.let { recorder ->
|
||
runCatching {
|
||
if (recorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
|
||
recorder.stop()
|
||
}
|
||
}
|
||
recorder.release()
|
||
}
|
||
|
||
microphoneRecorder = null
|
||
microphoneLevel = 0
|
||
talkHeardSound = false
|
||
frameProp = ""
|
||
}
|
||
}
|
||
|
||
fun walkPet() {
|
||
if (energy > 0) {
|
||
playPetSound(R.raw.pet_excited)
|
||
activityMood = "Excited"
|
||
startFrameAnimation("Footprints")
|
||
careScore = (careScore + 1).coerceAtMost(999)
|
||
health = 10
|
||
energy = (energy - 1).coerceAtLeast(0)
|
||
hunger = (hunger + 1).coerceAtMost(10)
|
||
dirtiness = (dirtiness + 1).coerceAtMost(10)
|
||
}
|
||
}
|
||
|
||
fun napPet() {
|
||
roughWakeAlert = false
|
||
|
||
// 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"
|
||
startFrameAnimation("Nap")
|
||
}
|
||
|
||
fun usePotty() {
|
||
if (location == "Outside") {
|
||
val pottyNeedMet = potty > 0
|
||
choosePottyFace()
|
||
startFrameAnimation("PottyClean")
|
||
careScore = (careScore + 1).coerceAtMost(999)
|
||
pottyNextTickAtMs = 0L
|
||
potty = 0
|
||
hunger = (hunger + 1).coerceAtMost(10)
|
||
|
||
if (pottyNeedMet) {
|
||
logNeedJournal("Potty", met = true)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun bathePet() {
|
||
if (location == "Inside" && (dirtiness > 0 || currentWant == "Bath") && !isTradingInPet && !isFindingNewPet && !isSleeping && !isBathing) {
|
||
val bathNeedMet = dirtiness > 0 || pottyStain.isNotEmpty()
|
||
bathWantMatched = completeWant("Bath")
|
||
bathBondAtStart = bond
|
||
pottyStain = ""
|
||
isBathing = true
|
||
activityMood = when {
|
||
bond >= 100 -> "Happy"
|
||
bond >= 60 -> "Calm"
|
||
else -> "Nervous"
|
||
}
|
||
startFrameAnimation("Bath")
|
||
|
||
if (bathNeedMet) {
|
||
logNeedJournal("Bath", met = true)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun resetPetAfterTradeIn() {
|
||
location = "Inside"
|
||
weather = newPetWeatherOptions.random()
|
||
nextWeatherAtMs = 0L
|
||
status = "Nervous"
|
||
hunger = 5
|
||
health = 10
|
||
happiness = 5
|
||
energy = 5
|
||
bond = 0
|
||
dirtiness = 0
|
||
potty = 0
|
||
boredom = 0
|
||
overfeedCount = 0
|
||
starvingCount = 0
|
||
overfeedNeedWindowOpen = false
|
||
hungerNeedWindowOpen = false
|
||
hungerNeedNotificationOpen = false
|
||
pottyNeedNotificationOpen = false
|
||
dirtinessNeedNotificationOpen = false
|
||
energyNeedNotificationOpen = false
|
||
recoveryCount = 0
|
||
sickCount = 0
|
||
goodFeedCount = 0
|
||
feedPottyCount = 0
|
||
playEnergyCount = 0
|
||
sameActionStreak = 0
|
||
boredomTriggerAt = (3..13).random()
|
||
boredomPenaltyCount = 0
|
||
ultraTrollTriggered = false
|
||
lastActionName = ""
|
||
weakCause = ""
|
||
activityMood = ""
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyStain = ""
|
||
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
|
||
isBathing = false
|
||
isTalking = false
|
||
bathBondAtStart = 0
|
||
bathWantMatched = false
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
statsMenuOpen = false
|
||
currentWant = ""
|
||
currentWantStartedAtMs = 0L
|
||
careJournalOpen = false
|
||
infoPanelOpen = false
|
||
careJournalTab = "All"
|
||
careJournalEntries = emptyList()
|
||
petName = ""
|
||
pendingPetName = ""
|
||
petTemplateId = "classic"
|
||
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
|
||
tradeInProgress = 0
|
||
tradeInStartedAtMs = 0L
|
||
}
|
||
|
||
fun beginTradeInPet() {
|
||
if (!isTradingInPet && !isFindingNewPet) {
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
currentWant = ""
|
||
currentWantStartedAtMs = 0L
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyStain = ""
|
||
isSleeping = false
|
||
isBathing = false
|
||
isTalking = false
|
||
sleepMode = ""
|
||
autoSleepStartedAtMs = 0L
|
||
tradeInConfirm = false
|
||
tradeInProgress = 0
|
||
tradeInStartedAtMs = System.currentTimeMillis()
|
||
isTradingInPet = true
|
||
|
||
// Force-save trade start immediately so closing the app mid-trade resumes correctly.
|
||
saveScope.launch {
|
||
context.saveLCDPetStats(
|
||
bond = bond,
|
||
petAgeDays = petAgeDays,
|
||
careScore = careScore,
|
||
neglectScore = neglectScore,
|
||
hunger = hunger,
|
||
health = health,
|
||
happiness = happiness,
|
||
energy = energy,
|
||
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,
|
||
weakCause = weakCause,
|
||
pottyStain = pottyStain,
|
||
happyOutsideTrip = happyOutsideTrip,
|
||
isTradingInPet = true,
|
||
isFindingNewPet = false,
|
||
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)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun cancelTradeInPet() {
|
||
if (isTradingInPet) {
|
||
isTradingInPet = false
|
||
tradeInConfirm = false
|
||
tradeInProgress = 0
|
||
tradeInStartedAtMs = 0L
|
||
|
||
// Force-save trade cancel immediately.
|
||
saveScope.launch {
|
||
context.saveLCDPetStats(
|
||
bond = bond,
|
||
petAgeDays = petAgeDays,
|
||
careScore = careScore,
|
||
neglectScore = neglectScore,
|
||
hunger = hunger,
|
||
health = health,
|
||
happiness = happiness,
|
||
energy = energy,
|
||
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,
|
||
weakCause = weakCause,
|
||
pottyStain = pottyStain,
|
||
happyOutsideTrip = happyOutsideTrip,
|
||
isTradingInPet = false,
|
||
isFindingNewPet = false,
|
||
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)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun cleanTestPet() {
|
||
if (!testingMode || isTradingInPet || isFindingNewPet) {
|
||
return
|
||
}
|
||
|
||
location = "Inside"
|
||
weather = "Sunny"
|
||
nextWeatherAtMs = 0L
|
||
status = if (bond < 30) "Nervous" else "Calm"
|
||
hunger = 5
|
||
health = 10
|
||
happiness = 5
|
||
energy = 10
|
||
dirtiness = 0
|
||
potty = 0
|
||
boredom = 0
|
||
overfeedCount = 0
|
||
starvingCount = 0
|
||
overfeedNeedWindowOpen = false
|
||
hungerNeedWindowOpen = false
|
||
hungerNeedNotificationOpen = false
|
||
pottyNeedNotificationOpen = false
|
||
dirtinessNeedNotificationOpen = false
|
||
energyNeedNotificationOpen = false
|
||
recoveryCount = 0
|
||
sickCount = 0
|
||
goodFeedCount = 0
|
||
feedPottyCount = 0
|
||
playEnergyCount = 0
|
||
sameActionStreak = 0
|
||
boredomTriggerAt = (3..13).random()
|
||
boredomPenaltyCount = 0
|
||
ultraTrollTriggered = false
|
||
lastActionName = ""
|
||
weakCause = ""
|
||
activityMood = ""
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyStain = ""
|
||
isSleeping = false
|
||
sleepMode = ""
|
||
autoSleepStartedAtMs = 0L
|
||
roughWakeAlert = false
|
||
roughWakeDarkFlash = false
|
||
roughWakeNervousHold = false
|
||
isBathing = false
|
||
isFeeding = false
|
||
feedingStartHunger = 0
|
||
isTalking = false
|
||
bathBondAtStart = 0
|
||
bathWantMatched = false
|
||
careMenuOpen = false
|
||
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") {
|
||
val sleptOutside = location == "Outside"
|
||
val startedAtMs = if (autoSleepStartedAtMs > 0L) autoSleepStartedAtMs else System.currentTimeMillis()
|
||
val sleptMs = (System.currentTimeMillis() - startedAtMs).coerceAtLeast(0L)
|
||
val energyGain = (sleptMs / roughSleepEnergyTickMs).toInt().coerceAtLeast(1)
|
||
|
||
energy = (energy + energyGain).coerceAtMost(10)
|
||
|
||
if (sleptOutside) {
|
||
location = "Inside"
|
||
bond = (bond - 1).coerceAtLeast(0)
|
||
}
|
||
|
||
// Keep the pet officially asleep during the dark wake-up pause.
|
||
// This prevents the UI from flashing Nervous before Rough Wake appears.
|
||
roughWakeDarkFlash = true
|
||
activityMood = "Asleep"
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
|
||
saveScope.launch {
|
||
delay(if (testingMode) 1_500L else 1_200L)
|
||
|
||
if (isSleeping && sleepMode == "Auto") {
|
||
isSleeping = false
|
||
sleepMode = ""
|
||
autoSleepStartedAtMs = 0L
|
||
autoSleepWakeAllowed = false
|
||
restoredAutoSleep = false
|
||
roughWakeDarkFlash = false
|
||
roughWakeNervousHold = false
|
||
|
||
status = "Nervous"
|
||
activityMood = "RoughWake"
|
||
roughWakeAlert = true
|
||
frameProp = "!?!"
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
happiness = (happiness - 1).coerceAtLeast(0)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 -> {
|
||
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" &&
|
||
(currentAutoSleepWakeAllowed || currentRestoredAutoSleep)
|
||
) {
|
||
restoredAutoSleep = true
|
||
}
|
||
|
||
if (currentSuppressBackgroundWant) {
|
||
nextWantAtMs = 0L
|
||
saveScope.launch {
|
||
context.setLCDPetNextWantAtMs(0L)
|
||
}
|
||
suppressBackgroundWantOnNextStop = false
|
||
}
|
||
}
|
||
|
||
else -> Unit
|
||
}
|
||
}
|
||
|
||
lifecycleOwner.lifecycle.addObserver(observer)
|
||
onDispose {
|
||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||
endTalk()
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(saveLoaded, restoredAutoSleep) {
|
||
if (saveLoaded && restoredAutoSleep && isSleeping && sleepMode == "Auto") {
|
||
activityMood = "Asleep"
|
||
roughWakeDarkFlash = true
|
||
|
||
if (framePropAnimation != "Nap") {
|
||
startFrameAnimation("Nap")
|
||
}
|
||
|
||
delay(if (testingMode) 1_500L else 2_000L)
|
||
|
||
if (isSleeping && sleepMode == "Auto") {
|
||
wakeFromAutoSleep()
|
||
}
|
||
}
|
||
}
|
||
|
||
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,
|
||
careScore,
|
||
neglectScore,
|
||
hunger,
|
||
health,
|
||
happiness,
|
||
energy,
|
||
dirtiness,
|
||
potty,
|
||
boredom,
|
||
starvingCount,
|
||
recoveryCount,
|
||
sickCount,
|
||
playEnergyCount,
|
||
sameActionStreak,
|
||
boredomTriggerAt,
|
||
boredomPenaltyCount,
|
||
ultraTrollTriggered,
|
||
lastActionName,
|
||
location,
|
||
weather,
|
||
status,
|
||
weakCause,
|
||
pottyStain,
|
||
happyOutsideTrip,
|
||
isTradingInPet,
|
||
isFindingNewPet,
|
||
tradeInProgress,
|
||
currentWant,
|
||
currentWantStartedAtMs,
|
||
nextWantAtMs,
|
||
petName,
|
||
isSleeping,
|
||
sleepMode,
|
||
roughWakeAlert,
|
||
autoSleepStartedAtMs,
|
||
nextWeatherAtMs,
|
||
bondReadyAtMs,
|
||
careCycleNextTickAtMs,
|
||
rainHappinessNextTickAtMs,
|
||
pottyNextTickAtMs,
|
||
dirtyHealthNextTickAtMs,
|
||
careJournalEntries,
|
||
saveLoaded
|
||
) {
|
||
if (saveLoaded) {
|
||
context.saveLCDPetStats(
|
||
bond = bond,
|
||
petAgeDays = petAgeDays,
|
||
careScore = careScore,
|
||
neglectScore = neglectScore,
|
||
hunger = hunger,
|
||
health = health,
|
||
happiness = happiness,
|
||
energy = energy,
|
||
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,
|
||
weakCause = weakCause,
|
||
pottyStain = pottyStain,
|
||
happyOutsideTrip = happyOutsideTrip,
|
||
isTradingInPet = isTradingInPet,
|
||
isFindingNewPet = isFindingNewPet,
|
||
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)
|
||
)
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(isTradingInPet, saveLoaded, tradeInStartedAtMs) {
|
||
if (isTradingInPet && saveLoaded) {
|
||
if (tradeInStartedAtMs <= 0L) {
|
||
val restoredProgress = tradeInProgress.coerceIn(0, tradeInProgressBlocks)
|
||
val restoredElapsedMs =
|
||
(tradeInWaitMs * restoredProgress) / tradeInProgressBlocks
|
||
|
||
tradeInStartedAtMs =
|
||
System.currentTimeMillis() - restoredElapsedMs
|
||
}
|
||
|
||
while (isTradingInPet) {
|
||
val elapsedMs =
|
||
(System.currentTimeMillis() - tradeInStartedAtMs)
|
||
.coerceAtLeast(0L)
|
||
|
||
tradeInRemainingMs =
|
||
(tradeInWaitMs - elapsedMs).coerceAtLeast(0L)
|
||
|
||
tradeInProgress =
|
||
((elapsedMs * tradeInProgressBlocks) / tradeInWaitMs)
|
||
.toInt()
|
||
.coerceIn(0, tradeInProgressBlocks)
|
||
|
||
if (tradeInRemainingMs <= 0L) {
|
||
break
|
||
}
|
||
|
||
delay(minOf(1_000L, tradeInRemainingMs))
|
||
}
|
||
|
||
if (isTradingInPet) {
|
||
tradeInProgress = tradeInProgressBlocks
|
||
tradeInRemainingMs = 0L
|
||
tradeInStartedAtMs = 0L
|
||
isTradingInPet = false
|
||
tradeInConfirm = false
|
||
statsMenuOpen = false
|
||
isFindingNewPet = true
|
||
|
||
// Force-save finding-new-pet state immediately.
|
||
context.saveLCDPetStats(
|
||
bond = bond,
|
||
petAgeDays = petAgeDays,
|
||
careScore = careScore,
|
||
neglectScore = neglectScore,
|
||
hunger = hunger,
|
||
health = health,
|
||
happiness = happiness,
|
||
energy = energy,
|
||
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,
|
||
weakCause = weakCause,
|
||
pottyStain = pottyStain,
|
||
happyOutsideTrip = happyOutsideTrip,
|
||
isTradingInPet = false,
|
||
isFindingNewPet = true,
|
||
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)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(isFindingNewPet) {
|
||
if (isFindingNewPet) {
|
||
delay(findingNewPetMs)
|
||
resetPetAfterTradeIn()
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(roughWakeAlert) {
|
||
if (roughWakeAlert) {
|
||
delay(roughWakeAlertMs)
|
||
roughWakeAlert = false
|
||
if (activityMood == "RoughWake") {
|
||
activityMood = ""
|
||
}
|
||
if (frameProp == "!?!") {
|
||
frameProp = ""
|
||
}
|
||
roughWakeNervousHold = true
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(roughWakeNervousHold) {
|
||
if (roughWakeNervousHold) {
|
||
delay(if (testingMode) 10_000L else 10_000L)
|
||
roughWakeNervousHold = false
|
||
}
|
||
}
|
||
|
||
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 &&
|
||
!isFeeding &&
|
||
!suppressBackgroundWantOnNextStop
|
||
) {
|
||
val nowMs = System.currentTimeMillis()
|
||
val dueAtMs =
|
||
if (nextWantAtMs > 0L) {
|
||
nextWantAtMs
|
||
} else {
|
||
(nowMs + wantCooldownOptionsMs.random()).also {
|
||
nextWantAtMs = it
|
||
saveScope.launch {
|
||
context.setLCDPetNextWantAtMs(it)
|
||
}
|
||
}
|
||
}
|
||
|
||
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()) {
|
||
if (nextWantAtMs != 0L) {
|
||
nextWantAtMs = 0L
|
||
saveScope.launch {
|
||
context.setLCDPetNextWantAtMs(0L)
|
||
}
|
||
}
|
||
|
||
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,
|
||
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, 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) {
|
||
bondReadyAtMs = 0L
|
||
bondReady = true
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(hunger, starvingCount, status, isSleeping, isTradingInPet, isFindingNewPet, needsPetName, saveLoaded) {
|
||
val notificationPetName = petName.ifBlank { "Your pet" }
|
||
|
||
if (!saveLoaded || isSleeping || isTradingInPet || isFindingNewPet || needsPetName) {
|
||
return@LaunchedEffect
|
||
}
|
||
|
||
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."
|
||
)
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(location, potty, isSleeping, isTradingInPet, isFindingNewPet, needsPetName, saveLoaded) {
|
||
val notificationPetName = petName.ifBlank { "Your pet" }
|
||
|
||
if (!saveLoaded || isSleeping || isTradingInPet || isFindingNewPet || needsPetName) {
|
||
return@LaunchedEffect
|
||
}
|
||
|
||
if (location == "Inside" && potty >= 9 && !pottyNeedNotificationOpen) {
|
||
pottyNeedNotificationOpen = true
|
||
sendNeedNotification(
|
||
notificationId = 3002,
|
||
message = "$notificationPetName has to potty soon."
|
||
)
|
||
} else if (location != "Inside" || potty < 9) {
|
||
pottyNeedNotificationOpen = false
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(dirtiness, status, isSleeping, isTradingInPet, isFindingNewPet, needsPetName, saveLoaded) {
|
||
val notificationPetName = petName.ifBlank { "Your pet" }
|
||
|
||
if (!saveLoaded || isSleeping || isTradingInPet || isFindingNewPet || needsPetName) {
|
||
return@LaunchedEffect
|
||
}
|
||
|
||
if (
|
||
dirtiness >= 9 &&
|
||
status !in listOf("Sick", "Weak", "Recovering", "RecoverWeak") &&
|
||
!dirtinessNeedNotificationOpen
|
||
) {
|
||
dirtinessNeedNotificationOpen = true
|
||
sendNeedNotification(
|
||
notificationId = 3003,
|
||
message = "$notificationPetName needs a bath soon."
|
||
)
|
||
} else if (dirtiness < 9 || status in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) {
|
||
dirtinessNeedNotificationOpen = false
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(energy, isSleeping, isBathing, isTalking, isTradingInPet, isFindingNewPet, needsPetName, saveLoaded) {
|
||
val notificationPetName = petName.ifBlank { "Your pet" }
|
||
|
||
if (!saveLoaded || isSleeping || isBathing || isTalking || isTradingInPet || isFindingNewPet || needsPetName) {
|
||
return@LaunchedEffect
|
||
}
|
||
|
||
if (energy <= 1 && !energyNeedNotificationOpen) {
|
||
energyNeedNotificationOpen = true
|
||
sendNeedNotification(
|
||
notificationId = 3004,
|
||
message = "$notificationPetName is exhausted. Nap soon."
|
||
)
|
||
} else if (energy > 1 || isSleeping) {
|
||
energyNeedNotificationOpen = false
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
if (status == "Sick" && weakCause == "Overfed" && hunger > 0) {
|
||
status = "Recovering"
|
||
recoveryCount = 0
|
||
}
|
||
} else if (status == "Weak") {
|
||
if (location == "Inside" && energy > 0 && hunger < 10) {
|
||
status = "RecoverWeak"
|
||
health = 1
|
||
}
|
||
} else if (status == "RecoverWeak") {
|
||
if (location == "Inside" && hunger < 10) {
|
||
status = "Calm"
|
||
recoveryCount = 0
|
||
overfeedCount = 0
|
||
starvingCount = 0
|
||
sickCount = 0
|
||
weakCause = ""
|
||
}
|
||
} else if (status == "Recovering") {
|
||
recoveryCount += 1
|
||
|
||
if (recoveryCount >= recoveryNeeded && hunger < 10) {
|
||
status = "Calm"
|
||
recoveryCount = 0
|
||
overfeedCount = 0
|
||
starvingCount = 0
|
||
sickCount = 0
|
||
weakCause = ""
|
||
}
|
||
} else {
|
||
if (hunger < 10) {
|
||
hunger += 1
|
||
overfeedCount = 0
|
||
overfeedNeedWindowOpen = false
|
||
|
||
if (hunger < 10) {
|
||
starvingCount = 0
|
||
hungerNeedWindowOpen = false
|
||
}
|
||
} else {
|
||
starvingCount += 1
|
||
|
||
if (starvingCount >= 3 && !hungerNeedWindowOpen) {
|
||
logNeedJournal("Hunger", met = false)
|
||
hungerNeedWindowOpen = true
|
||
neglectScore = (neglectScore + 1).coerceAtMost(999)
|
||
weakCause = "Starving"
|
||
|
||
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,
|
||
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
|
||
}
|
||
|
||
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,
|
||
saveLoaded
|
||
) {
|
||
if (
|
||
!saveLoaded ||
|
||
isSleeping ||
|
||
isTradingInPet ||
|
||
isFindingNewPet ||
|
||
needsPetName
|
||
) {
|
||
if (pottyNextTickAtMs != 0L) {
|
||
pottyNextTickAtMs = 0L
|
||
}
|
||
return@LaunchedEffect
|
||
}
|
||
|
||
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)
|
||
} else if (location == "Inside") {
|
||
logNeedJournal("Potty", met = false)
|
||
neglectScore = (neglectScore + 1).coerceAtMost(999)
|
||
choosePottyFace()
|
||
startFrameAnimation("PottyAccident")
|
||
potty = 0
|
||
dirtiness = (dirtiness + 3).coerceAtMost(10)
|
||
} else {
|
||
pottyStain = ""
|
||
choosePottyFace()
|
||
startFrameAnimation("PottyClean")
|
||
potty = 0
|
||
damageHealth()
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
val nowMs = System.currentTimeMillis()
|
||
val dueAtMs =
|
||
if (dirtyHealthNextTickAtMs > 0L) {
|
||
dirtyHealthNextTickAtMs
|
||
} else {
|
||
nowMs + dirtyHealthTickMs
|
||
}
|
||
|
||
if (dirtyHealthNextTickAtMs != dueAtMs) {
|
||
dirtyHealthNextTickAtMs = dueAtMs
|
||
}
|
||
|
||
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(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 = ""
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(frameProp, framePropAnimation, isTalking) {
|
||
if (frameProp.isNotEmpty() && framePropAnimation.isEmpty() && !isTalking) {
|
||
delay(activityMoodMs)
|
||
frameProp = ""
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(framePropAnimation, framePropAnimationId, isSleeping, isBathing) {
|
||
when (framePropAnimation) {
|
||
"Ball" -> {
|
||
val frames = listOf(
|
||
"o ",
|
||
" o ",
|
||
" o ",
|
||
" o ",
|
||
" o",
|
||
" o ",
|
||
" o ",
|
||
" o "
|
||
)
|
||
|
||
repeat(2) {
|
||
for (frame in frames) {
|
||
frameProp = frame
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
}
|
||
}
|
||
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
}
|
||
|
||
"Hand" -> {
|
||
val frames = listOf(
|
||
"\\☻/",
|
||
"-☻/",
|
||
"/☻\\",
|
||
"\\☻-"
|
||
)
|
||
|
||
repeat(3) {
|
||
for (frame in frames) {
|
||
frameProp = frame
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
}
|
||
}
|
||
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
}
|
||
|
||
"Talk" -> {
|
||
val frames = listOf(
|
||
"¢==-",
|
||
"¢==-)",
|
||
"¢==-))",
|
||
"¢==-=)"
|
||
)
|
||
|
||
repeat(3) {
|
||
for (frame in frames) {
|
||
frameProp = frame
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
}
|
||
}
|
||
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
}
|
||
|
||
"Footprints" -> {
|
||
val frames = listOf(
|
||
". . ",
|
||
" . . ",
|
||
" . . ",
|
||
" . .",
|
||
" . . ",
|
||
" . . "
|
||
)
|
||
|
||
repeat(2) {
|
||
for (frame in frames) {
|
||
frameProp = frame
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
}
|
||
}
|
||
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
}
|
||
|
||
"PottyClean" -> {
|
||
val frames = listOf(
|
||
" . ",
|
||
" : ",
|
||
" ... ",
|
||
" . "
|
||
)
|
||
|
||
repeat(2) {
|
||
for (frame in frames) {
|
||
frameProp = frame
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
}
|
||
}
|
||
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyFaceOverride = ""
|
||
}
|
||
|
||
"PottyAccident" -> {
|
||
val frames = listOf(
|
||
" . ",
|
||
" : ",
|
||
" ... ",
|
||
" ..... "
|
||
)
|
||
|
||
for (frame in frames) {
|
||
frameProp = frame
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
}
|
||
|
||
frameProp = ""
|
||
pottyStain = " ..... "
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyFaceOverride = ""
|
||
}
|
||
|
||
"Nap" -> {
|
||
while (isSleeping && framePropAnimation == "Nap") {
|
||
frameProp = "ZzZ"
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs * 2)
|
||
|
||
frameProp = "ZzZ"
|
||
framePropStyle = 1
|
||
delay(propAnimationTickMs * 2)
|
||
}
|
||
|
||
if (!isSleeping) {
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyFaceOverride = ""
|
||
}
|
||
}
|
||
|
||
"Bath" -> {
|
||
while (isBathing && framePropAnimation == "Bath") {
|
||
frameProp = "≈≈≈"
|
||
framePropStyle = 0
|
||
delay(propAnimationTickMs)
|
||
|
||
frameProp = "≈≈≈"
|
||
framePropStyle = 1
|
||
delay(propAnimationTickMs)
|
||
|
||
frameProp = "≈≈≈"
|
||
framePropStyle = 2
|
||
delay(propAnimationTickMs)
|
||
}
|
||
|
||
if (!isBathing) {
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(isBathing) {
|
||
if (isBathing) {
|
||
isTalking = false
|
||
val bathDuration = when {
|
||
bathBondAtStart < 30 -> lowBondBathMs
|
||
bathBondAtStart < 60 -> mediumBondBathMs
|
||
else -> trustedBathMs
|
||
}
|
||
|
||
delay(bathDuration)
|
||
|
||
val dirtinessBeforeBath = dirtiness
|
||
|
||
when {
|
||
bathBondAtStart < 30 -> {
|
||
dirtiness = (dirtinessBeforeBath - 3).coerceAtLeast(0)
|
||
happiness = (happiness - 1).coerceAtLeast(0)
|
||
}
|
||
|
||
bathBondAtStart < 60 -> {
|
||
dirtiness = (dirtinessBeforeBath - 6).coerceAtLeast(0)
|
||
}
|
||
|
||
bathBondAtStart < 90 -> {
|
||
dirtiness = 0
|
||
}
|
||
|
||
else -> {
|
||
dirtiness = 0
|
||
happiness = (happiness + 1).coerceAtMost(10)
|
||
}
|
||
}
|
||
|
||
if (bathWantMatched) {
|
||
dirtiness = 0
|
||
energy = (energy + 2).coerceAtMost(10)
|
||
happiness = 10
|
||
boredom = 0
|
||
bathWantMatched = false
|
||
}
|
||
|
||
if (dirtiness < dirtinessBeforeBath) {
|
||
dirtyHealthNextTickAtMs = 0L
|
||
careScore = (careScore + 1).coerceAtMost(999)
|
||
}
|
||
|
||
if (weakCause == "Dirty" && status == "Sick" && dirtiness < 10) {
|
||
careCycleNextTickAtMs = 0L
|
||
status = "Recovering"
|
||
recoveryCount = 0
|
||
}
|
||
|
||
buildBond()
|
||
isBathing = false
|
||
activityMood = ""
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
pottyStain = ""
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(
|
||
energy,
|
||
isSleeping,
|
||
isBathing,
|
||
isTalking,
|
||
isTradingInPet,
|
||
isFindingNewPet,
|
||
framePropAnimation,
|
||
saveLoaded
|
||
) {
|
||
if (
|
||
saveLoaded &&
|
||
energy <= 0 &&
|
||
!isSleeping &&
|
||
!isBathing &&
|
||
!isTalking &&
|
||
!isTradingInPet &&
|
||
!isFindingNewPet &&
|
||
framePropAnimation.isEmpty()
|
||
) {
|
||
delay(autoSleepDelayMs)
|
||
|
||
if (
|
||
energy <= 0 &&
|
||
!isSleeping &&
|
||
!isBathing &&
|
||
!isTalking &&
|
||
!isTradingInPet &&
|
||
!isFindingNewPet &&
|
||
framePropAnimation.isEmpty()
|
||
) {
|
||
autoSleepWakeAllowed = false
|
||
autoSleepStartedAtMs = System.currentTimeMillis()
|
||
sleepMode = "Auto"
|
||
isSleeping = true
|
||
activityMood = "Asleep"
|
||
startFrameAnimation("Nap")
|
||
|
||
// Force-save auto-sleep start immediately so reopening resumes asleep.
|
||
saveScope.launch {
|
||
context.saveLCDPetStats(
|
||
bond = bond,
|
||
petAgeDays = petAgeDays,
|
||
careScore = careScore,
|
||
neglectScore = neglectScore,
|
||
hunger = hunger,
|
||
health = health,
|
||
happiness = happiness,
|
||
energy = energy,
|
||
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,
|
||
weakCause = weakCause,
|
||
pottyStain = pottyStain,
|
||
happyOutsideTrip = happyOutsideTrip,
|
||
isTradingInPet = isTradingInPet,
|
||
isFindingNewPet = isFindingNewPet,
|
||
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)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(
|
||
isSleeping,
|
||
sleepMode,
|
||
saveLoaded,
|
||
restoredManualNap
|
||
) {
|
||
if (
|
||
!saveLoaded ||
|
||
!isSleeping ||
|
||
sleepMode != "Nap" ||
|
||
restoredManualNap
|
||
) {
|
||
return@LaunchedEffect
|
||
}
|
||
|
||
isTalking = false
|
||
|
||
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 = remainingSleepMs / sleepSteps
|
||
|
||
val startHealth = health
|
||
val startHappiness = happiness
|
||
val startEnergy = energy
|
||
val startHunger = hunger
|
||
val startPotty = potty
|
||
|
||
repeat(sleepSteps) { step ->
|
||
delay(stepDelay)
|
||
|
||
val progress = step + 1
|
||
|
||
health = startHealth + ((10 - startHealth) * progress / sleepSteps)
|
||
happiness = startHappiness + ((10 - startHappiness) * progress / sleepSteps)
|
||
energy = startEnergy + ((10 - startEnergy) * progress / sleepSteps)
|
||
|
||
if (startHunger < 5) {
|
||
hunger = startHunger + ((5 - startHunger) * progress / sleepSteps)
|
||
}
|
||
|
||
if (startPotty < 7) {
|
||
potty = startPotty + ((7 - startPotty) * progress / sleepSteps)
|
||
}
|
||
}
|
||
|
||
health = 10
|
||
happiness = 10
|
||
energy = 10
|
||
|
||
if (status in listOf("Sick", "Weak", "Recovering", "RecoverWeak")) {
|
||
careCycleNextTickAtMs = 0L
|
||
status = "Calm"
|
||
weakCause = ""
|
||
overfeedCount = 0
|
||
starvingCount = 0
|
||
overfeedNeedWindowOpen = false
|
||
hungerNeedWindowOpen = false
|
||
recoveryCount = 0
|
||
sickCount = 0
|
||
}
|
||
|
||
isSleeping = false
|
||
sleepMode = ""
|
||
autoSleepStartedAtMs = 0L
|
||
activityMood = ""
|
||
|
||
if (framePropAnimation == "Nap") {
|
||
frameProp = ""
|
||
framePropAnimation = ""
|
||
framePropStyle = 0
|
||
}
|
||
}
|
||
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 = (360f * layoutScale).dp
|
||
val petFrameOuterPadding = (5f * layoutScale).dp
|
||
val petFrameInnerPadding = (6f * layoutScale).dp
|
||
val meterTopPadding = (8f * layoutScale).dp
|
||
val meterSpacing = (3f * layoutScale).dp
|
||
val controlsTopSpacing = (6f * layoutScale).dp
|
||
val actionSpacing = (8f * layoutScale).dp
|
||
|
||
CompositionLocalProvider(LocalContentColor provides lcdDark) {
|
||
Box(
|
||
modifier = modifier
|
||
.fillMaxSize()
|
||
.background(petHomeBackground)
|
||
) {
|
||
val howToTopPadding = (28f * layoutScale * layoutScale).dp
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.verticalScroll(screenScrollState)
|
||
.padding(screenPadding)
|
||
) {
|
||
if (!needsPetName) {
|
||
Row(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
horizontalArrangement = Arrangement.SpaceBetween,
|
||
verticalAlignment = Alignment.Top
|
||
) {
|
||
Column(
|
||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||
) {
|
||
Text(
|
||
text = displayPetName,
|
||
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(
|
||
text = if (statsMenuOpen) "Stats ▲" else "Stats ▼",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.clickable {
|
||
statsMenuOpen = !statsMenuOpen
|
||
|
||
if (statsMenuOpen) {
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
infoPanelOpen = false
|
||
} else {
|
||
careJournalOpen = false
|
||
testWantMenuOpen = false
|
||
testBondMenuOpen = false
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
Column(
|
||
horizontalAlignment = Alignment.End,
|
||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||
) {
|
||
Text(
|
||
text = "$location | $weather",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.clickable(
|
||
enabled = downtimeMode == DowntimeMode.None
|
||
) {
|
||
toggleLocation()
|
||
}
|
||
)
|
||
|
||
Text(
|
||
text = "Extras",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.clickable {
|
||
infoPanelType = "Extras"
|
||
infoPanelOpen = true
|
||
statsMenuOpen = false
|
||
careJournalOpen = false
|
||
testWantMenuOpen = false
|
||
testBondMenuOpen = false
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (needsPetName) {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(top = 8.dp, bottom = 6.dp)
|
||
.border(2.dp, lcdDark)
|
||
.padding(8.dp),
|
||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||
) {
|
||
Text(
|
||
text = "CHOOSE NEW PET",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Text(
|
||
text = "PET TEMPLATE",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Text(
|
||
text = "Selected: ${pendingPetTemplate.label}",
|
||
color = lcdDark,
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
lcdPetTemplates.chunked(2).forEach { rowTemplates ->
|
||
Row(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||
) {
|
||
rowTemplates.forEach { template ->
|
||
val isSelectedTemplate = pendingPetTemplateId == template.id
|
||
|
||
Box(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.border(
|
||
width = if (isSelectedTemplate) 2.dp else 1.dp,
|
||
color = if (isSelectedTemplate) lcdDark else lcdFaded
|
||
)
|
||
.clickable {
|
||
pendingPetTemplateId = template.id
|
||
}
|
||
.padding(vertical = 4.dp, horizontal = 2.dp),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Column(
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
Text(
|
||
text = renderPetTemplateText(template, "^_^"),
|
||
fontSize = 11.sp,
|
||
lineHeight = 11.sp,
|
||
fontFamily = FontFamily.Monospace,
|
||
textAlign = TextAlign.Center,
|
||
color = lcdDark
|
||
)
|
||
Text(
|
||
text = template.label.uppercase(),
|
||
fontSize = 10.sp,
|
||
lineHeight = 10.sp,
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
color = lcdDark
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Text(
|
||
text = "NAME PET",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.border(1.dp, lcdDark)
|
||
.padding(6.dp)
|
||
) {
|
||
BasicTextField(
|
||
value = pendingPetName,
|
||
onValueChange = { typed ->
|
||
pendingPetName = typed.filter { it.isLetter() }.take(12)
|
||
},
|
||
singleLine = true,
|
||
textStyle = TextStyle(
|
||
color = lcdDark,
|
||
fontSize = 18.sp,
|
||
fontWeight = FontWeight.Bold
|
||
),
|
||
cursorBrush = SolidColor(lcdDark),
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
if (pendingPetName.isBlank()) {
|
||
Text(
|
||
text = "type name...",
|
||
color = lcdFaded,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
}
|
||
}
|
||
|
||
if (isReservedPetName) {
|
||
Text(
|
||
text = "reserved name",
|
||
color = lcdFaded,
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
|
||
LCDSubButton(
|
||
text = "KEEP NAME",
|
||
enabled = canKeepPetName,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
petTemplateId = pendingPetTemplateId
|
||
petName = trimmedPendingPetName.take(12)
|
||
pendingPetName = petName
|
||
statsMenuOpen = false
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
}
|
||
)
|
||
}
|
||
|
||
return@Column
|
||
}
|
||
|
||
if (statsMenuOpen) {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(top = 6.dp, bottom = 4.dp)
|
||
.border(2.dp, lcdDark)
|
||
.padding(8.dp),
|
||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||
) {
|
||
Text(
|
||
text = "Bond",
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
|
||
Row {
|
||
Text(
|
||
text = "Time",
|
||
modifier = Modifier.width(104.dp),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(text = petAgeText)
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Strength",
|
||
modifier = Modifier.width(104.dp),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(text = "$bondPercent%")
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Status",
|
||
modifier = Modifier.width(104.dp),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(text = bondRelationship)
|
||
}
|
||
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
|
||
Text(
|
||
text = "PET MANAGEMENT",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
if (isTradingInPet) {
|
||
Text(
|
||
text = "You’ll need to wait before a new pet can be found.",
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Text(
|
||
text = "New pet search begins in ${tradeCountdownText(tradeInRemainingMs)}",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Text(
|
||
text = "[$tradeInProgressBar]",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Text(
|
||
text = "You can still change your mind.",
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "CHANGE MIND",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
cancelTradeInPet()
|
||
}
|
||
)
|
||
} else {
|
||
LCDSubButton(
|
||
text = "SET PET FREE",
|
||
enabled = !isFindingNewPet,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
beginTradeInPet()
|
||
}
|
||
)
|
||
|
||
if (testingMode) {
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
|
||
Text(
|
||
text = "TESTING TOOLS",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST NOTIFICATION",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
sendTestNotification()
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST BACKGROUND 30S",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
scheduleLCDPetBackgroundTest(context)
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST HUNGER WARNING",
|
||
enabled = !isFindingNewPet && !isTradingInPet && !isSleeping,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
hunger = 10
|
||
starvingCount = 2
|
||
hungerNeedNotificationOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST POTTY WARNING",
|
||
enabled = !isFindingNewPet && !isTradingInPet && !isSleeping,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
location = "Inside"
|
||
potty = 9
|
||
pottyNeedNotificationOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST BATH WARNING",
|
||
enabled = !isFindingNewPet && !isTradingInPet && !isSleeping,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
dirtiness = 9
|
||
dirtinessNeedNotificationOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST BG ENERGY 30S",
|
||
enabled = !isFindingNewPet && !isTradingInPet && !isSleeping,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
// 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
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "CLEAN TEST PET",
|
||
enabled = !isFindingNewPet,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
cleanTestPet()
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = if (testBondMenuOpen) "TEST BOND ▲" else "TEST BOND ▼",
|
||
enabled = !isFindingNewPet && !isTradingInPet,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
testBondMenuOpen = !testBondMenuOpen
|
||
if (testBondMenuOpen) {
|
||
testWantMenuOpen = false
|
||
careJournalOpen = false
|
||
infoPanelOpen = false
|
||
}
|
||
}
|
||
)
|
||
|
||
if (testBondMenuOpen) {
|
||
LCDSubButton(
|
||
text = "SET BOND 0",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { bond = 0 }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "SET BOND 50",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { bond = 50 }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "SET BOND 80",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { bond = 80 }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "SET BOND 100",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { bond = 100 }
|
||
)
|
||
}
|
||
|
||
|
||
LCDSubButton(
|
||
text = if (testWantMenuOpen) "TEST WANTS ▲" else "TEST WANTS ▼",
|
||
enabled = !isFindingNewPet && !isTradingInPet && !isSleeping && !isBathing,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
testWantMenuOpen = !testWantMenuOpen
|
||
if (testWantMenuOpen) {
|
||
testBondMenuOpen = false
|
||
careJournalOpen = false
|
||
infoPanelOpen = false
|
||
}
|
||
}
|
||
)
|
||
|
||
if (testWantMenuOpen) {
|
||
LCDSubButton(
|
||
text = "FORCE SNACK WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { forceTestWant("Snack") }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "FORCE PLAY WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { forceTestWant("Play") }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "FORCE PET WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { forceTestWant("Pet") }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "FORCE TALK WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { forceTestWant("Talk") }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "FORCE OUTSIDE WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { forceTestWant("Outside") }
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "FORCE BATH WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
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 &&
|
||
!isTalking &&
|
||
framePropAnimation.isEmpty(),
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
downtimeMode = DowntimeMode.TouchGrass
|
||
location = "Outside"
|
||
happyOutsideTrip = false
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
testWantMenuOpen = false
|
||
statsMenuOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST WATCH TV",
|
||
enabled = !isSleeping &&
|
||
!isBathing &&
|
||
!isTalking &&
|
||
framePropAnimation.isEmpty(),
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
downtimeMode = DowntimeMode.WatchTv
|
||
selectedTvChannel = TvChannel.Weather
|
||
location = "Inside"
|
||
happyOutsideTrip = false
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
testWantMenuOpen = false
|
||
statsMenuOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "CLEAR DOWNTIME",
|
||
enabled = downtimeMode != DowntimeMode.None,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
downtimeMode = DowntimeMode.None
|
||
testWantMenuOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST POTTY FACE",
|
||
enabled = !isSleeping && !isBathing && framePropAnimation.isEmpty(),
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
location = "Outside"
|
||
weather = "Sunny"
|
||
happyOutsideTrip = false
|
||
if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) {
|
||
status = "Calm"
|
||
}
|
||
usePotty()
|
||
testWantMenuOpen = false
|
||
statsMenuOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST POTTY ACCIDENT",
|
||
enabled = !isSleeping && !isBathing && framePropAnimation.isEmpty(),
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
location = "Inside"
|
||
happyOutsideTrip = false
|
||
if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) {
|
||
status = "Calm"
|
||
}
|
||
choosePottyFace()
|
||
startFrameAnimation("PottyAccident")
|
||
pottyNextTickAtMs = 0L
|
||
potty = 0
|
||
dirtiness = (dirtiness + 3).coerceAtMost(10)
|
||
testWantMenuOpen = false
|
||
statsMenuOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "TEST POTTY OVERFLOW",
|
||
enabled = !isSleeping && !isBathing && framePropAnimation.isEmpty(),
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
location = "Outside"
|
||
weather = "Sunny"
|
||
happyOutsideTrip = false
|
||
if (status !in listOf("Sick", "Recovering", "Weak", "RecoverWeak")) {
|
||
status = "Calm"
|
||
}
|
||
pottyStain = ""
|
||
choosePottyFace()
|
||
startFrameAnimation("PottyClean")
|
||
pottyNextTickAtMs = 0L
|
||
potty = 0
|
||
testWantMenuOpen = false
|
||
statsMenuOpen = false
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "CLEAR WANT",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
currentWant = ""
|
||
currentWantStartedAtMs = 0L
|
||
testWantMenuOpen = false
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
LCDSubButton(
|
||
text = if (careJournalOpen) "CARE JOURNAL ▲" else "CARE JOURNAL ▼",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
careJournalOpen = !careJournalOpen
|
||
if (careJournalOpen) {
|
||
infoPanelOpen = false
|
||
testWantMenuOpen = false
|
||
testBondMenuOpen = false
|
||
}
|
||
}
|
||
)
|
||
|
||
if (careJournalOpen) {
|
||
Row(
|
||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
LCDSubButton(
|
||
text = "NEEDS",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { careJournalTab = "Needs" },
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "WANTS",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { careJournalTab = "Wants" },
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "ALL",
|
||
enabled = true,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = { careJournalTab = "All" },
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f
|
||
)
|
||
}
|
||
|
||
val visibleJournalEntries = when (careJournalTab) {
|
||
"Needs" -> careJournalEntries.filter { it.category == "Need" }
|
||
"Wants" -> careJournalEntries.filter { it.category == "Want" }
|
||
else -> careJournalEntries
|
||
}
|
||
|
||
when (careJournalTab) {
|
||
"Needs" -> {
|
||
Row {
|
||
Text(
|
||
text = "Need",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.width(150.dp)
|
||
)
|
||
Text(
|
||
text = "When",
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
}
|
||
}
|
||
|
||
"Wants" -> {
|
||
Row {
|
||
Text(
|
||
text = "Want",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(1.4f),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = "Resp.",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(0.8f),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = "When",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(0.9f),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
}
|
||
|
||
else -> {
|
||
Text(
|
||
text = "All entries",
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
}
|
||
}
|
||
|
||
if (visibleJournalEntries.isEmpty()) {
|
||
Text(
|
||
text = "No entries yet.",
|
||
color = lcdFaded,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
} else {
|
||
visibleJournalEntries.take(8).forEach { entry ->
|
||
val resultMark = if (entry.met) "✓" else "✕"
|
||
val actionText = "$resultMark ${entry.action}"
|
||
|
||
when (careJournalTab) {
|
||
"Needs" -> {
|
||
Row {
|
||
Text(
|
||
text = actionText,
|
||
modifier = Modifier.width(150.dp),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(text = journalDateText(entry.happenedAtMs))
|
||
}
|
||
}
|
||
|
||
"Wants" -> {
|
||
Row {
|
||
Text(
|
||
text = actionText,
|
||
modifier = Modifier.weight(1.4f),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = entry.responseMs?.let {
|
||
journalResponseText(it)
|
||
} ?: "—",
|
||
modifier = Modifier.weight(0.8f),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = journalDateText(entry.happenedAtMs),
|
||
modifier = Modifier.weight(0.9f),
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
}
|
||
|
||
else -> {
|
||
val responsePart = if (
|
||
entry.category == "Want" &&
|
||
entry.responseMs != null
|
||
) {
|
||
" · ${journalResponseText(entry.responseMs)}"
|
||
} else {
|
||
""
|
||
}
|
||
|
||
Text(
|
||
text = "${entry.category} $actionText$responsePart · ${journalDateText(entry.happenedAtMs)}",
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.heightIn(min = petFrameMinHeight)
|
||
.padding(top = petFrameOuterPadding, bottom = petFrameOuterPadding)
|
||
.background(lcdBackground)
|
||
.border(2.dp, petFrameForeground),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.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"
|
||
else -> ""
|
||
}
|
||
|
||
if (downtimeTitle.isNotEmpty()) {
|
||
Text(
|
||
text = downtimeTitle,
|
||
fontWeight = FontWeight.Bold,
|
||
color = petFrameForeground
|
||
)
|
||
} else {
|
||
Text(
|
||
text = healthBar,
|
||
color = petFrameForeground
|
||
)
|
||
}
|
||
|
||
Spacer(
|
||
modifier = Modifier.height(
|
||
if (downtimeActive) {
|
||
(4f * layoutScale).dp
|
||
} else {
|
||
(10f * layoutScale).dp
|
||
}
|
||
)
|
||
)
|
||
|
||
if (testingMode && downtimeMode == DowntimeMode.TouchGrass) {
|
||
TouchGrassActivityScreen(
|
||
lcdDark = petFrameForeground,
|
||
onGrassTap = {}
|
||
)
|
||
} else if (testingMode && downtimeMode == DowntimeMode.WatchTv) {
|
||
WatchTvActivityScreen(
|
||
lcdDark = petFrameForeground,
|
||
selectedChannel = selectedTvChannel,
|
||
weatherLabel = weather,
|
||
onChannelSelected = { selectedTvChannel = it }
|
||
)
|
||
} else {
|
||
Text(
|
||
text = petArt,
|
||
fontSize = 24.sp,
|
||
lineHeight = 24.sp,
|
||
fontFamily = FontFamily.Monospace,
|
||
textAlign = TextAlign.Center,
|
||
color =
|
||
if (petHomeColor == "K") {
|
||
petFrameForeground
|
||
} else if (displayStatus == "Recovering") {
|
||
sickGreen
|
||
} else {
|
||
petColor
|
||
}
|
||
)
|
||
}
|
||
|
||
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 = petFrameForeground,
|
||
softWrap = false,
|
||
maxLines = 1
|
||
)
|
||
|
||
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
|
||
},
|
||
color = petFrameForeground,
|
||
textAlign = TextAlign.Center
|
||
)
|
||
}
|
||
|
||
Spacer(modifier = Modifier.height(meterTopPadding))
|
||
|
||
Text(
|
||
text = "┈".repeat(28),
|
||
color = petFrameForeground,
|
||
fontFamily = FontFamily.Monospace,
|
||
textAlign = TextAlign.Center,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
|
||
Spacer(modifier = Modifier.height(meterSpacing))
|
||
|
||
Column(
|
||
verticalArrangement = Arrangement.spacedBy(meterSpacing)
|
||
) {
|
||
val meterLabelWidth = 120.dp
|
||
val meterBarPadding = 8.dp
|
||
|
||
Row {
|
||
Text(
|
||
text = "Happiness",
|
||
modifier = Modifier.width(meterLabelWidth),
|
||
textAlign = TextAlign.End,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = happinessBar,
|
||
modifier = Modifier.padding(start = meterBarPadding),
|
||
fontFamily = FontFamily.Monospace,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Energy",
|
||
modifier = Modifier.width(meterLabelWidth),
|
||
textAlign = TextAlign.End,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = energyBar,
|
||
modifier = Modifier.padding(start = meterBarPadding),
|
||
fontFamily = FontFamily.Monospace,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Hunger",
|
||
modifier = Modifier.width(meterLabelWidth),
|
||
textAlign = TextAlign.End,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = hungerBar,
|
||
modifier = Modifier.padding(start = meterBarPadding),
|
||
fontFamily = FontFamily.Monospace,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Dirtiness",
|
||
modifier = Modifier.width(meterLabelWidth),
|
||
textAlign = TextAlign.End,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = dirtinessBar,
|
||
modifier = Modifier.padding(start = meterBarPadding),
|
||
fontFamily = FontFamily.Monospace,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Potty",
|
||
modifier = Modifier.width(meterLabelWidth),
|
||
textAlign = TextAlign.End,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = pottyBar,
|
||
modifier = Modifier.padding(start = meterBarPadding),
|
||
fontFamily = FontFamily.Monospace,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
|
||
Row {
|
||
Text(
|
||
text = "Boredom",
|
||
modifier = Modifier.width(meterLabelWidth),
|
||
textAlign = TextAlign.End,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
Text(
|
||
text = boredomBar,
|
||
modifier = Modifier.padding(start = meterBarPadding),
|
||
fontFamily = FontFamily.Monospace,
|
||
maxLines = 1,
|
||
softWrap = false
|
||
)
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
if ((isSleeping && location == "Inside") || roughWakeDarkFlash) {
|
||
Box(
|
||
modifier = Modifier
|
||
.matchParentSize()
|
||
.background(Color.Gray.copy(alpha = 0.38f))
|
||
)
|
||
}
|
||
|
||
if (isFindingNewPet) {
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.background(Color.Gray.copy(alpha = 0.28f)),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text(
|
||
text = "FINDING PET...",
|
||
fontWeight = FontWeight.Bold,
|
||
color = petFrameForeground
|
||
)
|
||
}
|
||
}
|
||
|
||
if (infoPanelOpen) {
|
||
androidx.compose.ui.window.Popup(
|
||
alignment = Alignment.TopCenter
|
||
) {
|
||
Box(
|
||
modifier = Modifier
|
||
.width(androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp.dp)
|
||
.height(androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp.dp)
|
||
.background(petHomeBackground)
|
||
.border(2.dp, lcdDark)
|
||
.padding(10.dp)
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(top = 44.dp, bottom = 54.dp)
|
||
.verticalScroll(rememberScrollState()),
|
||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||
) {
|
||
Text(
|
||
text = when (infoPanelType) {
|
||
"HowTo" -> "HOW TO"
|
||
"Extras" -> "EXTRAS"
|
||
"Appearance" -> "APPEARANCE"
|
||
"Renovation" -> "HOME RENOVATION"
|
||
else -> "CREDITS / LICENSES"
|
||
},
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
if (infoPanelType == "HowTo") {
|
||
howToText.lines().forEach { howToLine ->
|
||
Text(
|
||
text = howToLine,
|
||
fontSize = 11.sp,
|
||
lineHeight = 13.sp,
|
||
fontFamily = FontFamily.Monospace,
|
||
fontWeight = if (
|
||
howToLine.isNotBlank() &&
|
||
howToLine == howToLine.uppercase()
|
||
) {
|
||
FontWeight.Bold
|
||
} else {
|
||
FontWeight.Normal
|
||
}
|
||
)
|
||
}
|
||
} 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: ")) {
|
||
val creditUrl = creditLine.removePrefix("Sound page: ").trim()
|
||
|
||
Text(
|
||
text = "Sound page:",
|
||
fontSize = 11.sp,
|
||
lineHeight = 13.sp,
|
||
fontFamily = FontFamily.Monospace
|
||
)
|
||
|
||
Text(
|
||
text = creditUrl,
|
||
fontSize = 11.sp,
|
||
lineHeight = 13.sp,
|
||
fontFamily = FontFamily.Monospace,
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.clickable {
|
||
context.startActivity(
|
||
Intent(
|
||
Intent.ACTION_VIEW,
|
||
Uri.parse(creditUrl)
|
||
)
|
||
)
|
||
}
|
||
)
|
||
} else if (
|
||
creditLine.startsWith("https://") ||
|
||
creditLine.startsWith("http://")
|
||
) {
|
||
Text(
|
||
text = creditLine,
|
||
fontSize = 11.sp,
|
||
lineHeight = 13.sp,
|
||
fontFamily = FontFamily.Monospace,
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.clickable {
|
||
context.startActivity(
|
||
Intent(
|
||
Intent.ACTION_VIEW,
|
||
Uri.parse(creditLine)
|
||
)
|
||
)
|
||
}
|
||
)
|
||
} else {
|
||
Text(
|
||
text = creditLine,
|
||
fontSize = 11.sp,
|
||
lineHeight = 13.sp,
|
||
fontFamily = FontFamily.Monospace
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
val infoPanelControlsOnLeft = infoPanelButtonCorner in listOf("BottomLeft", "TopLeft")
|
||
|
||
Row(
|
||
modifier = Modifier.align(
|
||
when (infoPanelButtonCorner) {
|
||
"BottomLeft" -> Alignment.BottomStart
|
||
"TopLeft" -> Alignment.TopStart
|
||
"TopRight" -> Alignment.TopEnd
|
||
else -> Alignment.BottomEnd
|
||
}
|
||
),
|
||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||
) {
|
||
if (infoPanelControlsOnLeft) {
|
||
Text(
|
||
text = "CLOSE",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier
|
||
.background(petHomeBackground)
|
||
.border(1.dp, lcdDark)
|
||
.clickable {
|
||
infoPanelOpen = false
|
||
}
|
||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||
)
|
||
|
||
Text(
|
||
text = "MOVE",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier
|
||
.background(petHomeBackground)
|
||
.border(1.dp, lcdDark)
|
||
.clickable {
|
||
infoPanelButtonCorner = when (infoPanelButtonCorner) {
|
||
"BottomRight" -> "BottomLeft"
|
||
"BottomLeft" -> "TopLeft"
|
||
"TopLeft" -> "TopRight"
|
||
else -> "BottomRight"
|
||
}
|
||
}
|
||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||
)
|
||
} else {
|
||
Text(
|
||
text = "MOVE",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier
|
||
.background(petHomeBackground)
|
||
.border(1.dp, lcdDark)
|
||
.clickable {
|
||
infoPanelButtonCorner = when (infoPanelButtonCorner) {
|
||
"BottomRight" -> "BottomLeft"
|
||
"BottomLeft" -> "TopLeft"
|
||
"TopLeft" -> "TopRight"
|
||
else -> "BottomRight"
|
||
}
|
||
}
|
||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||
)
|
||
|
||
Text(
|
||
text = "CLOSE",
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier
|
||
.background(petHomeBackground)
|
||
.border(1.dp, lcdDark)
|
||
.clickable {
|
||
infoPanelOpen = false
|
||
}
|
||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Spacer(modifier = Modifier.height(controlsTopSpacing))
|
||
|
||
Column(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
verticalArrangement = Arrangement.spacedBy(actionSpacing)
|
||
) {
|
||
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" &&
|
||
(currentWant == "Snack" || hunger >= 3) &&
|
||
!(status in listOf(
|
||
"Sick",
|
||
"Weak",
|
||
"Recovering",
|
||
"RecoverWeak"
|
||
) && weakCause == "Overfed")
|
||
val canPlay = canStartAction && location == "Inside" && energy > 0 && status !in listOf(
|
||
"Weak",
|
||
"RecoverWeak"
|
||
)
|
||
val canPet = canStartAction && location == "Inside"
|
||
val canTalk = canAct && location == "Inside"
|
||
val canNap = canStartAction && location == "Inside" && energy < 10
|
||
|
||
val canPotty = canStartAction && location == "Outside" && potty > 0
|
||
val canWalk = canStartAction && location == "Outside" && weather != "Snow" && energy > 0 && status !in listOf(
|
||
"Sick",
|
||
"Recovering",
|
||
"Weak",
|
||
"RecoverWeak"
|
||
)
|
||
|
||
if (location == "Inside") {
|
||
LCDMenuButton(
|
||
text = if (careMenuOpen) "CARE ▲" else "CARE ▼",
|
||
enabled = canStartAction,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
careMenuOpen = !careMenuOpen
|
||
if (careMenuOpen) {
|
||
socializeMenuOpen = false
|
||
if (!testingMode) {
|
||
statsMenuOpen = false
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
if (careMenuOpen) {
|
||
LCDSubMenuFrame(lcdDark = lcdDark) {
|
||
LCDSubButton(
|
||
text = "BATHE",
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f,
|
||
enabled = canBathe,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
bathePet()
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "FEED",
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f,
|
||
enabled = canFeed,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
feedPet()
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "NAP",
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f,
|
||
enabled = canNap,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
napPet()
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
LCDMenuButton(
|
||
text = if (socializeMenuOpen) "SOCIALIZE ▲" else "SOCIALIZE ▼",
|
||
enabled = canStartAction,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
socializeMenuOpen = !socializeMenuOpen
|
||
if (socializeMenuOpen) {
|
||
careMenuOpen = false
|
||
if (!testingMode) {
|
||
statsMenuOpen = false
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
if (socializeMenuOpen) {
|
||
LCDSubMenuFrame(lcdDark = lcdDark) {
|
||
LCDSubButton(
|
||
text = "PET",
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f,
|
||
enabled = canPet,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
petPet()
|
||
}
|
||
)
|
||
|
||
LCDSubButton(
|
||
text = "PLAY",
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f,
|
||
enabled = canPlay,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
playPet()
|
||
}
|
||
)
|
||
|
||
LCDHoldSubButton(
|
||
text = "TALK",
|
||
modifier = Modifier.weight(1f),
|
||
fillWidthFraction = 1f,
|
||
enabled = canTalk,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onPress = {
|
||
if (microphonePermissionGranted) {
|
||
beginTalk()
|
||
} else {
|
||
microphonePermissionLauncher.launch(
|
||
Manifest.permission.RECORD_AUDIO
|
||
)
|
||
}
|
||
},
|
||
onRelease = { _ ->
|
||
endTalk()
|
||
}
|
||
)
|
||
}
|
||
}
|
||
} else {
|
||
LCDMenuButton(
|
||
text = "WALK",
|
||
enabled = canWalk,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
walkPet()
|
||
}
|
||
)
|
||
|
||
LCDMenuButton(
|
||
text = "POTTY",
|
||
enabled = canPotty,
|
||
lcdDark = lcdDark,
|
||
lcdFaded = lcdFaded,
|
||
onClick = {
|
||
usePotty()
|
||
}
|
||
)
|
||
}
|
||
|
||
if (!careMenuOpen && !socializeMenuOpen) {
|
||
Text(
|
||
text = "How To",
|
||
fontWeight = FontWeight.Bold,
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(top = howToTopPadding, bottom = 4.dp)
|
||
.clickable {
|
||
infoPanelType = "HowTo"
|
||
infoPanelOpen = true
|
||
statsMenuOpen = false
|
||
careJournalOpen = false
|
||
testWantMenuOpen = false
|
||
testBondMenuOpen = false
|
||
careMenuOpen = false
|
||
socializeMenuOpen = false
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
@Preview(showBackground = true)
|
||
@Composable
|
||
fun GreetingPreview() {
|
||
LCDPetTheme {
|
||
Greeting("LCDPet")
|
||
}
|
||
}
|