11 Commits
Author SHA1 Message Date
Kris 817fc21cc1 Update source code URL 2026-09-04 18:02:10 -04:00
Kris f84f72e7b6 Update source link to GitLab 2026-07-29 15:29:03 -04:00
Kris a515c4df73 Update v1.2 screenshots 2026-07-29 03:53:55 -04:00
Kris 32bf2d0992 Release LCDPet v1.2 2026-07-29 03:12:37 -04:00
Kris fe566ab88b Prepare LCDPet 1.1 update 2026-07-13 13:14:26 -04:00
Kris e758605c3c Add unique LCDPet launcher icon 2026-07-09 13:14:57 -04:00
Kris 08c2a70d10 Remove Foojay resolver for F-Droid build 2026-07-08 13:06:17 -04:00
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
41 changed files with 4407 additions and 689 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`
+3 -2
View File
@@ -15,8 +15,8 @@ android {
applicationId = "today.notfeelingit.lcdpet"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = 3
versionName = "1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -47,6 +47,7 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation("androidx.datastore:datastore-preferences:1.2.1")
implementation("androidx.work:work-runtime-ktx:2.11.2")
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+2
View File
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
+56 -1
View File
@@ -1,4 +1,17 @@
LCDPet Audio Credits
LCDPet Credits / Licenses
LCDPet Software License
LCDPet is free software licensed under the GNU General Public License v3.0.
You may run, study, share, and modify LCDPet under the terms of that license.
Source code:
https://notfeelingit.today/Kris/LCDPet
The complete license text is included in the source repository's LICENSE file.
AUDIO CREDITS
Weather Channel music:
“Calm Piano Jazz Loop” by ispeakwaves
@@ -26,3 +39,45 @@ App file: tv_off.ogg
License: Creative Commons 0 / CC0
Use: Watch TV CRT power-off sound
Edited for length
Pet excited / activity sound:
“Formula 1 radio” by Fenrir21
Source: Freesound
Sound page: https://freesound.org/people/Fenrir21/sounds/825219/
Original sound ID: 825219
App file: pet_excited.ogg
License: Creative Commons 0 / CC0
Use: PLAY and WALK activity one-shot
Converted and edited for app use
Pet eating sound:
“Beep Beep.wav” by TubbsMedia
Source: Freesound
Sound page: https://freesound.org/people/TubbsMedia/sounds/475981/
Original sound ID: 475981
App file: pet_eating.ogg
License: Creative Commons 0 / CC0
Use: Snack and full-meal eating one-shot
Converted and edited for app use
Pet becoming-sick sound:
“random-beeps” by SkySpeira
Source: Freesound
Sound page: https://freesound.org/people/SkySpeira/sounds/716508/
Original sound ID: 716508
App file: pet_sick.ogg
License: Creative Commons 0 / CC0
Use: One-shot when the pet transitions into Sick
Converted and edited for app use
Urgent Need notification sound:
“Train Door Beep, A.wav” by InspectorJ
Source: Freesound
Sound page: https://freesound.org/people/InspectorJ/sounds/346573/
Creator website: https://www.jshaw.co.uk/
Original sound ID: 346573
App file: need_urgent.ogg
License: Creative Commons Attribution 4.0
Use: Urgent Hunger, Potty, Dirtiness, and Energy notifications
Converted and edited for app use
+816
View File
@@ -0,0 +1,816 @@
LCDPET HOW TO
STARTING A NEW PET
When LCDPet starts without an active pet, the CHOOSE NEW PET screen appears.
Choose one of these pet templates:
Classic
Scruffy
Round Bean
Pocket Pup
Batty
Goblin
Tap a preview to select it.
Enter a name under NAME PET.
Names:
Must contain letters only
Can be up to 12 letters long
Cannot use the reserved names Patch or Petch
KEEP NAME remains unavailable until the name is valid.
Tap KEEP NAME to begin caring for the pet.
MAIN SCREEN
The pet's name and the Stats control appear at the upper left.
The current location and weather appear at the upper right.
Tap the location and weather line to move between Inside and Outside.
Credits / Licenses appears below the location and weather line.
The health hearts appear above the pet.
The line below the pet shows the most urgent current alert, an active Want, or the pet's normal status.
Only one message is shown at a time. An urgent warning can temporarily hide a Want without cancelling it.
The six status bars appear below the pet:
Happiness
Energy
Hunger
Dirtiness
Potty
Boredom
Filled blocks show the current value.
For Happiness and Energy, a higher value is generally better.
For Hunger, Dirtiness, Potty, and Boredom, a higher value means the pet needs more attention.
MENUS AND UNAVAILABLE ACTIONS
Inside contains the CARE and SOCIALIZE menus.
Outside contains WALK and POTTY.
Opening Stats, Care, Socialize, How To, or Credits / Licenses closes other major menus as needed.
Actions are temporarily unavailable while the pet is sleeping, bathing, being traded, talking, or completing another action.
An unavailable button appears faded.
How To appears near the bottom of the screen while the Care and Socialize menus are closed.
INSIDE AND OUTSIDE
Tap the Inside / Outside and weather line near the top of the screen to change location.
Most care and social actions happen Inside.
Walk and Potty are available Outside.
The location cannot be changed while another action is in progress.
A Weak or RecoverWeak pet cannot be taken Outside.
Going Outside normally increases Dirtiness by 1 and fills Happiness.
If the weather is Rain, going Outside normally fills Dirtiness to 10, sets Happiness to 8, and makes the pet Nervous.
Completing an Outside Want avoids the normal Rain penalty and fills Happiness.
Returning Inside sets the weather display to Sunny and usually returns a healthy pet to Calm.
Leaving a pet Outside while the app is closed removes 1 Energy for each hour away.
If enough Energy is lost, the pet falls asleep Outside and receives the outdoor Rough Wake penalties when the app is reopened.
WEATHER
A new pet can begin with Sunny, Cloudy, Rain, or Snow weather.
Weather changes automatically after a random 4, 6, 9, or 12 hours.
The normal weather cycle is:
Sunny changes to Cloudy.
Cloudy changes to Sunny or Rain.
Rain changes to Cloudy.
Snow changes to Sunny.
Weather can change while the pet is Inside.
In good outdoor weather, the pet's mood depends on Bond:
Bond below 40: Nervous
Bond 40 through 89: Calm
Bond 90 or higher: Happy
Rain makes the pet Nervous until Bond reaches 100, unless the outing completed an Outside Want.
Remaining Outside in Rain lowers Happiness by 1 each hour unless the outing completed an Outside Want.
CARE
Open CARE while Inside to find BATHE, FEED, and NAP.
BATHE
BATHE is available when Dirtiness is above zero or the pet currently Wants a Bath.
Bathing removes an indoor accident stain.
Bath effectiveness depends on Bond:
Bond below 30:
The bath lasts about 2 minutes.
Dirtiness is reduced by 3.
Happiness is reduced by 1.
Bond 30 through 59:
The bath lasts about 4 minutes.
Dirtiness is reduced by 6.
Bond 60 through 89:
The bath lasts about 6 minutes.
Dirtiness is completely removed.
Bond 90 or higher:
The bath lasts about 6 minutes.
Dirtiness is completely removed.
Happiness increases by 1.
A Bath Want gives a special result:
Dirtiness becomes 0.
Happiness becomes 10.
Energy increases by 2.
Boredom becomes 0.
A bath adds 1 Care when it reduces Dirtiness.
Bathing a pet made Sick by maximum Dirtiness begins recovery once Dirtiness drops below 10.
FEED
A high Hunger value means the pet needs food.
A low Hunger value means the pet is already full.
FEED lowers Hunger by 1.
Every third successful meal increases Potty by 1.
Every second successful meal increases Happiness by 1.
A successful meal adds 1 Care and may build Bond.
Feeding while Hunger is already at 0 counts as overfeeding.
Three overfeeding attempts add 1 Neglect, remove Health, and can make the pet Sick or Weak.
FEED is unavailable while a pet is Sick, Weak, or recovering specifically because of overfeeding.
A Snack Want is a small treat rather than a full meal.
Completing a Snack Want:
Increases Happiness by 1.
Increases Energy by 1.
Adds 1 Care.
Can build Bond immediately.
Does not change Hunger.
Does not increase Potty.
Does not count as overfeeding.
NAP
NAP is available while Inside whenever Energy is below 10.
A manual Nap lasts a random 2, 3, 4, or 5 hours.
During the Nap, Health, Happiness, and Energy gradually rise.
When the Nap finishes:
Health becomes 10.
Happiness becomes 10.
Energy becomes 10.
If Hunger was below 5, it rises to 5.
If Potty was below 7, it rises to 7.
The pet may therefore need food and the bathroom after waking.
A completed Nap can clear Sick, Weak, Recovering, and RecoverWeak conditions.
SOCIALIZE
Open SOCIALIZE while Inside to find PET, PLAY, and TALK.
Repeating the same social action too many times can raise Boredom.
Changing to another social action lowers Boredom by 1.
PET
PET is available Inside while no other action is running.
Petting:
Increases Happiness by 1.
Adds 1 Care.
May build Bond.
Completing a Pet Want gives twice the normal Bond gain and bypasses the normal Bond cooldown.
PLAY
PLAY requires at least 1 Energy.
PLAY is unavailable while the pet is Weak or RecoverWeak.
Ordinary Play:
Increases Happiness by 1.
Increases Hunger by 1.
Adds 1 Care.
Every second ordinary Play lowers Energy by 1.
Completing a Play Want:
Increases Happiness by 2.
Adds 1 Care.
Does not increase Hunger.
Does not lower Energy.
Does not build Boredom.
Gives twice the normal Bond gain immediately.
TALK
Press and hold TALK while speaking or making sounds.
Release TALK to finish the action.
The microphone graphic changes according to the live sound level.
TALK does not recognize words, create transcripts, save recordings, or upload audio.
The first press may request microphone permission.
After granting permission, release the button and hold TALK again to begin the action.
If microphone permission is denied, TALK cannot be completed until permission is granted.
Completing an ordinary Talk:
Increases Happiness by 1.
Adds 1 Care.
May build Bond.
Completing a Talk Want gives five times the normal Bond gain and bypasses the normal Bond cooldown.
OUTSIDE ACTIONS
Move the pet Outside to find WALK and POTTY.
WALK
WALK requires at least 1 Energy.
WALK is unavailable while the pet is Sick, Weak, Recovering, or RecoverWeak.
Walking:
Restores Health to 10.
Lowers Energy by 1.
Increases Hunger by 1.
Increases Dirtiness by 1.
Adds 1 Care.
POTTY
POTTY is available Outside when the Potty value is above zero.
Using POTTY:
Resets Potty to 0.
Increases Hunger by 1.
Adds 1 Care.
Records the Potty Need as completed.
If Potty is already full while the pet remains Outside, the pet handles it automatically.
This resets Potty to 0 but removes 1 Health.
NEEDS
Needs are ongoing care conditions shown by the status bars and warning messages.
Meeting a Need can add Care and create a completed entry in the Care Journal.
Ignoring a serious Need can add Neglect, remove Health, and cause sickness.
HUNGER NEED
Hunger normally increases by 1 every 2 hours.
When Hunger reaches 10, the pet is starving.
After enough additional missed hunger checks:
The Care Journal records a missed Hunger Need.
Neglect increases by 1.
Health decreases by 1.
The pet becomes Sick, or Weak if Health reaches 0.
Feed the pet before Hunger remains full for too long.
POTTY NEED
Potty normally increases by 1 every 3 hours.
The app warns the player when Potty reaches 9 while the pet is Inside.
If Potty is already full when the next Potty increase happens Inside:
The pet has an accident.
The Care Journal records a missed Potty Need.
Neglect increases by 1.
Potty resets to 0.
Dirtiness increases by 3.
An accident stain appears.
Bathing removes the stain.
DIRTINESS NEED
Dirtiness can increase from outdoor trips, walking, Rain, and indoor accidents.
The app warns the player when Dirtiness reaches 9.
If Dirtiness remains at 10 for about 12 hours:
The Care Journal records a missed Bath Need.
Neglect increases by 1.
Health decreases by 1.
The pet becomes Sick, or Weak if Health reaches 0.
Bathe the pet to correct sickness caused by Dirtiness.
ENERGY NEED
The app warns the player when Energy reaches 1.
Some actions become unavailable when Energy is too low.
If Energy remains at 0 for 30 minutes while no other action is running, the pet falls asleep automatically.
WANTS
A Want is a temporary request from the pet.
A new Want appears after a random 8, 12, 18, or 24 hours.
Each Want lasts 10 minutes.
When a Want appears, the app can send a notification.
If the Want is not completed before it expires, the Care Journal records it as missed.
Missing a Want does not add Neglect.
Possible Wants while Inside:
Snack
Play
Pet
Talk
Outside
Bath joins the indoor Want list at 100 Bond.
Possible Wants while Outside:
Play
Pet
Talk
Those actions are performed Inside, so bring the pet back Inside to complete them.
Completed Wants give special rewards.
Snack:
A small treat that increases Happiness and Energy without affecting Hunger or Potty.
Play:
Avoids the usual Hunger and Energy costs and gives twice the normal Bond gain.
Pet:
Gives twice the normal Bond gain.
Talk:
Gives five times the normal Bond gain.
Outside:
Fills Happiness, avoids the normal Rain entry penalty, and builds Bond immediately.
Bath:
Completely cleans the pet, fills Happiness, adds Energy, and clears Boredom.
BOND
Bond measures the relationship between the player and the pet.
Bond ranges from 0 to 100.
The relationship names are:
0 through 24: New Pet
25 through 49: Settled
50 through 79: Pal
80 through 99: Best Friend
100: BFF
Ordinary Bond growth can happen once every 6 hours.
Some completed Wants bypass that cooldown.
The normal Bond gain increases as Bond rises:
Bond 0 through 20: 2 points
Bond 21 through 50: 3 points
Bond 51 through 80: 5 points
Bond 81 through 99: 7 points
Some Wants multiply that normal gain.
Bond cannot rise above 100.
Repeating one social action 25 times in a row removes 5 Bond once during that streak.
Changing actions resets that streak.
BOREDOM
Boredom can increase when Play, Pet, or Talk is repeated too many times.
The first Boredom increase can happen after a random 3 through 13 repeats.
Further increases happen at additional random intervals if the same action continues.
As Boredom rises, repeated actions remove Happiness and Energy more often.
At Boredom 9 or 10, each triggered penalty removes 2 Happiness and 2 Energy.
Changing to another social action lowers Boredom by 1 and reduces the accumulated penalty.
HEALTH AND STATUS
Health is shown by ten hearts above the pet.
Health can be lost through starvation, overfeeding, maximum Dirtiness, or allowing a full Potty meter to overflow Outside.
The pet's face and status change according to its condition.
Sick pets appear green.
COMMON STATUSES
Happy:
The pet is in a strong positive mood.
Calm:
The pet is comfortable but not highly excited.
Nervous:
Common for a new or low-Bond pet, during Rain, or after a Rough Wake.
Sad:
Happiness is 3 or lower while Inside.
Excited:
A temporary status during actions such as Play or Walk.
Asleep:
The pet is taking a Nap or has fallen asleep automatically.
Sick:
The pet is losing Health because of a serious care problem.
Weak:
Health has reached 0.
Recovering:
The pet is recovering from ordinary sickness.
RecoverWeak:
The pet is in the second stage of recovery after becoming Weak.
RoughWake:
The temporary warning after automatic sleep.
Trading:
The current pet is being traded for a new pet.
SICKNESS AND RECOVERY
A Sick pet loses 1 Health every 30 minutes.
Hunger also continues increasing while the pet is Sick.
Correct the cause of sickness:
Starving:
Feed the pet.
Overfed:
Stop feeding until Hunger rises above the stuffed range.
Dirty:
Bathe the pet until Dirtiness drops below 10.
Correcting the cause begins Recovering.
Recovering is checked about every 10 minutes.
Recovery takes longer after the pet has remained Sick for more checks.
Happiness of 8 or higher can shorten recovery.
A Weak pet must remain Inside with some Energy and Hunger below 10.
After about 2 hours under those conditions, Weak changes to RecoverWeak and Health becomes 1.
After about another 2 hours Inside with Hunger below 10, the pet returns to Calm.
A manual Nap can also clear sickness and weakness when the Nap completes.
Do not take a Sick, Weak, or recovering pet on a Walk.
AUTOMATIC SLEEP AND ROUGH WAKE
If Energy remains at 0 for 30 minutes, the pet falls asleep automatically.
Automatic sleep is saved immediately.
The pet gains 1 Energy for each hour spent asleep, with at least 1 Energy restored when it wakes.
Leaving and returning to the app wakes an automatically sleeping pet.
Automatic waking causes a Rough Wake:
Happiness decreases by 1.
The pet becomes Nervous.
A Rough wake alert appears for about 30 seconds.
The pet remains Nervous for about another 10 seconds.
If the pet fell asleep Outside:
The pet is returned Inside.
Bond decreases by 1.
The normal Rough Wake penalties also apply.
A chosen indoor Nap is safer and gives much stronger recovery.
CARE AND NEGLECT
Care records successful positive actions.
Care increases by 1 for:
A successful Feed
A completed Snack Want
Play
Pet
Talk
Walk
Potty
A Bath that reduces Dirtiness
Neglect records serious harmful care failures.
Neglect increases by 1 for:
Three overfeeding attempts
Starving long enough to become Sick
An indoor Potty accident
Remaining fully dirty long enough to become Sick
Care and Neglect each stop increasing at 999.
A missed Want is recorded in the Care Journal but does not add Neglect.
STATS
Tap Stats below the pet's name to open the Stats panel.
Opening Stats closes the Care and Socialize menus.
Stats contains the Pet Record, Pet Management, and Care Journal.
PET RECORD
The Pet Record shows:
Bond
The current Bond percentage, Bond bar, and relationship name.
Age
The pet's current age record.
Care
The number of successful care actions recorded.
Neglect
The number of serious harmful care failures recorded.
CARE JOURNAL
Open Stats, then tap CARE JOURNAL.
The Care Journal stores up to 50 entries.
The screen shows the eight newest entries matching the selected tab.
Tabs:
NEEDS
Shows Need entries, the result, and when they happened.
WANTS
Shows Want entries, response time, and when they happened.
ALL
Shows Needs and Wants together.
Symbols:
✓ means completed or met.
✕ means missed or expired.
A completed Want can show how quickly the player responded.
If its original start time is unavailable after reopening the app, the response time may show N/A or a dash.
PET MANAGEMENT AND TRADE-IN
Open Stats and find PET MANAGEMENT.
Tap TRADE PET IN to begin the trade-in countdown immediately.
There is no separate confirmation screen.
The new-pet search begins after 2 hours.
A ten-block progress bar and remaining time appear during the wait.
Tap CHANGE MIND at any time during the countdown to cancel the trade-in.
Closing the app does not cancel the countdown.
When the countdown reaches zero, CHANGE MIND is no longer available.
The screen shows FINDING PET for about 5 seconds.
The current pet is then replaced.
Trading resets:
Pet name and template
Bond
Care
Neglect
Status bars
Health status
Care Journal
Active Wants
Sleep state
Location
Other current-pet progress
The CHOOSE NEW PET screen then appears so a new template and name can be selected.
NOTIFICATIONS
On Android 13 or newer, LCDPet requests notification permission when the app opens.
Denying notification permission does not stop gameplay.
It only prevents LCDPet notifications from appearing.
Notifications can be sent for:
New Wants
Critical Hunger
Potty reaching 9 Inside
Dirtiness reaching 9
Energy reaching 1
Tapping a notification opens LCDPet.
SAVING AND TIME AWAY
LCDPet saves the pet's current state on the device.
Saved information includes:
Name and template
Status bars and Health
Bond, Care, and Neglect
Location and weather
Health condition and sickness cause
Accident stain
Active Want
Trade-in state
Sleep state
Care Journal
The trade-in countdown continues using real elapsed time while the app is closed.
Automatic sleep uses real elapsed time while the app is closed.
A pet left Outside loses 1 Energy for each hour the app remains closed.
Most other gameplay timers do not calculate every hour spent away.
They resume from the saved values when the app is reopened.
An active Want can remain after reopening, but its ten-minute expiry period begins again and its original response time may be unavailable.
A manual Nap remains saved, but time spent with the app closed is not counted toward the original Nap duration.
CREDITS AND LICENSES
Tap Credits / Licenses near the upper-right corner of the main screen.
This panel contains acknowledgements, sound credits, license information, and source links.
Web addresses in Credits / Licenses can be tapped to open them.
HOW TO AND CREDITS CONTROLS
How To and Credits / Licenses open as full-screen scrolling panels.
CLOSE returns to the pet.
MOVE moves the MOVE and CLOSE controls to another corner.
The controls cycle through:
Bottom right
Bottom left
Top left
Top right
Moving the controls in How To also moves them in Credits / Licenses, and vice versa.
@@ -14,12 +14,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -30,6 +33,8 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
@@ -40,6 +45,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.delay
@Composable
@@ -51,7 +58,15 @@ fun TouchGrassActivityScreen(
var grassWaveTick by remember { mutableIntStateOf(0) }
var grassWidthPx by remember { mutableIntStateOf(1) }
val grassColumns = 33
val density = LocalDensity.current
val grassFontSize = 12.sp
val grassLineHeight = 13.sp
val estimatedCharacterWidthPx = with(density) {
grassFontSize.toPx() * 0.62f
}
val grassColumns = (
grassWidthPx / estimatedCharacterWidthPx.coerceAtLeast(1f)
).toInt().coerceIn(16, 33)
fun updateGrassFinger(xPosition: Float) {
val safeWidth = maxOf(grassWidthPx, 1)
@@ -77,13 +92,17 @@ fun TouchGrassActivityScreen(
columns = grassColumns
),
fontFamily = FontFamily.Monospace,
fontSize = grassFontSize,
lineHeight = grassLineHeight,
textAlign = TextAlign.Center,
softWrap = false,
maxLines = 7,
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
grassWidthPx = coordinates.size.width
}
.pointerInput(Unit) {
.pointerInput(grassColumns) {
detectDragGestures(
onDragStart = { offset ->
updateGrassFinger(offset.x)
@@ -128,19 +147,59 @@ fun WatchTvActivityScreen(
// 1 = on, 2 = CRT shutting off, 0 = off
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var appInForeground by remember {
mutableStateOf(
lifecycleOwner.lifecycle.currentState.isAtLeast(
Lifecycle.State.RESUMED
)
)
}
var weatherChannelPlayer by remember {
mutableStateOf<MediaPlayer?>(null)
}
var staticChannelPlayer by remember {
mutableStateOf<MediaPlayer?>(null)
}
val tvTextFontSize = with(LocalDensity.current) { 14.dp.toSp() }
val tvTextLineHeight = with(LocalDensity.current) { 18.dp.toSp() }
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
appInForeground = true
}
Lifecycle.Event.ON_PAUSE,
Lifecycle.Event.ON_STOP -> {
appInForeground = false
}
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LaunchedEffect(selectedChannel, tvPowerState) {
tvFrame = 0
while (true) {
delay(if (tvPowerState == 2) 120L else 450L)
delay(if (tvPowerState == 2) 145L else 450L)
tvFrame += 1
}
}
LaunchedEffect(tvPowerState) {
if (tvPowerState == 2) {
delay(420L)
delay(580L)
tvPowerState = 0
}
}
@@ -158,7 +217,12 @@ fun WatchTvActivityScreen(
start()
}
while (powerOffPlayer?.isPlaying == true) {
while (
powerOffPlayer?.isPlaying == true &&
lifecycleOwner.lifecycle.currentState.isAtLeast(
Lifecycle.State.RESUMED
)
) {
delay(50L)
}
} finally {
@@ -167,34 +231,91 @@ fun WatchTvActivityScreen(
}
}
DisposableEffect(selectedChannel, tvPowerState) {
var player: MediaPlayer? = null
if (tvPowerState == 1) {
val soundResource = when (selectedChannel) {
TvChannel.Weather -> R.raw.weather_channel_jazz
TvChannel.NoSignal -> R.raw.tv_static_light
DisposableEffect(tvPowerState) {
val weatherPlayer =
if (tvPowerState == 1) {
MediaPlayer.create(
context,
R.raw.weather_channel_jazz
)?.apply {
isLooping = true
setVolume(0f, 0f)
start()
}
} else {
null
}
val channelVolume = when (selectedChannel) {
TvChannel.Weather -> 0.75f
TvChannel.NoSignal -> 0.60f
val staticPlayer =
if (tvPowerState == 1) {
MediaPlayer.create(
context,
R.raw.tv_static_light
)?.apply {
isLooping = true
setVolume(0f, 0f)
start()
}
} else {
null
}
player = MediaPlayer.create(context, soundResource)?.apply {
isLooping = true
setVolume(channelVolume, channelVolume)
start()
}
}
weatherChannelPlayer = weatherPlayer
staticChannelPlayer = staticPlayer
onDispose {
player?.stop()
player?.release()
player = null
if (weatherChannelPlayer === weatherPlayer) {
weatherChannelPlayer = null
}
if (staticChannelPlayer === staticPlayer) {
staticChannelPlayer = null
}
listOf(weatherPlayer, staticPlayer).forEach { player ->
runCatching {
if (player?.isPlaying == true) {
player.stop()
}
}
player?.release()
}
}
}
LaunchedEffect(
appInForeground,
tvPowerState,
selectedChannel,
weatherChannelPlayer,
staticChannelPlayer
) {
val canHearTv =
appInForeground && tvPowerState == 1
val weatherVolume =
if (canHearTv && selectedChannel == TvChannel.Weather) {
0.75f
} else {
0f
}
val staticVolume =
if (canHearTv && selectedChannel == TvChannel.NoSignal) {
0.60f
} else {
0f
}
weatherChannelPlayer?.setVolume(
weatherVolume,
weatherVolume
)
staticChannelPlayer?.setVolume(
staticVolume,
staticVolume
)
}
Column(
modifier = Modifier
.fillMaxWidth()
@@ -267,25 +388,73 @@ fun WatchTvActivityScreen(
.padding(horizontal = 10.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = if (tvPowerState == 1) {
tvPreviewText(
channel = selectedChannel,
frame = tvFrame,
weatherLabel = weatherLabel
if (
tvPowerState == 1 &&
selectedChannel == TvChannel.Weather
) {
val weatherLines = weatherPreviewText(weatherLabel).lines()
val weatherGraphic = weatherLines.joinToString("\n") {
it.take(11)
}
val weatherDetails = weatherLines.joinToString("\n") {
it.drop(15)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = weatherGraphic,
fontFamily = FontFamily.Monospace,
fontSize = tvTextFontSize,
lineHeight = tvTextLineHeight,
color = lcdDark,
modifier = Modifier.width(92.dp),
softWrap = false,
maxLines = 3
)
} else {
tvPowerOffPreviewText(
frame = tvFrame,
shuttingDown = tvPowerState == 2
Spacer(modifier = Modifier.width(34.dp))
Text(
text = weatherDetails,
fontFamily = FontFamily.Monospace,
fontSize = tvTextFontSize,
lineHeight = tvTextLineHeight,
color = lcdDark,
modifier = Modifier.offset(x = (-10).dp),
softWrap = false,
maxLines = 3
)
},
fontFamily = FontFamily.Monospace,
textAlign = if (tvPowerState == 1) TextAlign.Start else TextAlign.Center,
color = if (tvPowerState == 1) lcdDark else Color.White,
modifier = Modifier.fillMaxWidth(),
softWrap = !(tvPowerState == 1 && selectedChannel == TvChannel.NoSignal)
)
}
} else {
Text(
text = if (tvPowerState == 1) {
tvPreviewText(
channel = selectedChannel,
frame = tvFrame,
weatherLabel = weatherLabel
)
} else {
tvPowerOffPreviewText(
frame = tvFrame,
shuttingDown = tvPowerState == 2
)
},
fontFamily = FontFamily.Monospace,
fontSize = tvTextFontSize,
lineHeight = tvTextLineHeight,
textAlign = if (tvPowerState == 1) {
TextAlign.Start
} else {
TextAlign.Center
},
color = if (tvPowerState == 1) lcdDark else Color.White,
modifier = Modifier.fillMaxWidth(),
softWrap = false,
maxLines = 5
)
}
}
}
@@ -418,12 +587,19 @@ private fun weatherPreviewText(weatherLabel: String): String {
val normalizedWeather = weatherLabel.lowercase()
val conditionText = when {
normalizedWeather.contains("snow") -> "SNOWING"
normalizedWeather.contains("rain") -> "RAINING"
normalizedWeather.contains("cloud") || normalizedWeather.contains("overcast") -> "CLOUDY"
else -> "SUNNY"
}
val iconLines = when (conditionText) {
"SNOWING" -> listOf(
" .--. ",
" (____) ",
" * * * "
)
"RAINING" -> listOf(
" .--. ",
" (____) ",
@@ -0,0 +1,266 @@
package today.notfeelingit.lcdpet
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import java.util.concurrent.TimeUnit
private const val LCDPET_WORKER_CHANNEL_ID = "lcdpet_alerts"
private const val LCDPET_WORKER_URGENT_CHANNEL_ID = "lcdpet_urgent_alerts_v2"
private const val INPUT_WORK_TYPE = "work_type"
private const val INPUT_NOTIFICATION_ID = "notification_id"
private const val INPUT_NOTIFICATION_TITLE = "notification_title"
private const val INPUT_NOTIFICATION_MESSAGE = "notification_message"
private const val INPUT_URGENT_NEED = "urgent_need"
private const val WORK_TYPE_NOTIFICATION = "notification"
private const val WORK_TYPE_WANT = "want"
private const val LCDPET_BACKGROUND_TEST_WORK = "lcdpet_background_test"
class LCDPetNotificationWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
val workType =
inputData.getString(INPUT_WORK_TYPE) ?: WORK_TYPE_NOTIFICATION
if (workType == WORK_TYPE_WANT) {
val createdWant =
applicationContext.createLCDPetWantIfDue()
?: return Result.success()
sendLCDPetWorkerNotification(
context = applicationContext,
notificationId = 2001,
title = "LCDPet",
message = wantNotificationText(createdWant)
)
return Result.success()
}
val notificationId = inputData.getInt(INPUT_NOTIFICATION_ID, -1)
val title = inputData.getString(INPUT_NOTIFICATION_TITLE) ?: "LCDPet"
val message = inputData.getString(INPUT_NOTIFICATION_MESSAGE).orEmpty()
val urgentNeed = inputData.getBoolean(INPUT_URGENT_NEED, false)
if (notificationId < 0 || message.isBlank()) {
return Result.failure()
}
sendLCDPetWorkerNotification(
context = applicationContext,
notificationId = notificationId,
title = title,
message = message,
urgentNeed = urgentNeed
)
return Result.success()
}
}
fun scheduleLCDPetNotificationWork(
context: Context,
uniqueWorkName: String,
delayMs: Long,
notificationId: Int,
message: String,
title: String = "LCDPet",
urgentNeed: Boolean = false
) {
val request = OneTimeWorkRequestBuilder<LCDPetNotificationWorker>()
.setInitialDelay(delayMs.coerceAtLeast(0L), TimeUnit.MILLISECONDS)
.setInputData(
workDataOf(
INPUT_WORK_TYPE to WORK_TYPE_NOTIFICATION,
INPUT_NOTIFICATION_ID to notificationId,
INPUT_NOTIFICATION_TITLE to title,
INPUT_NOTIFICATION_MESSAGE to message,
INPUT_URGENT_NEED to urgentNeed
)
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName,
ExistingWorkPolicy.REPLACE,
request
)
}
fun scheduleLCDPetWantWork(
context: Context,
uniqueWorkName: String,
delayMs: Long
) {
val request = OneTimeWorkRequestBuilder<LCDPetNotificationWorker>()
.setInitialDelay(
delayMs.coerceAtLeast(0L),
TimeUnit.MILLISECONDS
)
.setInputData(
workDataOf(
INPUT_WORK_TYPE to WORK_TYPE_WANT
)
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName,
ExistingWorkPolicy.REPLACE,
request
)
}
fun cancelLCDPetNotificationWork(
context: Context,
uniqueWorkName: String
) {
WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName)
}
fun scheduleLCDPetBackgroundTest(context: Context) {
scheduleLCDPetNotificationWork(
context = context,
uniqueWorkName = LCDPET_BACKGROUND_TEST_WORK,
delayMs = 30_000L,
notificationId = 1099,
message = "Background worker test: beep boop."
)
}
private fun wantNotificationText(
createdWant: LCDPetCreatedWant
): String {
val notificationPetName =
createdWant.petName.ifBlank { "Your pet" }
return when (createdWant.wantName) {
"Snack" -> "$notificationPetName wants a snack."
"Play" -> "$notificationPetName wants to play."
"Pet" -> "$notificationPetName wants pets."
"Talk" -> "$notificationPetName is looking for you."
"Outside" -> "$notificationPetName wants to go outside."
"Bath" -> "$notificationPetName wants a bath."
else -> "$notificationPetName wants your attention."
}
}
private fun sendLCDPetWorkerNotification(
context: Context,
notificationId: Int,
title: String,
message: String,
urgentNeed: Boolean = false
) {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (urgentNeed) {
val urgentSoundUri =
Uri.parse(
"android.resource://${context.packageName}/${R.raw.need_urgent}"
)
val urgentAudioAttributes =
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
val urgentChannel = NotificationChannel(
LCDPET_WORKER_URGENT_CHANNEL_ID,
"LCDPet Urgent Needs",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Urgent Hunger, Potty, Bath, and Energy alerts."
setSound(urgentSoundUri, urgentAudioAttributes)
enableVibration(true)
setVibrationPattern(longArrayOf(0L, 250L))
}
manager.createNotificationChannel(urgentChannel)
} else {
val channel = NotificationChannel(
LCDPET_WORKER_CHANNEL_ID,
"LCDPet Alerts",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Pet care reminders and LCDPet status alerts."
}
manager.createNotificationChannel(channel)
}
}
val launchIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
context,
notificationId,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val channelId =
if (urgentNeed) {
LCDPET_WORKER_URGENT_CHANNEL_ID
} else {
LCDPET_WORKER_CHANNEL_ID
}
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (urgentNeed && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
val urgentSoundUri =
Uri.parse(
"android.resource://${context.packageName}/${R.raw.need_urgent}"
)
builder.setSound(urgentSoundUri)
builder.setVibrate(longArrayOf(0L, 250L))
}
NotificationManagerCompat
.from(context)
.notify(notificationId, builder.build())
}
File diff suppressed because it is too large Load Diff
@@ -15,6 +15,7 @@ val Context.lcdPetDataStore by preferencesDataStore(name = "lcdpet_save")
data class LCDPetRecordSave(
val bond: Int = 0,
val petAgeDays: Int = 0,
val nextAgeAtMs: Long = 0L,
val careScore: Int = 0,
val neglectScore: Int = 0,
val hunger: Int = 5,
@@ -24,6 +25,15 @@ data class LCDPetRecordSave(
val dirtiness: Int = 0,
val potty: Int = 0,
val boredom: Int = 0,
val starvingCount: Int = 0,
val recoveryCount: Int = 0,
val sickCount: Int = 0,
val playEnergyCount: Int = 0,
val sameActionStreak: Int = 0,
val boredomTriggerAt: Int = 0,
val boredomPenaltyCount: Int = 0,
val ultraTrollTriggered: Boolean = false,
val lastActionName: String = "",
val location: String = "Inside",
val weather: String = "Sunny",
val status: String = "Nervous",
@@ -33,19 +43,36 @@ data class LCDPetRecordSave(
val isTradingInPet: Boolean = false,
val isFindingNewPet: Boolean = false,
val tradeInProgress: Int = 0,
val tradeInStartedAtMs: Long = 0L,
val currentWant: String = "",
val currentWantStartedAtMs: Long = 0L,
val nextWantAtMs: Long = 0L,
val petName: String = "",
val petTemplateId: String = "classic",
val isSleeping: Boolean = false,
val sleepMode: String = "",
val roughWakeAlert: Boolean = false,
val autoSleepStartedAtMs: Long = 0L,
val outsideEnergyNextTickAtMs: Long = 0L,
val nextWeatherAtMs: Long = 0L,
val bondReadyAtMs: Long = 0L,
val careCycleNextTickAtMs: Long = 0L,
val rainHappinessNextTickAtMs: Long = 0L,
val pottyNextTickAtMs: Long = 0L,
val dirtyHealthNextTickAtMs: Long = 0L,
val careJournalRaw: String = "",
val lastSavedAtMs: Long = System.currentTimeMillis()
)
data class LCDPetCreatedWant(
val wantName: String,
val petName: String
)
private object LCDPetSaveKeys {
val BOND = intPreferencesKey("bond")
val PET_AGE_DAYS = intPreferencesKey("pet_age_days")
val NEXT_AGE_AT_MS = longPreferencesKey("next_age_at_ms")
val CARE_SCORE = intPreferencesKey("care_score")
val NEGLECT_SCORE = intPreferencesKey("neglect_score")
val HUNGER = intPreferencesKey("hunger")
@@ -55,6 +82,17 @@ private object LCDPetSaveKeys {
val DIRTINESS = intPreferencesKey("dirtiness")
val POTTY = intPreferencesKey("potty")
val BOREDOM = intPreferencesKey("boredom")
val STARVING_COUNT = intPreferencesKey("starving_count")
val RECOVERY_COUNT = intPreferencesKey("recovery_count")
val SICK_COUNT = intPreferencesKey("sick_count")
val PLAY_ENERGY_COUNT = intPreferencesKey("play_energy_count")
val SAME_ACTION_STREAK = intPreferencesKey("same_action_streak")
val BOREDOM_TRIGGER_AT = intPreferencesKey("boredom_trigger_at")
val BOREDOM_PENALTY_COUNT =
intPreferencesKey("boredom_penalty_count")
val ULTRA_TROLL_TRIGGERED =
booleanPreferencesKey("ultra_troll_triggered")
val LAST_ACTION_NAME = stringPreferencesKey("last_action_name")
val LOCATION = stringPreferencesKey("location")
val WEATHER = stringPreferencesKey("weather")
val STATUS = stringPreferencesKey("status")
@@ -64,13 +102,34 @@ private object LCDPetSaveKeys {
val IS_TRADING_IN_PET = booleanPreferencesKey("is_trading_in_pet")
val IS_FINDING_NEW_PET = booleanPreferencesKey("is_finding_new_pet")
val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress")
val TRADE_IN_STARTED_AT_MS = longPreferencesKey("trade_in_started_at_ms")
val CURRENT_WANT = stringPreferencesKey("current_want")
val CURRENT_WANT_STARTED_AT_MS =
longPreferencesKey("current_want_started_at_ms")
val NEXT_WANT_AT_MS = longPreferencesKey("next_want_at_ms")
val PET_NAME = stringPreferencesKey("pet_name")
val PET_TEMPLATE_ID = stringPreferencesKey("pet_template_id")
val IS_SLEEPING = booleanPreferencesKey("is_sleeping")
val SLEEP_MODE = stringPreferencesKey("sleep_mode")
val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert")
val AUTO_SLEEP_STARTED_AT_MS = longPreferencesKey("auto_sleep_started_at_ms")
val OUTSIDE_ENERGY_NEXT_TICK_AT_MS =
longPreferencesKey("outside_energy_next_tick_at_ms")
val NEXT_WEATHER_AT_MS =
longPreferencesKey("next_weather_at_ms")
val BOND_READY_AT_MS =
longPreferencesKey("bond_ready_at_ms")
val CARE_CYCLE_NEXT_TICK_AT_MS =
longPreferencesKey("care_cycle_next_tick_at_ms")
val RAIN_HAPPINESS_NEXT_TICK_AT_MS =
longPreferencesKey("rain_happiness_next_tick_at_ms")
val POTTY_NEXT_TICK_AT_MS =
longPreferencesKey("potty_next_tick_at_ms")
val DIRTY_HEALTH_NEXT_TICK_AT_MS =
longPreferencesKey("dirty_health_next_tick_at_ms")
val CARE_JOURNAL = stringPreferencesKey("care_journal")
val APPEARANCE = stringPreferencesKey("appearance")
val PET_HOME_COLOR = stringPreferencesKey("pet_home_color")
val LAST_SAVED_AT_MS = longPreferencesKey("last_saved_at_ms")
}
@@ -79,6 +138,8 @@ val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
LCDPetRecordSave(
bond = preferences[LCDPetSaveKeys.BOND] ?: 0,
petAgeDays = preferences[LCDPetSaveKeys.PET_AGE_DAYS] ?: 0,
nextAgeAtMs =
preferences[LCDPetSaveKeys.NEXT_AGE_AT_MS] ?: 0L,
careScore = preferences[LCDPetSaveKeys.CARE_SCORE] ?: 0,
neglectScore = preferences[LCDPetSaveKeys.NEGLECT_SCORE] ?: 0,
hunger = preferences[LCDPetSaveKeys.HUNGER] ?: 5,
@@ -88,6 +149,24 @@ val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
dirtiness = preferences[LCDPetSaveKeys.DIRTINESS] ?: 0,
potty = preferences[LCDPetSaveKeys.POTTY] ?: 0,
boredom = preferences[LCDPetSaveKeys.BOREDOM] ?: 0,
starvingCount =
preferences[LCDPetSaveKeys.STARVING_COUNT] ?: 0,
recoveryCount =
preferences[LCDPetSaveKeys.RECOVERY_COUNT] ?: 0,
sickCount =
preferences[LCDPetSaveKeys.SICK_COUNT] ?: 0,
playEnergyCount =
preferences[LCDPetSaveKeys.PLAY_ENERGY_COUNT] ?: 0,
sameActionStreak =
preferences[LCDPetSaveKeys.SAME_ACTION_STREAK] ?: 0,
boredomTriggerAt =
preferences[LCDPetSaveKeys.BOREDOM_TRIGGER_AT] ?: 0,
boredomPenaltyCount =
preferences[LCDPetSaveKeys.BOREDOM_PENALTY_COUNT] ?: 0,
ultraTrollTriggered =
preferences[LCDPetSaveKeys.ULTRA_TROLL_TRIGGERED] ?: false,
lastActionName =
preferences[LCDPetSaveKeys.LAST_ACTION_NAME] ?: "",
location = preferences[LCDPetSaveKeys.LOCATION] ?: "Inside",
weather = preferences[LCDPetSaveKeys.WEATHER] ?: "Sunny",
status = preferences[LCDPetSaveKeys.STATUS] ?: "Nervous",
@@ -97,17 +176,75 @@ val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
isTradingInPet = preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] ?: false,
isFindingNewPet = preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] ?: false,
tradeInProgress = preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] ?: 0,
tradeInStartedAtMs = preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] ?: 0L,
currentWant = preferences[LCDPetSaveKeys.CURRENT_WANT] ?: "",
currentWantStartedAtMs =
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] ?: 0L,
nextWantAtMs =
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] ?: 0L,
petName = preferences[LCDPetSaveKeys.PET_NAME] ?: "",
petTemplateId = preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] ?: "classic",
isSleeping = preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false,
sleepMode = preferences[LCDPetSaveKeys.SLEEP_MODE] ?: "",
roughWakeAlert = preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] ?: false,
autoSleepStartedAtMs = preferences[LCDPetSaveKeys.AUTO_SLEEP_STARTED_AT_MS] ?: 0L,
outsideEnergyNextTickAtMs =
preferences[LCDPetSaveKeys.OUTSIDE_ENERGY_NEXT_TICK_AT_MS] ?: 0L,
nextWeatherAtMs =
preferences[LCDPetSaveKeys.NEXT_WEATHER_AT_MS] ?: 0L,
bondReadyAtMs =
preferences[LCDPetSaveKeys.BOND_READY_AT_MS] ?: 0L,
careCycleNextTickAtMs =
preferences[LCDPetSaveKeys.CARE_CYCLE_NEXT_TICK_AT_MS] ?: 0L,
rainHappinessNextTickAtMs =
preferences[LCDPetSaveKeys.RAIN_HAPPINESS_NEXT_TICK_AT_MS] ?: 0L,
pottyNextTickAtMs =
preferences[LCDPetSaveKeys.POTTY_NEXT_TICK_AT_MS] ?: 0L,
dirtyHealthNextTickAtMs =
preferences[LCDPetSaveKeys.DIRTY_HEALTH_NEXT_TICK_AT_MS] ?: 0L,
careJournalRaw = preferences[LCDPetSaveKeys.CARE_JOURNAL] ?: "",
lastSavedAtMs = preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] ?: System.currentTimeMillis()
)
}
val Context.lcdPetAppearanceFlow: Flow<String>
get() = lcdPetDataStore.data.map { preferences ->
preferences[LCDPetSaveKeys.APPEARANCE] ?: "System"
}
suspend fun Context.setLCDPetAppearance(appearance: String) {
val savedAppearance =
appearance.takeIf { it in setOf("System", "Light", "Dark") }
?: "System"
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.APPEARANCE] = savedAppearance
}
}
val Context.lcdPetHomeColorFlow: Flow<String>
get() = lcdPetDataStore.data.map { preferences ->
preferences[LCDPetSaveKeys.PET_HOME_COLOR] ?: "LCD"
}
suspend fun Context.setLCDPetHomeColor(homeColor: String) {
val savedHomeColor =
homeColor.takeIf {
it in setOf("LCD", "R", "G", "B", "Y", "P", "K")
} ?: "LCD"
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.PET_HOME_COLOR] = savedHomeColor
}
}
suspend fun Context.setLCDPetNextAgeAtMs(nextAgeAtMs: Long) {
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.NEXT_AGE_AT_MS] =
nextAgeAtMs.coerceAtLeast(0L)
}
}
suspend fun Context.saveLCDPetStats(
bond: Int,
petAgeDays: Int,
@@ -120,6 +257,15 @@ suspend fun Context.saveLCDPetStats(
dirtiness: Int,
potty: Int,
boredom: Int,
starvingCount: Int,
recoveryCount: Int,
sickCount: Int,
playEnergyCount: Int,
sameActionStreak: Int,
boredomTriggerAt: Int,
boredomPenaltyCount: Int,
ultraTrollTriggered: Boolean,
lastActionName: String,
location: String,
weather: String,
status: String,
@@ -129,12 +275,22 @@ suspend fun Context.saveLCDPetStats(
isTradingInPet: Boolean,
isFindingNewPet: Boolean,
tradeInProgress: Int,
tradeInStartedAtMs: Long = 0L,
currentWant: String,
petName: String,
petTemplateId: String = "classic",
isSleeping: Boolean,
sleepMode: String,
roughWakeAlert: Boolean,
autoSleepStartedAtMs: Long,
currentWantStartedAtMs: Long = 0L,
nextWantAtMs: Long = 0L,
nextWeatherAtMs: Long,
bondReadyAtMs: Long,
careCycleNextTickAtMs: Long,
rainHappinessNextTickAtMs: Long,
pottyNextTickAtMs: Long,
dirtyHealthNextTickAtMs: Long,
careJournalRaw: String? = null
) {
lcdPetDataStore.edit { preferences ->
@@ -149,6 +305,24 @@ suspend fun Context.saveLCDPetStats(
preferences[LCDPetSaveKeys.DIRTINESS] = dirtiness
preferences[LCDPetSaveKeys.POTTY] = potty
preferences[LCDPetSaveKeys.BOREDOM] = boredom
preferences[LCDPetSaveKeys.STARVING_COUNT] =
starvingCount.coerceAtLeast(0)
preferences[LCDPetSaveKeys.RECOVERY_COUNT] =
recoveryCount.coerceAtLeast(0)
preferences[LCDPetSaveKeys.SICK_COUNT] =
sickCount.coerceAtLeast(0)
preferences[LCDPetSaveKeys.PLAY_ENERGY_COUNT] =
playEnergyCount.coerceAtLeast(0)
preferences[LCDPetSaveKeys.SAME_ACTION_STREAK] =
sameActionStreak.coerceAtLeast(0)
preferences[LCDPetSaveKeys.BOREDOM_TRIGGER_AT] =
boredomTriggerAt.coerceAtLeast(0)
preferences[LCDPetSaveKeys.BOREDOM_PENALTY_COUNT] =
boredomPenaltyCount.coerceAtLeast(0)
preferences[LCDPetSaveKeys.ULTRA_TROLL_TRIGGERED] =
ultraTrollTriggered
preferences[LCDPetSaveKeys.LAST_ACTION_NAME] =
lastActionName
preferences[LCDPetSaveKeys.LOCATION] = location
preferences[LCDPetSaveKeys.WEATHER] = weather
preferences[LCDPetSaveKeys.STATUS] = status
@@ -158,8 +332,26 @@ suspend fun Context.saveLCDPetStats(
preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] = isTradingInPet
preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] = isFindingNewPet
preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress
preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] = tradeInStartedAtMs
preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] =
currentWantStartedAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] =
nextWantAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.NEXT_WEATHER_AT_MS] =
nextWeatherAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.BOND_READY_AT_MS] =
bondReadyAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.CARE_CYCLE_NEXT_TICK_AT_MS] =
careCycleNextTickAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.RAIN_HAPPINESS_NEXT_TICK_AT_MS] =
rainHappinessNextTickAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.POTTY_NEXT_TICK_AT_MS] =
pottyNextTickAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.DIRTY_HEALTH_NEXT_TICK_AT_MS] =
dirtyHealthNextTickAtMs.coerceAtLeast(0L)
preferences[LCDPetSaveKeys.PET_NAME] = petName
preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] = petTemplateId
preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping
preferences[LCDPetSaveKeys.SLEEP_MODE] = sleepMode
preferences[LCDPetSaveKeys.ROUGH_WAKE_ALERT] = roughWakeAlert
@@ -170,3 +362,88 @@ suspend fun Context.saveLCDPetStats(
preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = System.currentTimeMillis()
}
}
suspend fun Context.createLCDPetWantIfDue(
nowMs: Long = System.currentTimeMillis()
): LCDPetCreatedWant? {
var createdWant: LCDPetCreatedWant? = null
lcdPetDataStore.edit { preferences ->
val currentWant =
preferences[LCDPetSaveKeys.CURRENT_WANT].orEmpty()
val nextWantAtMs =
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] ?: 0L
val petName =
preferences[LCDPetSaveKeys.PET_NAME].orEmpty()
val location =
preferences[LCDPetSaveKeys.LOCATION] ?: "Inside"
val bond =
preferences[LCDPetSaveKeys.BOND] ?: 0
val status =
preferences[LCDPetSaveKeys.STATUS] ?: "Nervous"
val isSleeping =
preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false
val isTradingInPet =
preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] ?: false
val isFindingNewPet =
preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] ?: false
val canCreateWant =
currentWant.isBlank() &&
nextWantAtMs > 0L &&
nowMs >= nextWantAtMs &&
petName.isNotBlank() &&
status !in setOf("Sick", "Weak") &&
!isSleeping &&
!isTradingInPet &&
!isFindingNewPet
if (!canCreateWant) {
return@edit
}
val possibleWants =
if (location == "Inside") {
listOf("Snack", "Play", "Pet", "Talk", "Outside") +
if (bond >= 100) listOf("Bath") else emptyList()
} else {
listOf("Play", "Pet", "Talk")
}
val wantName = possibleWants.random()
preferences[LCDPetSaveKeys.CURRENT_WANT] = wantName
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = nowMs
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = 0L
preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = nowMs
createdWant = LCDPetCreatedWant(
wantName = wantName,
petName = petName
)
}
return createdWant
}
suspend fun Context.clearLCDPetWants() {
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.CURRENT_WANT] = ""
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = 0L
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = 0L
}
}
suspend fun Context.setLCDPetNextWantAtMs(nextWantAtMs: Long) {
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] =
nextWantAtMs.coerceAtLeast(0L)
}
}
suspend fun Context.setLCDPetOutsideEnergyNextTickAtMs(nextTickAtMs: Long) {
lcdPetDataStore.edit { preferences ->
preferences[LCDPetSaveKeys.OUTSIDE_ENERGY_NEXT_TICK_AT_MS] =
nextTickAtMs.coerceAtLeast(0L)
}
}
@@ -1,15 +1,16 @@
package today.notfeelingit.lcdpet.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
@@ -17,42 +18,16 @@ private val DarkColorScheme = darkColorScheme(
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun LCDPetTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
colorScheme =
if (darkTheme) DarkColorScheme
else LightColorScheme,
typography = Typography,
content = content
)
}
}
@@ -1,170 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#C6E2AD" />
</shape>
@@ -1,30 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
<path android:fillColor="#789C60" android:pathData="M18,18 Q18,10 26,10 L82,10 Q90,10 90,18 L90,90 Q90,98 82,98 L26,98 Q18,98 18,90 Z"/>
<path android:fillColor="#D3EBBC" android:pathData="M26,26 Q26,20 32,20 L76,20 Q82,20 82,26 L82,82 Q82,88 76,88 L32,88 Q26,88 26,82 Z"/>
<path android:fillColor="#465F3A" android:pathData="M36,42 L44,42 L44,50 L36,50 Z"/>
<path android:fillColor="#465F3A" android:pathData="M64,42 L72,42 L72,50 L64,50 Z"/>
<path android:fillColor="#8CB074" android:pathData="M30,54 L36,54 L36,60 L30,60 Z"/>
<path android:fillColor="#8CB074" android:pathData="M72,54 L78,54 L78,60 L72,60 Z"/>
<path android:fillColor="#465F3A" android:pathData="M45,62 L63,62 L63,67 L45,67 Z"/>
<path android:fillColor="#465F3A" android:pathData="M38,78 L46,78 L46,83 L38,83 Z"/>
<path android:fillColor="#465F3A" android:pathData="M62,78 L70,78 L70,83 L62,83 Z"/>
<path android:fillColor="#5A7A48" android:pathData="M49,11 Q49,8 52,8 L56,8 Q59,8 59,11 L59,18 L49,18 Z"/>
</vector>
@@ -3,4 +3,4 @@
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
</adaptive-icon>
@@ -3,4 +3,4 @@
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

@@ -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,10 @@
LCDPet 1.2 adds:
• Snow weather and Home Renovation colors
• Full meals, Snack Wants, and improved Want safety
• Pet activity sounds and sound-detected TALK
• Better notification sound, vibration, and foreground behavior
• Responsive layouts, aligned status meters, and appearance controls
• Continuous TV channels that resume in place
• Improved Age, Nap, care-cycle, weather, Bond, Potty, and health persistence
• Numerous notification, lifecycle, and stability fixes
@@ -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: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

@@ -0,0 +1 @@
Tiny offline LCD-style virtual pet for Android
@@ -0,0 +1 @@
LCDPet
-3
View File
@@ -11,9 +11,6 @@ pluginManagement {
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {