diff --git a/README.md b/README.md index 2089209..fde3d77 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ It is designed to feel like a simple handheld digital pet: feed your pet, bathe ## Features +- Starter pet templates - Offline/local-first virtual pet - LCD-style toy-inspired interface - Care actions: feed, bathe, nap, walk, potty, pet, and talk diff --git a/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt b/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt index 1dc406b..93826ab 100644 --- a/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt +++ b/app/src/main/java/today/notfeelingit/lcdpet/MainActivity.kt @@ -72,6 +72,75 @@ private data class CareJournalEntry( 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) @@ -376,6 +445,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { var pottyFaceBag by remember { mutableStateOf(emptyList()) } 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) } @@ -583,6 +654,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { currentWant = save.currentWant petName = save.petName pendingPetName = save.petName + petTemplateId = save.petTemplateId.ifBlank { "classic" } + pendingPetTemplateId = petTemplateId isSleeping = save.isSleeping sleepMode = save.sleepMode roughWakeAlert = save.roughWakeAlert @@ -648,7 +721,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { val petAgeText = statAgeText(petAgeDays) val displayPetName = petName.ifBlank { "Pet" } - val needsPetName = saveLoaded && petName.isBlank() && !isTradingInPet && !isFindingNewPet + val needsPetName = saveLoaded && (petName.isBlank() || petTemplateId.isBlank()) && !isTradingInPet && !isFindingNewPet val trimmedPendingPetName = pendingPetName.trim() val normalizedPendingPetName = trimmedPendingPetName.lowercase() val isNameValidationActive = !testingMode @@ -656,6 +729,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { 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" @@ -796,13 +871,13 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { } val petArt = buildAnnotatedString { - append(" /---\\ \n") - append("( ") + append(selectedPetTemplate.topLine) + append(selectedPetTemplate.facePrefix) withStyle(SpanStyle(fontWeight = if (pottyFaceOverride.isNotEmpty()) FontWeight.Bold else FontWeight.Normal)) { append(petExpression) } - append(" )\n") - append(" /| |\\ ") + append(selectedPetTemplate.faceSuffix) + append(selectedPetTemplate.feetLine) } val petColor = if (status == "Sick") sickGreen else lcdDark @@ -1343,6 +1418,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { careJournalEntries = emptyList() petName = "" pendingPetName = "" + petTemplateId = "classic" + pendingPetTemplateId = "classic" happyOutsideTrip = false petAgeDays = 0 careScore = 0 @@ -1397,6 +1474,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = 0, currentWant = currentWant, petName = petName, + petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, @@ -1438,6 +1516,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = 0, currentWant = currentWant, petName = petName, + petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, @@ -1652,6 +1731,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = tradeInProgress, currentWant = currentWant, petName = petName, + petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, @@ -1700,6 +1780,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = tradeInProgressBlocks, currentWant = currentWant, petName = petName, + petTemplateId = petTemplateId, isSleeping = isSleeping, sleepMode = sleepMode, roughWakeAlert = roughWakeAlert, @@ -2310,6 +2391,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { tradeInProgress = tradeInProgress, currentWant = currentWant, petName = petName, + petTemplateId = petTemplateId, isSleeping = true, sleepMode = "Auto", roughWakeAlert = roughWakeAlert, @@ -2470,7 +2552,75 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(6.dp) ) { Text( - text = "NAME NEW PET", + 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() @@ -2522,6 +2672,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { lcdDark = lcdDark, lcdFaded = lcdFaded, onClick = { + petTemplateId = pendingPetTemplateId petName = trimmedPendingPetName.take(12) pendingPetName = petName statsMenuOpen = false diff --git a/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt b/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt index 5ab5055..29ba206 100644 --- a/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt +++ b/app/src/main/java/today/notfeelingit/lcdpet/PetStorage.kt @@ -35,6 +35,7 @@ data class LCDPetRecordSave( val tradeInProgress: Int = 0, val currentWant: String = "", val petName: String = "", + val petTemplateId: String = "classic", val isSleeping: Boolean = false, val sleepMode: String = "", val roughWakeAlert: Boolean = false, @@ -66,6 +67,7 @@ private object LCDPetSaveKeys { val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress") val CURRENT_WANT = stringPreferencesKey("current_want") val PET_NAME = stringPreferencesKey("pet_name") + val PET_TEMPLATE_ID = stringPreferencesKey("pet_template_id") val IS_SLEEPING = booleanPreferencesKey("is_sleeping") val SLEEP_MODE = stringPreferencesKey("sleep_mode") val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert") @@ -99,6 +101,7 @@ val Context.lcdPetRecordSaveFlow: Flow tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0, currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "", petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "", + petTemplateId = preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] ?: "classic", isSleeping = preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false, sleepMode = preferences[LCDPetSaveKeys.SLEEP_MODE] ?: "", roughWakeAlert = preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] ?: false, @@ -131,6 +134,7 @@ suspend fun Context.saveLCDPetStats( tradeInProgress: Int, currentWant: String, petName: String, + petTemplateId: String = "classic", isSleeping: Boolean, sleepMode: String, roughWakeAlert: Boolean, @@ -160,6 +164,7 @@ suspend fun Context.saveLCDPetStats( preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant preferences[LCDPetSaveKeys.PET_NAME] = petName + preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] = petTemplateId preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping preferences[LCDPetSaveKeys.SLEEP_MODE] = sleepMode preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] = roughWakeAlert diff --git a/fastlane/metadata/android/en-US/changelogs/1.txt b/fastlane/metadata/android/en-US/changelogs/1.txt index 19f590d..224e0aa 100644 --- a/fastlane/metadata/android/en-US/changelogs/1.txt +++ b/fastlane/metadata/android/en-US/changelogs/1.txt @@ -1,3 +1,3 @@ Initial v1.0 release candidate. -Adds the core LCDPet care loop, local save/load, Wants, care status tracking, optional local notifications, Credits / Licenses screen, and offline-first Android gameplay. +Adds starter pet templates, the core LCDPet care loop, local save/load, Wants, care status tracking, optional local notifications, Credits / Licenses screen, and offline-first Android gameplay. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 31915b0..90dc7f7 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -3,6 +3,7 @@ LCDPet is a tiny offline LCD-style virtual pet for Android. Feed your pet, bathe it, let it nap, take it outside, handle random Wants, and keep an eye on its care status. The app is designed around a simple handheld digital-pet feel with local progress, toy-like interactions, and a low-pressure care loop. Features: +* Starter pet templates * Offline/local-first virtual pet * LCD-style toy-inspired interface diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_choose_new_pet_templates.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_choose_new_pet_templates.png new file mode 100644 index 0000000..9eccdae Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_choose_new_pet_templates.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_recovery_system.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_recovery_system.png deleted file mode 100644 index 52f2c00..0000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_recovery_system.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_beanbag_asleep_rain.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_beanbag_asleep_rain.png new file mode 100644 index 0000000..5e339c3 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_beanbag_asleep_rain.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_watch_tv.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_watch_tv.png similarity index 100% rename from fastlane/metadata/android/en-US/images/phoneScreenshots/03_watch_tv.png rename to fastlane/metadata/android/en-US/images/phoneScreenshots/05_watch_tv.png