4 Commits
Author SHA1 Message Date
Kris f91de4ccfd Add starter pet templates 2026-07-05 16:21:33 -04:00
Kris 4fdc52fcd1 Add F-Droid screenshots 2026-07-05 14:48:55 -04:00
Kris 019810615a Add F-Droid metadata text 2026-07-05 14:08:12 -04:00
Kris c9f2f221ba Add README and privacy policy 2026-07-05 14:03:52 -04:00
13 changed files with 250 additions and 6 deletions
+23
View File
@@ -0,0 +1,23 @@
# Privacy Policy
LCDPet is an offline/local-first Android app.
## Data collection
LCDPet does not collect, transmit, sell, or share personal data.
## Network access
LCDPet does not request internet permission and does not connect to external servers.
## Local storage
LCDPet stores pet progress locally on the device.
## Notifications
LCDPet may request notification permission on supported Android versions. Notifications are used only for local pet care reminders.
## Third-party services
LCDPet does not use analytics, advertising SDKs, trackers, cloud services, or account systems.
+42
View File
@@ -0,0 +1,42 @@
# LCDPet
LCDPet is a tiny offline LCD-style virtual pet for Android.
It is designed to feel like a simple handheld digital pet: feed your pet, bathe it, let it nap, take it outside, handle Wants, and keep an eye on its care status.
## Features
- Starter pet templates
- Offline/local-first virtual pet
- LCD-style toy-inspired interface
- Care actions: feed, bathe, nap, walk, potty, pet, and talk
- Random Wants and care reminders
- Optional Android notifications for pet care alerts
- No ads
- No tracking
- No internet permission
- No gameplay purchases or pay-to-win mechanics
## Privacy
LCDPet stores pet data locally on your device.
It does not use internet access, analytics, advertising SDKs, trackers, accounts, cloud sync, or external servers.
The notification permission is used only for local pet care reminders.
## License
LCDPet is licensed under the GNU General Public License v3.0.
See [LICENSE](LICENSE) for details.
## Credits
Audio credits and license/source notes are included inside the app under:
`Credits / Licenses`
The audio credits source file is:
`app/src/main/assets/audio_credits.txt`
@@ -72,6 +72,75 @@ private data class CareJournalEntry(
val happenedAtMs: 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() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -376,6 +445,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
var pottyFaceBag by remember { mutableStateOf(emptyList<String>()) } var pottyFaceBag by remember { mutableStateOf(emptyList<String>()) }
var petName by remember { mutableStateOf("") } var petName by remember { mutableStateOf("") }
var pendingPetName 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 happyOutsideTrip by remember { mutableStateOf(false) }
var petAgeDays by remember { mutableIntStateOf(0) } var petAgeDays by remember { mutableIntStateOf(0) }
@@ -583,6 +654,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
currentWant = save.currentWant currentWant = save.currentWant
petName = save.petName petName = save.petName
pendingPetName = save.petName pendingPetName = save.petName
petTemplateId = save.petTemplateId.ifBlank { "classic" }
pendingPetTemplateId = petTemplateId
isSleeping = save.isSleeping isSleeping = save.isSleeping
sleepMode = save.sleepMode sleepMode = save.sleepMode
roughWakeAlert = save.roughWakeAlert roughWakeAlert = save.roughWakeAlert
@@ -648,7 +721,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
val petAgeText = statAgeText(petAgeDays) val petAgeText = statAgeText(petAgeDays)
val displayPetName = petName.ifBlank { "Pet" } 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 trimmedPendingPetName = pendingPetName.trim()
val normalizedPendingPetName = trimmedPendingPetName.lowercase() val normalizedPendingPetName = trimmedPendingPetName.lowercase()
val isNameValidationActive = !testingMode val isNameValidationActive = !testingMode
@@ -656,6 +729,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
val isReservedPetName = isNameValidationActive && normalizedPendingPetName in reservedPetNames val isReservedPetName = isNameValidationActive && normalizedPendingPetName in reservedPetNames
val hasValidPetNameLetters = trimmedPendingPetName.isNotEmpty() && trimmedPendingPetName.all { it.isLetter() } val hasValidPetNameLetters = trimmedPendingPetName.isNotEmpty() && trimmedPendingPetName.all { it.isLetter() }
val canKeepPetName = hasValidPetNameLetters && !isReservedPetName val canKeepPetName = hasValidPetNameLetters && !isReservedPetName
val pendingPetTemplate = lcdPetTemplateById(pendingPetTemplateId)
val selectedPetTemplate = lcdPetTemplateById(petTemplateId)
val bondRelationship = when { val bondRelationship = when {
bond >= 100 -> "BFF" bond >= 100 -> "BFF"
@@ -796,13 +871,13 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
} }
val petArt = buildAnnotatedString { val petArt = buildAnnotatedString {
append(" /---\\ \n") append(selectedPetTemplate.topLine)
append("( ") append(selectedPetTemplate.facePrefix)
withStyle(SpanStyle(fontWeight = if (pottyFaceOverride.isNotEmpty()) FontWeight.Bold else FontWeight.Normal)) { withStyle(SpanStyle(fontWeight = if (pottyFaceOverride.isNotEmpty()) FontWeight.Bold else FontWeight.Normal)) {
append(petExpression) append(petExpression)
} }
append(" )\n") append(selectedPetTemplate.faceSuffix)
append(" /| |\\ ") append(selectedPetTemplate.feetLine)
} }
val petColor = if (status == "Sick") sickGreen else lcdDark val petColor = if (status == "Sick") sickGreen else lcdDark
@@ -1343,6 +1418,8 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
careJournalEntries = emptyList() careJournalEntries = emptyList()
petName = "" petName = ""
pendingPetName = "" pendingPetName = ""
petTemplateId = "classic"
pendingPetTemplateId = "classic"
happyOutsideTrip = false happyOutsideTrip = false
petAgeDays = 0 petAgeDays = 0
careScore = 0 careScore = 0
@@ -1397,6 +1474,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
tradeInProgress = 0, tradeInProgress = 0,
currentWant = currentWant, currentWant = currentWant,
petName = petName, petName = petName,
petTemplateId = petTemplateId,
isSleeping = isSleeping, isSleeping = isSleeping,
sleepMode = sleepMode, sleepMode = sleepMode,
roughWakeAlert = roughWakeAlert, roughWakeAlert = roughWakeAlert,
@@ -1438,6 +1516,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
tradeInProgress = 0, tradeInProgress = 0,
currentWant = currentWant, currentWant = currentWant,
petName = petName, petName = petName,
petTemplateId = petTemplateId,
isSleeping = isSleeping, isSleeping = isSleeping,
sleepMode = sleepMode, sleepMode = sleepMode,
roughWakeAlert = roughWakeAlert, roughWakeAlert = roughWakeAlert,
@@ -1652,6 +1731,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
tradeInProgress = tradeInProgress, tradeInProgress = tradeInProgress,
currentWant = currentWant, currentWant = currentWant,
petName = petName, petName = petName,
petTemplateId = petTemplateId,
isSleeping = isSleeping, isSleeping = isSleeping,
sleepMode = sleepMode, sleepMode = sleepMode,
roughWakeAlert = roughWakeAlert, roughWakeAlert = roughWakeAlert,
@@ -1700,6 +1780,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
tradeInProgress = tradeInProgressBlocks, tradeInProgress = tradeInProgressBlocks,
currentWant = currentWant, currentWant = currentWant,
petName = petName, petName = petName,
petTemplateId = petTemplateId,
isSleeping = isSleeping, isSleeping = isSleeping,
sleepMode = sleepMode, sleepMode = sleepMode,
roughWakeAlert = roughWakeAlert, roughWakeAlert = roughWakeAlert,
@@ -2310,6 +2391,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
tradeInProgress = tradeInProgress, tradeInProgress = tradeInProgress,
currentWant = currentWant, currentWant = currentWant,
petName = petName, petName = petName,
petTemplateId = petTemplateId,
isSleeping = true, isSleeping = true,
sleepMode = "Auto", sleepMode = "Auto",
roughWakeAlert = roughWakeAlert, roughWakeAlert = roughWakeAlert,
@@ -2470,7 +2552,75 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.spacedBy(6.dp) verticalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text( 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, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -2522,6 +2672,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
lcdDark = lcdDark, lcdDark = lcdDark,
lcdFaded = lcdFaded, lcdFaded = lcdFaded,
onClick = { onClick = {
petTemplateId = pendingPetTemplateId
petName = trimmedPendingPetName.take(12) petName = trimmedPendingPetName.take(12)
pendingPetName = petName pendingPetName = petName
statsMenuOpen = false statsMenuOpen = false
@@ -35,6 +35,7 @@ data class LCDPetRecordSave(
val tradeInProgress: Int = 0, val tradeInProgress: Int = 0,
val currentWant: String = "", val currentWant: String = "",
val petName: String = "", val petName: String = "",
val petTemplateId: String = "classic",
val isSleeping: Boolean = false, val isSleeping: Boolean = false,
val sleepMode: String = "", val sleepMode: String = "",
val roughWakeAlert: Boolean = false, val roughWakeAlert: Boolean = false,
@@ -66,6 +67,7 @@ private object LCDPetSaveKeys {
val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress") val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress")
val CURRENT_WANT = stringPreferencesKey("current_want") val CURRENT_WANT = stringPreferencesKey("current_want")
val PET_NAME = stringPreferencesKey("pet_name") val PET_NAME = stringPreferencesKey("pet_name")
val PET_TEMPLATE_ID = stringPreferencesKey("pet_template_id")
val IS_SLEEPING = booleanPreferencesKey("is_sleeping") val IS_SLEEPING = booleanPreferencesKey("is_sleeping")
val SLEEP_MODE = stringPreferencesKey("sleep_mode") val SLEEP_MODE = stringPreferencesKey("sleep_mode")
val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert") val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert")
@@ -99,6 +101,7 @@ val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0, tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0,
currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "", currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "",
petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "", petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "",
petTemplateId = preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] ?: "classic",
isSleeping = preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false, isSleeping = preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false,
sleepMode = preferences[LCDPetSaveKeys.SLEEP_MODE] ?: "", sleepMode = preferences[LCDPetSaveKeys.SLEEP_MODE] ?: "",
roughWakeAlert = preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] ?: false, roughWakeAlert = preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] ?: false,
@@ -131,6 +134,7 @@ suspend fun Context.saveLCDPetStats(
tradeInProgress: Int, tradeInProgress: Int,
currentWant: String, currentWant: String,
petName: String, petName: String,
petTemplateId: String = "classic",
isSleeping: Boolean, isSleeping: Boolean,
sleepMode: String, sleepMode: String,
roughWakeAlert: Boolean, roughWakeAlert: Boolean,
@@ -160,6 +164,7 @@ suspend fun Context.saveLCDPetStats(
preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress
preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant
preferences[LCDPetSaveKeys.PET_NAME] = petName preferences[LCDPetSaveKeys.PET_NAME] = petName
preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] = petTemplateId
preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping
preferences[LCDPetSaveKeys.SLEEP_MODE] = sleepMode preferences[LCDPetSaveKeys.SLEEP_MODE] = sleepMode
preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] = roughWakeAlert preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] = roughWakeAlert
@@ -0,0 +1,3 @@
Initial v1.0 release candidate.
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.
@@ -0,0 +1,18 @@
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
* Care actions including feed, bathe, nap, walk, potty, pet, and talk
* Random Wants and care reminders
* Optional Android notifications for local pet care alerts
* No ads
* No tracking
* No internet permission
* No gameplay purchases or pay-to-win mechanics
LCDPet stores pet data locally on your device. It does not use accounts, analytics, advertising SDKs, trackers, cloud sync, or external servers.
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

@@ -0,0 +1 @@
Tiny offline LCD-style virtual pet for Android
@@ -0,0 +1 @@
LCDPet